UIHostingController должен расширяться, чтобы соответствовать содержимому
У меня есть обычай UIViewControllerRepresentable
(код, связанный с макетом, показан ниже). Это пытается воспроизвести собственный SwiftUIScrollView
, за исключением того, что он прокручивается снизу, кроме верха.
Просмотр иерархии
view: UIView
|
\- scrollView: UIScrollView
|
\- innerView: UIView
|
\- hostingController.view: SwiftUI hosting view
Все это работает по назначению при инициализации представления. Представление размещения заполняется своим содержимым, а ограничения гарантируют, что представление прокруткиcontentSize
установлен правильно.
Однако, когда содержимое вида размещения изменяется, hostingController.view
не меняет размер, чтобы соответствовать его содержимому.
Зеленый: как и предполагалось, представление прокрутки соответствует размеру контроллера представления хоста.
Синий: само представление хостинга. Он сохраняет размер, который был при первой загрузке, и не расходуется должным образом.
Красный: представление стека в представлении хостинга. На этом снимке экрана содержимое было добавлено в стек, что привело к его расширению. В результате вы можете увидеть разницу в размере.
UIHostingController (синий) должен расширяться, чтобы соответствовать его содержимому (красный).
Размер содержимого прокрутки не задан явно, потому что это обрабатывается автоматической компоновкой.
Ниже показан код ограничения, если это помогает.
class UIBottomScrollViewController<Content: View>: UIViewController, UIScrollViewDelegate {
var hostingController: UIHostingController<Content>! = nil
init(rootView: Content) {
self.hostingController = UIHostingController<Content>(rootView: rootView)
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
var scrollView: UIScrollView = UIScrollView()
var innerView = UIView()
override func loadView() {
self.view = UIView()
self.addChild(hostingController)
view.addSubview(scrollView)
scrollView.addSubview(innerView)
innerView.addSubview(hostingController.view)
scrollView.delegate = self
scrollView.scrollsToTop = true
scrollView.isScrollEnabled = true
scrollView.clipsToBounds = false
scrollView.layoutMargins = .zero
scrollView.preservesSuperviewLayoutMargins = true
scrollView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
scrollView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
scrollView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
innerView.topAnchor.constraint(equalTo: scrollView.topAnchor).isActive = true
innerView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
innerView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
innerView.leftAnchor.constraint(equalTo: scrollView.leftAnchor).isActive = true
innerView.rightAnchor.constraint(equalTo: scrollView.rightAnchor).isActive = true
innerView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor).isActive = true
hostingController.view.topAnchor.constraint(equalTo: innerView.topAnchor).isActive = true
hostingController.view.leftAnchor.constraint(equalTo: innerView.leftAnchor).isActive = true
hostingController.view.rightAnchor.constraint(equalTo: innerView.rightAnchor).isActive = true
hostingController.view.bottomAnchor.constraint(equalTo: innerView.bottomAnchor).isActive = true
hostingController.view.autoresizingMask = []
hostingController.view.layoutMargins = .zero
hostingController.view.insetsLayoutMarginsFromSafeArea = false
hostingController.view.translatesAutoresizingMaskIntoConstraints = false
scrollView.autoresizingMask = []
scrollView.layoutMargins = .zero
scrollView.insetsLayoutMarginsFromSafeArea = false
scrollView.translatesAutoresizingMaskIntoConstraints = false
innerView.autoresizingMask = []
innerView.layoutMargins = .zero
innerView.insetsLayoutMarginsFromSafeArea = false
innerView.translatesAutoresizingMaskIntoConstraints = false
hostingController.didMove(toParent: self)
scrollView.keyboardDismissMode = .interactive
}
}
struct BottomScrollView<Content: View>: UIViewControllerRepresentable {
var content: () -> Content
init(@ViewBuilder content: @escaping () -> Content) {
self.content = content
}
func makeUIViewController(context: Context) -> UIBottomScrollViewController<Content> {
let vc = UIBottomScrollViewController(rootView: self.content())
return vc
}
func updateUIViewController(_ viewController: UIBottomScrollViewController<Content>, context: Context) {
viewController.hostingController.rootView = self.content()
}
}
6 ответов
Я столкнулся с той же проблемой с подобной иерархией представлений, включающей UIHostingController
и прокрутил просмотры, и нашел уродливый хак, чтобы заставить его работать. Обычно я добавляю ограничение по высоте и обновляю константу вручную:
private var heightConstraint: NSLayoutConstraint?
...
override func viewDidLoad() {
...
heightConstraint = viewHost.view.heightAnchor.constraint(equalToConstant: 0)
...
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
//
viewHost.view.sizeToFit()
heightConstraint?.constant = viewHost.view.bounds.height
heightConstraint?.isActive = true
}
Это ужасный код, но это единственное, что заставило его работать.
Для меня решение было намного проще, чем любой другой ответ, который я вижу здесь (ни один из которых не работал), хотя мне потребовалось некоторое время, чтобы его найти.
Все, что я сделал, это создал тонкий подкласс этих вызовов.
viewDidLayoutSubviews()
class SelfSizingHostingController<Content>: UIHostingController<Content> where Content: View {
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
self.view.invalidateIntrinsicContentSize()
}
}
Как и в исходном вопросе, у меня есть представление SwiftUI, которое я размещаю внутри
В моем случае все было действительно так просто. Это работает для меня в iOS 14+ (не тестировалось на iOS 13), где изменение содержимого SwiftUI, которое привело бы к новому внутреннему размеру, правильно обновляет мой макет UIKit на основе автозапуска в режиме прокрутки. Честно говоря, это похоже на ошибку, что это не неявное поведение
Обновленный ответ для iOS 16:
Теперь вы можете просто установитьyourHostingController.sizingOptions = [.intrinsicContentSize]
и он будет автоматически обновлять/аннулировать внутренний размер содержимого при изменении представления swiftUI (даже при изменении внутреннего состояния).
Если вы встраиваете всплывающее окно, проверьте другой вариант изменения размера (.preferredContentSize
), чтобы правильно изменить размер всплывающих окон
Это воспроизводит то, что говорил @Rengers, но я хотел включить мое решение, на то, чтобы выяснить это, у меня ушло довольно много времени.
Надеюсь сэкономить время
struct SizingView<T: View>: View {
let view: T
let updateSizeHandler: ((_ size: CGSize) -> Void)
init(view: T, updateSizeHandler: @escaping (_ size: CGSize) -> Void) {
self.view = view
self.updateSizeHandler = updateSizeHandler
}
var body: some View {
view.background(
GeometryReader { proxy in
Color.clear
.preference(key: SizePreferenceKey.self, value: proxy.size)
}
)
.onPreferenceChange(SizePreferenceKey.self) { preferences in
updateSizeHandler(preferences)
}
}
func size(with view: T, geometry: GeometryProxy) -> T {
updateSizeHandler?(geometry.size)
return view
}
}
Я не рекомендую использовать SelfSizingHostingController. Вы можете получить цикл Auto Layout с ним (мне это удалось).
Лучшим решением оказалось позвонить
invalidateIntrinsicContentSize()
сразу после установки содержимого. Как здесь:
hostingController.rootView = content
hostingController.view.invalidateIntrinsicContentSize()
Я столкнулся с той же проблемой, и ни одно из предложений не помогло мне. Затем я нашел следующий класс в
SwiftUIX
проект: https://github.com/SwiftUIX/SwiftUIX/blob/master/Sources/Intermodular/Helpers/UIKit/UIHostingView.swift
Это сработало отлично, за исключением анимации SwiftUI, которая все еще работает, но не выглядит точно так же, как в чистом контексте SwiftUI.