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, которое я размещаю внутри в , который необходимо совместить с другими представлениями в представлении прокручиваемого содержимого. Внутренний размер представления 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.

Другие вопросы по тегам