Skip to content →

Page View Controller in a Container View

Late one evening this week I decided to turn Netflix off and see what I could cobble together with regard to a feature a teammate of mine was working on, namely, using UIPageViewController to provide paginated content.

PageViewControllerDemoAs seen in the animation, for us there are some key characteristics of the targeted implementation. These include:

Swift 3 language and UIKit features illustrated but not specifically discussed include:

  • Protocols
  • Switch case value bindings akin to if let value = value as? SomeType
  • String splitting using map(); substrings
  • UITableViewController and UITableViewControllerDataSource
  • Storyboard and programmatic instantiation of view controllers
  • UIView.animate(withDuration:animations:)

If any of these topics interest you and you want a quick fix, take a look at the project on GitHub, written in Swift 3 (updated for Swift 4). Have specific questions? Keep reading.

FAQ

  • How do I provide paginated content? There’s a storyboard piece and a programmatic piece. In summary, on the storyboard you drop in and hook up a Page View Controller object into your scene flow and add a content object of your choosing. Programmatically, you need only implement a couple UIPageViewControllerDataSource methods … and provide the initial view controller. See PageViewController.swift for that implementation. Details below.
  • How do I use a page scroll rather than curl transition? In the Attributes Inspector for the Page View Controller storyboard scene, set “Transition Style” to “Scroll”.
  • How do I add a page indicator? Implement UIPageViewControllerDataSource methods presentationCount(for:) and presentationIndex(for:); see PageViewController.swift. If you don’t want a page indicator, don’t implement these methods.
  • How do I set the color of the page indicator dots? See PageViewController viewDidLoad():
    let pageControl = UIPageControl.appearance(whenContainedInInstancesOf: [PageViewController.self])
    pageControl.pageIndicatorTintColor = .lightGray
    pageControl.currentPageIndicatorTintColor = .black
    
  • How do I embed a paginated subview? The storyboard is all you need. The process I follow:
    1. Drop a Container View onto the containing view. If you’re good with the tag-along view controller, stop here.
    2. Otherwise, delete the tag-along view controller.
    3. Add the desired controller to the storyboard.
    4. Control-drag from the container to the controller.
    5. Select the “Embed” segue.
  • How do I use a segmented control to switch between subviews? My approach is through manipulation of each subview’s hidden property, accessed by @IBOutlet connection. See MainViewController.swift.
  • How do I reuse a view controller in a storyboard? I recommend embedded Container Views (they’re awesome!). In this project, I have one container view with an Embed segue directly to the content table view controller; then I have a second container view with an Embed segue to the Page View Controller that is programmatically connected to the same table view controller.
  • How do I access a container-embedded view controller? Rather simply…when you know how. The trick is to override prepare(for:sender:) in your container view controller. By doing so, you get access to the destination (embedded) view controller via the provided UIStoryboardSegue. See MainViewController.swift.

Details

Okay, let’s dive into some details. Keep in mind the context of this discussion is the specific implementation as given on GitHub.

Storyboard

Assuming you know the basics of Interface Builder storyboarding, here are some things to be aware of:

  • There are two container views on the project storyboard; although, because they’re stacked, you only see one. The first, named “Paged Container View”, has an embed segue to the Page View Controller scene. The second, “Unpaged Container View”, has an embed segue to Table View Controller.
  • Table View Controller has a Storyboard ID of “TableViewController”, which PageViewController makes use of when serving up paged view controllers.
  • There is no direct connection between Page View Controller and Table View Controller on the storyboard. As just alluded to, that is done programmatically in PageViewController.
  • The Table View Controller has one basic prototype cell matched with a class defined in TableViewController.swift named BasicCell.

Page View Controller

The back end of the Page View Controller storyboard scene is a UIPageViewController-subclassed PageViewController that conforms to the UIPageViewControllerDataSource protocol, much the way UITableViewController now does inherently for its data source and delegate.

At a minimum, a UIPageViewController needs only two things to get paging working:

  1. The plugged-in viewControllerAfter and viewControllerBefore data source methods.
  2. The initial view controller (or view controllers if you’re presenting two pages at at time).

Our page content is presented by a TableViewController. Rather than pre-populating a collection of controllers for all pages, I serve them up on-demand one at a time.

PageViewController maintains no state of its own as concerns which page we’re currently viewing. We leave that to each TableViewController via its section property. In viewDidLoad() we initialize the first Table View Controller’s section to zero, and the page view controller’s data source methods take it from there.

When UIPageViewController asks its data source for the previous or next page, we query the current view controller’s section value to do some bounds checking and initialize the new controller. If within bounds, we return the new controller; otherwise, we return nil.

Table View Controller

The reusability of this controller in two contexts hinges entirely on the optional section property:

  • When non-nil, as set only by the PageViewController, we can know the data is paginated and should only serve up one section, which one being determined by this section value.
  • When nil (i.e., left in its initial state), we know the controller is being used by the directly embedded container view, and needs to serve up data for all sections.

