1

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

StackGU
  • 868
  • 9
  • 22
  • 1
    Your goal is going against of the definition of appearance callbacks. `viewWillAppear` is being called before the view is added into the UIWindow’s view hierarchy. `viewWillDisappear` is being called when the view is removed from that hierarchy. It’s never good to change the meaning of the callbacks. – Eugene Dudnyk Sep 10 '20 at 23:39
  • 1
    The `addChild` calls `willMove` for you. You’re supposed to call `didMove` when done adding children, not `willMove`. And I’d do these three `didMove` only after the scroll view has been added. – Rob Sep 11 '20 at 03:57

1 Answers1

2

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.

Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • Thanks for the answer, I opted for the scrollview because in that way I could use the UIScrollViewDelegate function viewDidScroll content offset on the entire width of the scrollView, while I don't know how to get the content offset of the entire content of a UIPageViewController – StackGU Sep 11 '20 at 05:40
  • Whatever works for you. With page view controller, you don't need to worry about the scroll view position, and you deal just with "gee, what page is showing". It's a lot easier, IMHO, but that's why I showed you both, so you could pick. – Rob Sep 11 '20 at 05:44
  • 1
    FYI, I've uploaded my demo at https://github.com/robertmryan/PagingDemo. – Rob Sep 11 '20 at 05:50
  • Thanks so much, very appreciated! – StackGU Sep 11 '20 at 16:23