Search code examples
iosswiftuiviewcontrollersegueuipageviewcontroller

UIPageViewController between two UIViewControllers


I've just started programming in Swift, what I'm trying to accomplish is a very simple app with an initial UIViewController, a UIPageViewController that shows some book pages and a destination UIViewController.

My approach so far is this:

  1. The UIViewController1 is loaded and has a showPage button that simply shows UIPageViewController

present(walkthroughViewController, animated: true, completion: nil)

  1. When the user reaches the last page of the UIPageViewController, I show the destination UIViewController2, addressing the segue from the start UIViewController

    override func onUIPageViewControllerRigthClosing(){
        let pvc = self.presentingViewController as! StartPageController
        dismiss(animated: true){
            pvc.performSegue(withIdentifier: "startTest", sender: nil)
        }
    }
    

Everything works correctly, but the problem is that when UIPageViewController is dismissed, the Starting UIViewController is showed and then is showed the second with the animated segue. What I am trying to achieve is to directly display the target UiViewController to the user on the dismiss of the UIPageViewController, without showing the transition with animation from start View to the destination View.

I'm completely wrong approaching or there is a way to do the segue before dismissing the UIPageViewController?

Here I created a gif that shows the problem, when I close the UIPageViewController I see the previous view in transition: GIF demo


Solution

  • I suggest you using this approach: for these screens transitions use childViewControllers instead of presenting them modally and dismissing with default UIKit functions.

    You have problems with naming, so let me rename view controllers. Say, you have:

    • RootViewController (the first screen, user see after app launch).
    • OnboardingViewController (your pageViewController or other container)
    • AppContentViewController (actually app main screen)

    I suggest you using this approach: for screens transitions on RootViewController use childViewControllers instead of presenting them modally and dismissing with default UIKit functions.

    Here is sample code that works with childViewControllers

    extension UIViewController {
        func displayChildController(_ content: UIViewController, duration: TimeInterval = 0.4, animation: (() -> ())? = nil, completion: @escaping () -> () = {}) {
            content.view.frame = CGRect(x: 0, y: 0, width: self.view.frame.size.width, height: self.view.frame.size.height)
            view.addSubview(content.view)
            addChildViewController(content)
            UIView.animate(withDuration: animation != nil ? duration : 0, animations: {() -> Void in
                animation?()
            }, completion: {(_ finished: Bool) -> Void in
                content.didMove(toParentViewController: self)
                completion()
            })
        }
    
        func hideChildController(_ content: UIViewController, duration: TimeInterval = 0.4, animation: (() -> ())? = nil, completion: @escaping () -> () = {}) {
    
            UIView.animate(withDuration: animation != nil ? duration : 0, animations: {() -> Void in
                animation?()
            }, completion: {(_ finished: Bool) -> Void in
                content.willMove(toParentViewController: nil)
                content.view.removeFromSuperview()
                content.removeFromParentViewController()
                completion()
            })
        }
    }
    

    Here is "algorithm":

    I assuming that you are using single storyboard with all these view controllers.

    1. On OnBoardingViewController declare onDoneCallback:

      class OnBoardingViewController: ... {
          var onDoneCallback = {}
          ...
      }
      
    2. On RootViewController when you need present OnboardingViewController:

      func presentOnboardingScreen() {
          let onboardingVC = self.storyboard?.instantiateViewController(withIdentifier: "OnboardingViewController") as! OnboardingViewController
          onboardingVC.transform = .init(translationX: 0, y: self.view.frame.height)
      
          onboardingVC.onDoneCallback = {
              self.presentAppContentAfterOnboarding() // see below
          }
      
          displayChildController(onboardingVC, duration: 0.3, animation: {
              vc.view.transform = .identity
          })
      }
      
    3. When you need call onDoneCallback closure on OnboardingViewController

    4. presentAppContentAfterOnboarding method on RootViewController could look like:

      func presentAppContentAfterOnboarding() {
          let onboardingVC = self.childViewControllers.last as! OnboardingViewController
          let appContentVC = self.storyboard?.instantiateViewController(withIdentifier: "AppContentViewController") as! AppContentViewController
          displayChildController(appContentVC)
          view.insertSubview(appContentVC.view, belowSubview: onboardingVC.view)
          hideChildController(childVC, duration: duration, animation: {
              onboardingVC.view.transform = .init(translationX: 0, y: self.view.frame.height)
          })
      }
      

    Note. Don't forget to set Storyboard ID of OnboardingViewController and AppContentViewController in your storyboard.

    Here is the sample project