Functionally, this TableViewController is entirely a UITableViewDataSource, providing the bare minimum number of sections, rows per section, section title (not necessary, but useful), and cells. It calls upon an abstracted BasicTableDataModel for those values. This model is assigned by the PageViewController, which it receives from the MainViewController.

Anytime these methods reference the section, they use the sweet nil-coalescing operator to give priority to the optional section property, and failing that, the parameterized section or indexPath value.

Page Indicator

Actually managed within the PageViewController, the page indicator is made possible by implementing two additional UIPageViewControllerDataSource methods: presentationCount(for:) and presentationIndex(for:).

They’re straightforward enough. The only oddity is presentationIndex(for:) is called after viewControllers is initialized to an empty array, but before it is populated. Thus the reason for the nil and count > 0 checks.

Customization of the indicator, that is, its color, is another peculiarity. UIPageControl utilizes an “appearance proxy”, which implements the UIAppearance protocol.

For this project, it comes down to the three lines of code from viewDidLoad(), as given earlier and repeated here:

let pageControl = UIPageControl.appearance(whenContainedInInstancesOf: [PageViewController.self])
pageControl.pageIndicatorTintColor = .lightGray
pageControl.currentPageIndicatorTintColor = .black

If you were only to have one page indicator across your app or want them all to have the same colors, you can replace the first line with:

let pageControl = UIPageControl.appearance()

And, if follows, you can do that anywhere, such as in the AppDelegate.

Basic Table Data Model Protocol

An update to my original implementation addresses my glossing over how model data might work its way into the table view in the real world.

In this update, I add a BasicTableDataModel protocol and two conforming types (LatinTableDataModel and NumericTableDataModel) that present distinct data for use in the paged and non-paged views.

The MainViewController is now the keeper of the models, passing them down to the container view controllers via the segue method prepare(for:sender:). The PageViewController then passes its assigned model down to the individual TableViewControllers it instantiates.

Wrap Up

Adding a page view controller to your project is as simple as:

  1. Dropping a Page View Controller into your storyboard
  2. Connecting to a UIPageViewController-derived class that implements its data source protocol
  3. Employing an approach for serving up paged view controllers that fits with your use case, which really just comes down to providing a view controller with the page-specific model data it needs to display
  4. Optionally, configuring the page indicator by implementing a couple more data source methods and customizing it to a color suitable for your view.

By way of bonus material, you also got to see how to:

  • Reuse and tailor model data for a single view controller in multiple contexts
  • Use a segmented control to switch view contexts within a single controller.

Good stuff. Now, back to Netflix…

Published in User Interface

10 Comments

  1. Anonymous Anonymous

    Thank you for the instruction. Very clear cut. However I also would like to interact with the table (i.e. Select item). It doesn’t seem to trigger the UITableViewDelegate methods. Do you have suggestion?

    • Kevin Kevin

      Thank you for your feedback and question! In the Attributes inspector for the BasicCell in the Main storyboard, set the flag for User Interaction Enabled.

      • Anonymous Anonymous

        Thank you for that. By setting the “user interaction” on cell, it will display selection on the screen but the UITableViewDelegate methods (i.e. did selectItem will not get trigger). This is because the tableviewcontroller is within the containingview. I fixed it by doing the following:

        1) enable userinteraction on the BasicCell as mentioned
        2) add UITableViewDelegate to PageViewController and MainViewContoller to handle “page” and “unpage” use cases
        3) add the delegate method “didSelectRowAt…” on both PageViewController and MainViewController with different print message to test
        4) add “viewDidAppear” method in TableViewController this way to tie all together

        override func viewDidAppear(_animated: Bool) {
        super.viewDidAppear(animated)
        if let parentDelegate = parent as? UITableViewDelegate {
        tableView.delegate = parentDelegate }
        }

        That will assign the delegate to the parent so to recognize the event.

        • Kevin Kevin

          #1 and #3 should be all you need; works on my end. UITableViewControllers already conform to UITableViewDelegate (and UITableViewDataSource), so no additional logic is needed to get the did-select notification.

          • Anonymous Anonymous

            Weird for me that it didn’t work. I put this in TableViewController and remove #2 and#4 steps. The line was never called.

            public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
            print(“hello”)
            }

          • Kevin Kevin

            I pushed a couple updates to Github, the first (76fbbcb) demonstrating the did-select stub, the second (e0d77fc) updating for Xcode 9. See if that doesn’t work for you.

          • Anonymous Anonymous

            Thank you for the update. Works like a charm. 🙂

          • Kevin Kevin

            No problem, and good to hear!

  2. Norris Norris

    Very clear codes and comments.
    Thanks you very much!
    You saved me days and weeks! 🙂

  3. Anonymous Anonymous

    Awesome

Leave a Reply

Your email address will not be published.