我应该在 SwiftUI 的 updateUIViewController(_:context:) 中调用 viewDidLoad()

Should I call viewDidLoad() inside updateUIViewController(_:context:) in SwiftUI

我创建 UIScrollView 以集成到 SwiftUI 视图中。它包含 UIHostingController 来托管 SwiftUI 视图。当我更新 UIHostingController 时,UIScrollView 不会更改其约束。我既不能滚动到顶部也不能滚动到底部。当我尝试在 updateUIViewController(_:context:) 中调用 viewDidLoad() 时,它的工作方式与我预期的一样。这是我的示例代码,

struct ContentView: View {
@State private var max = 100
var body: some View {
    VStack {
        Button("Add") { self.max += 2 }
            ScrollableView {
                ForEach(0..<self.max, id: \.self) { index in
                    Text("Hello \(index)")
                        .frame(width: UIScreen.main.bounds.width, height: 100)
                        .background(Color(red: Double.random(in: 0...255) / 255, green: Double.random(in: 0...255) / 255, blue: Double.random(in: 0...255) / 255))
                }
            }
        }
    }
}
class ScrollViewController<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()

    override func viewDidLoad() {
        self.view = UIView()
        self.addChild(hostingController)
        view.addSubview(scrollView)
        scrollView.addSubview(hostingController.view)

        scrollView.delegate = self
        scrollView.scrollsToTop = true
        scrollView.isScrollEnabled = true

        makeConstraints()

        hostingController.didMove(toParent: self)
    }

    func makeConstraints() {
        scrollView.widthAnchor.constraint(equalTo: view.widthAnchor).isActive = true
        scrollView.heightAnchor.constraint(equalTo: view.heightAnchor).isActive = true

        hostingController.view.widthAnchor.constraint(equalTo: scrollView.widthAnchor).isActive = true
        hostingController.view.topAnchor.constraint(equalTo: scrollView.topAnchor).isActive = true
        hostingController.view.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor).isActive = true

        hostingController.view.translatesAutoresizingMaskIntoConstraints = false
        scrollView.translatesAutoresizingMaskIntoConstraints = false
    }
}
struct ScrollableView<Content: View>: UIViewControllerRepresentable {
    var content: () -> Content

    init(@ViewBuilder content: @escaping () -> Content) {
        self.content = content
    }

    func makeUIViewController(context: Context) -> ScrollViewController<Content> {
        let vc = ScrollViewController(rootView: self.content())
        return vc
    }
    func updateUIViewController(_ viewController: ScrollViewController<Content>, context: Context) {
        viewController.hostingController.rootView = self.content()
        viewController.viewDidLoad()
    }
}

我认为这不是一个好方法。我想知道是否有更新控制器的最佳方法。如果有人知道最好的解决方案,请与我分享。谢谢。

你是对的,我们永远不应该调用我们自己的 viewDidLoad


让我们使用视图调试器诊断问题。因此,例如,这里是(将 max 设置为 8 以使其易于管理):

注意宿主控制器的高度 view 是 800(因为我们有 8 个子视图,每个 100 pt)。到目前为止,还不错。

现在点击“添加”按钮并重复:

我们可以看到问题不是滚动视图,而是托管视图控制器的视图。即使现在有 10 个项目,它仍然认为托管视图控制器的视图高度为 800。

所以,我们可以调用 setNeedsUpdateConstraints 来解决问题:

func updateUIViewController(_ viewController: ScrollViewController<Content>, context: Context) {
    viewController.hostingController.rootView = content()
    viewController.hostingController.view.setNeedsUpdateConstraints()
}

因此:

struct ContentView: View {
    @State private var max = 8

    var body: some View {
        GeometryReader { geometry in                  // don't reference `UIScreen.main.bounds` as that doesn’t work in split screen multitasking
            VStack {
                Button("Add") { self.max += 2 }
                ScrollableView {
                    ForEach(0..<self.max, id: \.self) { index in
                        Text("Hello \(index)")
                            .frame(width: geometry.size.width, height: 100)
                            .background(Color(red: .random(in: 0...1), green: .random(in: 0...1), blue: .random(in: 0...1)))
                    }
                }
            }
        }
    }
}

class ScrollViewController<Content: View>: UIViewController {
    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()

    override func viewDidLoad() {
        super.viewDidLoad()                            // you need to call `super`
        // self.view = UIView()                        // don't set `self.view`

        addChild(hostingController)
        view.addSubview(scrollView)
        scrollView.addSubview(hostingController.view)

        // scrollView.delegate = self                  // you're not currently using this delegate protocol, so we probably shouldn't set the delegate

        // scrollView.scrollsToTop = true              // these are the default values
        // scrollView.isScrollEnabled = true

        makeConstraints()

        hostingController.didMove(toParent: self)
    }

    func makeConstraints() {
        NSLayoutConstraint.activate([
            // constraints for scroll view w/in main view

            scrollView.topAnchor.constraint(equalTo: view.topAnchor),
            scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
            scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),

            // define contentSize of scroll view relative to hosting controller's view

            hostingController.view.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor),
            hostingController.view.bottomAnchor.constraint(equalTo: scrollView.contentLayoutGuide.bottomAnchor),
            hostingController.view.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor),
            hostingController.view.trailingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.trailingAnchor)
        ])

        hostingController.view.translatesAutoresizingMaskIntoConstraints = false
        scrollView.translatesAutoresizingMaskIntoConstraints = false
    }
}

struct ScrollableView<Content: View>: UIViewControllerRepresentable {
    var content: () -> Content

    init(@ViewBuilder content: @escaping () -> Content) {
        self.content = content
    }

    func makeUIViewController(context: Context) -> ScrollViewController<Content> {
        ScrollViewController(rootView: content())
    }

    func updateUIViewController(_ viewController: ScrollViewController<Content>, context: Context) {
        viewController.hostingController.rootView = content()
        viewController.hostingController.view.setNeedsUpdateConstraints()
    }
}