Using UIPageViewControllers with Segues

15 Jun, 2015
Xebia Background Header Wave
I've always wondered how to configure a UIPageViewController much like you configure a UITabBarController inside a Storyboard. It would be nice to create standard embed segues to the view controllers that make up the pages. Unfortunately, such a thing is not possible and currently you can't do a whole lot in a Storyboard with page view controllers. So I thought I'd create a custom way of connecting pages through segues. This post explains how.

Without segues

First let's create an example without using segues and then later we'll try to modify it to use segues. Screen Shot 2015-06-14 at 10.01.10 In the above Storyboard we have 2 scenes. One page view controller and another for the individual pages, the content view controller. The page view controller will have 4 pages that each display their page number. It's about as simple as a page view controller can get. Below the code of our simple content view controller: [code language="obj-c"] class MyContentViewController: UIViewController { @IBOutlet weak var pageNumberLabel: UILabel! var pageNumber: Int! override func viewDidLoad() { super.viewDidLoad() pageNumberLabel.text = "Page \(pageNumber)" } } [/code] That means that our page view controller just needs to instantiate an instance of our MyContentViewController for each page and set the pageNumber. And since there is no segue between the two scenes, the only way to create an instance of the MyContentViewController is programmatically with the UIStoryboard.instantiateViewControllerWithIdentifier(_:). Of course for that to work, we need to give the content view controller scene an identifier. We'll choose MyContentViewController to match the name of the class. Our page view controller will look like this: [code language="obj-c"] class MyPageViewController: UIPageViewController { let numberOfPages = 4 override func viewDidLoad() { super.viewDidLoad() setViewControllers([createViewController(1)], direction: .Forward, animated: false, completion: nil) dataSource = self } func createViewController(pageNumber: Int) -> UIViewController { let contentViewController = storyboard?.instantiateViewControllerWithIdentifier("MyContentViewController") as! MyContentViewController contentViewController.pageNumber = pageNumber return contentViewController } } extension MyPageViewController: UIPageViewControllerDataSource { func pageViewController(pageViewController: UIPageViewController, viewControllerAfterViewController viewController: UIViewController) -> UIViewController? { return createViewController( mod((viewController as! MyContentViewController).pageNumber, numberOfPages) + 1) } func pageViewController(pageViewController: UIPageViewController, viewControllerBeforeViewController viewController: UIViewController) -> UIViewController? { return createViewController( mod((viewController as! MyContentViewController).pageNumber - 2, numberOfPages) + 1) } } func mod(x: Int, m: Int) -> Int{ let r = x % m return r < 0 ? r + m : r } [/code] Here we created the createViewController(_:) method that has the page number as argument and creates the instance of MyContentViewController and sets the page number. This method is called from viewDidLoad() to set the initial page and from the two UIPageViewControllerDataSource methods that we're implementing here to get to the next and previous page. The custom mod(_:_) function is used to have a continuous page navigation where the user can go from the last page to the first and vice versa (the built-in % operator does not do a true modules operation that we need here).

Using segues

The above sample is pretty simple. So how can we change it to use a segue? First of all we need to create a segue between the two scenes. Since we're not doing anything standard here, it will have to be a custom segue. Now we need a way to get an instance of our content view controller through the segue which we can use from our createViewController(_:). The only method we can use to do anything with the segue is UIViewController.performSegueWithIdentifier(_:sender:). We know that calling that method will create an instance of our content view controller, which is the destination of the segue, but we then need a way to get this instance back into our createViewController(_:) method. The only place we can reference the new content view controller instance is from within the custom segue. From it's init method we can set it to a variable which the createViewController(_:) can also access. That looks something as following. First we create the variable: [code language="obj-c"] var nextViewController: MyContentViewController? [/code] Next we create a new custom segue class that assigns the destination view controller (the new MyContentViewController) to this variable. [code language="obj-c"] public class PageSegue: UIStoryboardSegue { public override init!(identifier: String?, source: UIViewController, destination: UIViewController) { super.init(identifier: identifier, source: source, destination: destination) if let pageViewController = source as? MyPageViewController { pageViewController.nextViewController = destinationViewController as? MyContentViewController } } public override func perform() {} } [/code] Since we're only interested in getting the reference to the created view controller we don't need to do anything extra in the perform() method. And the page view controller itself will handle displaying the pages so our segue implementation remains pretty simple. Now we can change our createViewController(_:) method: [code language="obj-c"] func createViewController(pageNumber: Int) -> UIViewController { performSegueWithIdentifier("Page", sender: self) nextViewController!.pageNumber = pageNumber return nextViewController! } [/code] The method looks a bit odd since it's not we're never assigning nextViewController anywhere in this view controller. And we're relying on the fact that the segue is created synchronously from the performSegueWithIdentifier call. Otherwise this wouldn't work. Now we can create the segue in our Storyboard. We need to give it the same identifier as we used above and set the Segue Class to PageSegue Screen Shot 2015-06-14 at 11.58.49 Generic class Now we can finally create segues to visualise the relationship between page view controller and content view controller. But let's see if we can write a generic class that has most of the logic which we can reuse for each UIPageViewController. We'll create a class called SeguePageViewController which will be the super class for our MyPageViewController. We'll move the PageSegue to the same source file and refactor some code to make it more generic: [code language="obj-c"] public class SeguePageViewController: UIPageViewController { var pageSegueIdentifier = "Page" var nextViewController: UIViewController? public func createViewController(sender: AnyObject?) -> MyContentViewController { performSegueWithIdentifier(pageSegueIdentifier, sender: sender) return nextViewController! } } public class PageSegue: UIStoryboardSegue { public override init!(identifier: String?, source: UIViewController, destination: UIViewController) { super.init(identifier: identifier, source: source, destination: destination) if let pageViewController = source as? SeguePageViewController { pageViewController.nextViewController = destinationViewController as? UIViewController } } public override func perform() {} } [/code] As you can see, we moved the nextViewController variable and createViewController(_:) to this class and use UIViewController instead of our concrete MyContentViewController class. We also introduced a new variable pageSegueIdentifier to be able to change the identifier of the segue. The only thing missing now is setting the pageNumber of our MyContentViewController. Since we just made things generic we can't set it from here, so what's the best way to deal with that? You might have noticed that the createViewController(_:) method now has a sender: AnyObject? as argument, which in our case is still the page number. And we know another method that receives this sender object: prepareForSegue(_:sender:). All we need to do is implement this in our MyPageViewController class. [code language="obj-c"] override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) { if segue.identifier == pageSegueIdentifier { let vc = segue.destinationViewController as! MyContentViewController vc.pageNumber = sender as! Int } } [/code] If you're surprised to see the sender argument being used this way you might want to read my previous post about Understanding the ‘sender’ in segues and use it to pass on data to another view controller


Wether or not you'd want to use this approach of using segues for page view controllers might be a matter of personal preference. It doesn't give any huge benefits but it does give you a visual indication of what's happening in the Storyboard. It doesn't really save you any code since the amount of code in MyPageViewController remained about the same. We just replaced the createViewController(_:) with the prepareForSegue(_:sender:) method. It would be nice if Apple offers a better solution that depends less on the data source of a page view controller and let you define the paging logic directly in the storyboard.

Get in touch with us to learn more about the subject and related solutions

Explore related posts