Search code examples
iosswiftuiviewcontrolleruiscrollviewviewcontroller-lifecyle

How to call embedded ViewControllers lifecycles in UIScrollView - Swift - Programmatically


I am creating a scrollView that is able to display ViewController views one after the other. This is the code I implemented:

scrollView.contentSize = CGSize(width: screenWidth * 3, height: screenHeight)
    
    let firstVC = FirstViewController()
    
    let secondVC = SecondViewController()
    
    let thirdVC = ThirdViewController()
    
    self.addChild(firstVC)
    self.scrollView.addSubview(firstVC.view)
    firstVC.willMove(toParent: self)
    
    self.addChild(secondVC)
    self.scrollView.addSubview(secondVC.view)
    secondVC.willMove(toParent: self)

    self.addChild(thirdVC)
    self.scrollView.addSubview(thirdVC.view)
    thirdVC.willMove(toParent: self)
    
    
    firstVC.view.frame.origin = CGPoint.zero
    secondVC.view.frame.origin = CGPoint(x: screenWidth, y: 0)
    thirdVC.view.frame.origin = CGPoint(x: screenWidth*2, y: 0)
    
    
    view.addSubview(scrollView)
    scrollView.fillSuperview()

I wanted to know if it was possible to call each ViewController lifecycle method whenever I'm scrolling through them.

So for example when I'm passing from vc1 to vc2 I want that:

vc1 fires viewwillDisappear method

vc2 fires viewWillAppear method


Solution

  • The easiest solution is to use a page view controller. When you do that, the appearance methods for the children will be called for you automatically (and it gets you out of all of that dense code to manually populate and configure the scroll view):

    class MainViewController: UIPageViewController {
        let controllers = [RedViewController(), GreenViewController(), BlueViewController()]
    
        override func viewDidLoad() {
            super.viewDidLoad()
    
            dataSource = self
            setViewControllers([controllers.first!], direction: .forward, animated: true, completion: nil)
        }
    }
    
    extension MainViewController: UIPageViewControllerDataSource {
        func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
            if let index = controllers.firstIndex(where: { $0 == viewController }), index < (controllers.count - 1) {
                return controllers[index + 1]
            }
            return nil
        }
    
        func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
            if let index = controllers.firstIndex(where: { $0 == viewController }), index > 0 {
                return controllers[index - 1]
            }
            return nil
        }
    }
    

    If you really want to use the scroll view approach, don't do the view controller containment code up front, but rather only add them as they scroll into view (and remove them when they scroll out of view). You just need to set a delegate for your scroll view and implement a UIScrollViewDelegate method.

    So, for example, I might only populate my scroll view with container subviews for these three child view controllers. (Note containerViews in my example below are just blank UIView instances, laid out where the child view controller views will eventually go.) Then I can see if the CGRect of the visible portion of the scroll view intersects with a container view, and do the view controller containment in a just-in-time manner.

    extension ViewController: UIScrollViewDelegate {
        func scrollViewDidScroll(_ scrollView: UIScrollView) {
            let rect = CGRect(origin: scrollView.contentOffset, size: scrollView.bounds.size)
            for (index, containerView) in containerViews.enumerated() {
                let controller = controllers[index]
                let controllerView = controller.view!
                if rect.intersects(containerView.frame) {
                    if controllerView.superview == nil {
                        // a container view has scrolled into view, but the associated
                        // child controller view has not been added to the view hierarchy, yet
                        // so let's do that now
    
                        addChild(controller)
                        containerView.addSubview(controllerView)
                        controllerView.translatesAutoresizingMaskIntoConstraints = false
    
                        NSLayoutConstraint.activate([
                            controllerView.topAnchor.constraint(equalTo: containerView.topAnchor),
                            controllerView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor),
                            controllerView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
                            controllerView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor)
                        ])
    
                        controller.didMove(toParent: self)
                    }
                } else {
                    if controllerView.superview != nil {
                        // a container view has scrolled out of view, but the associated
                        // child controller view is still in the view hierarchy, so let's
                        // remove it.
    
                        controller.willMove(toParent: nil)
                        controllerView.removeFromSuperview()
                        controller.removeFromParent()
                    }
                }
            }
        }
    }
    

    In both of these scenarios, you will receive the containment calls as the views appear.