Search code examples
iosswiftuipageviewcontroller

How Can I slide Views programmatically using buttons with existing UIPageViewController


I'm developing an app which has 3 main Views and in order to enable users to change the 3 Views with just swiping(like Tinder app), I created a PageViewController and it works well. This PageViewController works like below.

  1. When the User tap either "Login" or "SignUp" button, it segues to the PageViewController
  2. The PageViewController set the firstViewController as an initial View
  3. In the PageViewController, It has 3 methods which are 「getFirst」(To get the firstViewController), 「getSecond」(To get the secondViewController), 「getThird」(To get the thirdViewController).

I also added buttons at the upper side of each 3 Views and want users to able to tap those buttons to slide to other Views just like it does when they swipe those Views(This is also like Tinder).

So for experiment, I added two buttons on the SecondViewController. The left one is for moving to the FirstViewController and the right one is for moving to the ThirdViewController. And then, at the SecondViewController file, I added "UIPageViewControllerDelegate" and tried to call 「getFirst」 method for the left button and call 「getThird」 method for the right button from the PageViewController file.

But When I tap the left button, I've got this error message on the 「getFirst」 method line in the PageViewController file(I would probably get the same error for the right button as well).

Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value

The codes are below.

PageViewController

import UIKit

class PageViewController: UIPageViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        self.setViewControllers([getFirst()], direction: .forward, animated: true, completion: nil)
        self.dataSource = self as UIPageViewControllerDataSource
    }
}


extension PageViewController : UIPageViewControllerDataSource {

    //FirstViewController
    @objc func getFirst() -> FirstViewController {
        return storyboard!.instantiateViewController(withIdentifier: "FirstViewController") as! FirstViewController
    }
    //SecondViewController
    @objc func getSecond() -> SecondViewController {
        return storyboard!.instantiateViewController(withIdentifier: "SecondViewController") as! SecondViewController
    }
    //ThirdViewController
    @objc func getThird() -> ThirdViewController {
        return storyboard!.instantiateViewController(withIdentifier: "ThirdViewController") as! ThirdViewController
    }

    func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
        if viewController.isKind(of: ThirdViewController.self) {
            // 3 -> 2
            return getSecond()
        } else if viewController.isKind(of: SecondViewController.self) {
            // 2 -> 1
            return getFirst()
        } else {
            // 1 -> end of the road
            return nil
        }
    }

    func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
        if viewController.isKind(of: FirstViewController.self) {
            // 1 -> 2
            return getSecond()
        } else if viewController.isKind(of: SecondViewController.self) {
            // 2 -> 3
            return getThird()
        } else {
            // 3 -> end of the road
            return nil
        }
    }
}

SecondViewController

import UIKit
class SecondViewController: UIViewController, UIPageViewControllerDelegate {

    let pageHelper = PageViewController()
    override func viewDidLoad() {
        super.viewDidLoad()

        pageHelper.delegate = self
    }

    @IBAction func toFirstTapped(_ sender: Any) {
        pageHelper.getFirst()
    }
}

What am I missing?? Maybe I can't make it work this way(I can't reuse the PageViewController from other files?).

Could someone help me please!


Solution

  • First, PageViewController instantiates a child controller which instantiates a PageViewController.. Is that really a good idea? Then in your toFirstTapped, you instantiate a FirstViewController but do nothing with it..

    If you do nothing with it, how do you expect it to work?

    In any case, try the below:

    protocol PageNavigationDelegate : class {
        weak var pageController: PageViewController? { get set }
    }
    
    class PageViewController: UIPageViewController {
        override func viewDidLoad() {
            super.viewDidLoad()
            self.setViewControllers([getFirst()], direction: .forward, animated: true, completion: nil)
            self.dataSource = self as UIPageViewControllerDataSource
        }
    }
    
    
    extension PageViewController : UIPageViewControllerDataSource {
    
        func getFirst() -> FirstViewController {
            let controller = storyboard!.instantiateViewController(withIdentifier: "FirstViewController") as! FirstViewController
            controller.pageController = self
            return controller
        }
    
        func getSecond() -> SecondViewController {
            let controller = storyboard!.instantiateViewController(withIdentifier: "SecondViewController") as! SecondViewController
            controller.pageController = self
            return controller
        }
    
        func getThird() -> ThirdViewController {
            let controller = storyboard!.instantiateViewController(withIdentifier: "ThirdViewController") as! ThirdViewController
            controller.pageController = self
            return controller
        }
    
        @discardableResult
        func goToPreviousPage() -> Bool {
            let controller = self.viewControllers!.first!
            if controller.isKind(of: SecondViewController.self) {
                self.setViewControllers([getFirst()], direction: .reverse, animated: true, completion: nil)
                return true
            }
    
            if controller.isKind(of: ThirdViewController.self) {
                self.setViewControllers([getSecond()], direction: .reverse, animated: true, completion: nil)
                return true
            }
    
            return false
        }
    
        @discardableResult
        func goToNextPage() -> Bool {
            let controller = self.viewControllers!.first!
            if controller.isKind(of: FirstViewController.self) {
                self.setViewControllers([getSecond()], direction: .forward, animated: true, completion: nil)
                return true
            }
    
            if controller.isKind(of: SecondViewController.self) {
                self.setViewControllers([getThird()], direction: .forward, animated: true, completion: nil)
                return true
            }
    
            return false
        }
    
        func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
    
            if viewController.isKind(of: SecondViewController.self) {
                return getFirst()
            }
    
            if viewController.isKind(of: ThirdViewController.self) {
                return getSecond()
            }
    
            return nil
        }
    
        func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
    
            if viewController.isKind(of: FirstViewController.self) {
                return getSecond()
            }
    
            if viewController.isKind(of: SecondViewController.self) {
                return getThird()
            }
    
            return nil
        }
    }
    
    class PageNavigationController : UIViewController, PageNavigationDelegate {
        var pageController: PageViewController?
    
        override func viewDidLoad() {
            super.viewDidLoad()
        }
    
        @IBAction func goToPreviousController(_ sender: Any) {
            pageController?.goToPreviousPage()
        }
    
        @IBAction func goToNextController(_ sender: Any) {
            pageController?.goToNextPage()
        }
    }
    
    class FirstViewController: PageNavigationController {
    
    }
    
    class SecondViewController: PageNavigationController {
    
    }
    
    class ThirdViewController: PageNavigationController {
    
    }
    

    First we created a protocol. We made sure that all our PageController's sub-controllers will implement it. This way, they can navigate to previous and next pages..

    Next, we create our extension which has the goToPreviousPage and goToNextPage function.. When we instantiate each controller page, we assign self to its protocol implementation variable. That way, the controller can access the PageViewController's methods..

    Finally, in our First, Second, and Third controllers, we just tell it to go forward or backward on button press.

    Personally, I'd prefer to keep track of controllers in an array and just return those in my delegates.