Search code examples
uiviewcontrollerswiftuiviewdidloaduihostingcontroller

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


I create UIScrollView to be integrated inside SwiftUI view. It contains UIHostingController to host SwiftUI view. When I update UIHostingController, UIScrollView does not change its constraints. I can scroll neither to top nor to bottom. When I try to call viewDidLoad() inside updateUIViewController(_:context:), it works like I expect. Here is my sample code,

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

I don't think it is a good way to do. I want to know if there is the best way to update controller. If anyone knows the best solution, share me please. Thanks.


Solution

  • You are correct, we should never call our own viewDidLoad.


    Let’s diagnose the issue, using the view debugger. So, for example, here it is (setting max to 8 to keep it manageable):

    before

    Note the height of the hosting controller’s view is 800 (because we have 8 subviews, 100 pt each). So far, so good.

    Now tap the “add” button and repeat:

    enter image description here

    We can see that the problem isn’t the scroll view, but rather the hosting view controller’s view. Even though there are now 10 items, it still thinks the hosting view controller’s view’s height is 800.

    So, we can call setNeedsUpdateConstraints and that fixes the problem:

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

    Thus:

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