【问题标题】:UIHostingController should expand to fit contentsUIHostingController 应该扩展以适应内容
【发布时间】:2020-02-12 09:32:15
【问题描述】:

我有一个自定义的UIViewControllerRepresentable(与布局相关的代码如下所示)。这试图复制原生 SwiftUI ScrollView,除了它从底部滚动到顶部。

查看层次结构

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()
    }
}

【问题讨论】:

  • 这个运气好吗?我自己也遇到过同样的问题。
  • 在我自己的情况下,到目前为止,我最好的方法是只使用内置的 SwiftUI 可滚动对象之一(我想到了 List),而不是使用 UIScrollView 包装。我已经尝试过常规的 SwiftUI ScrollView,但这似乎与 Buttons 有问题。
  • 我现在遇到了这个问题。在过去的几个月里,有人找到解决方案了吗?
  • @Rengers 刚刚针对一个非常相似的问题发布了我的解决方法,可能值得看看相同的方法是否适合您:stackoverflow.com/a/62263294/642233

标签: ios swift swiftui ios-autolayout uihostingcontroller


【解决方案1】:

对我来说,这个解决方案比我在这里看到的任何其他答案都简单得多(没有一个有效),尽管我花了很长时间才找到它。

我所做的只是创建UIHostingController 的一个瘦子类,它在其视图上调用invalidateIntrinsicContentSize() 以响应viewDidLayoutSubviews()

class SelfSizingHostingController<Content>: UIHostingController<Content> where Content: View {

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        self.view.invalidateIntrinsicContentSize()
    }
}

与原始问题类似,我在 UIScrollViewUIViewController 中托管了一个 SwiftUI 视图,该视图需要与滚动内容视图中的其他视图一起布局。 SwiftUI 视图的内在大小会根据其内容和用户选择的动态类型大小而变化。

就我而言,这真的很简单。它适用于 iOS 14+(未在 iOS 13 上测试),其中 SwiftUI 内容的更改会导致新的内在大小正确更新滚动视图中基于自动布局的 UIKit 布局。老实说,这不是UIHostingController 的隐含行为,这感觉就像一个错误。

【讨论】:

  • 这绝对是正确答案!!!干得好!
【解决方案2】:

这与@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
    }
}

【讨论】:

    【解决方案3】:

    我在涉及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
    }
    

    这是可怕的代码,但它是我发现的唯一让它工作的东西。

    【讨论】:

    • 感谢您的回答!我也试过这个,但不幸的是无法让它工作,因为当 SwiftUI 视图改变它的内容时,viewDidLayoutSubviews 没有被调用。然而,我确实找到了另一种解决方法,涉及使用 GeometryReader 获取 SwiftUI 视图大小,然后使用回调将其传递回 UIViewController。
    • 很高兴你找到了方法!希望这个月我们能得到一些 SwiftUI 的增强和修复,让我们避免这样的变通方法。
    • 这是一个很好的解决方案!不幸的是,我猜它不能在 tableview/collectionview 上使用,因为 viewDidLayotuSubviews 很昂贵 - 一遍又一遍地处理许多单元格。(在调试器中无法崩溃)
    【解决方案4】:

    我遇到了同样的问题,但没有任何建议对我有用。然后在SwiftUIX项目中找到了如下类:https://github.com/SwiftUIX/SwiftUIX/blob/master/Sources/Intermodular/Helpers/UIKit/UIHostingView.swift

    这非常有效,除了 SwiftUI 动画仍然有效,但看起来与纯 SwiftUI 上下文中的不完全相同。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2015-09-19
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-09-20
      • 2015-02-21
      • 2023-03-11
      • 1970-01-01
      相关资源
      最近更新 更多