Swift ios not sure on how to connect or use an embedded UIPageViewController

2.9k views Asked by At

I decided to drop my UIImageView and instead use an UIPageViewController so that users can swipe between images on the same page.

What I have now is a normal UIViewController, in that I added a containerView in which I have embedded a UIPageViewController.

So the UIPageViewController acts as a childview to the viewcontroller.

I have a "details" page with some info about the product X. And instead of only showing one image I want the image to be "swipeable" so that users can see different images. But I dont want the PageControllerView to act as a full-size view, the image should still be visible inside the viewcontroller at all time.

And my problem now is that I am stuck. I have no idea on how to hook up the two controllers. So if anyone has an example app/tutorial on embedded segues/child view using swift please share.

I worked out a solution thanks to the answer bellow:

In my storyboard I have 3 VC's. VC1 with and embedded container view that has a segue to a UIPageControllerView, then I added a 3rd VC "content view controller" to display images for the UIPageControllerView.

ViewController.swift

import UIKit

class ViewController: UIViewController, UIPageViewControllerDataSource {

    var pageViewController: UIPageViewController!
    var pageTitles: NSArray!
    var pageImages: NSArray!

    override func viewDidLoad()
    {
        super.viewDidLoad()

    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

    @IBAction func restartAction(sender: AnyObject)
    {
        var startVC = self.viewControllerAtIndex(0) as ContentViewController
        var viewControllers = NSArray(object: startVC)

        self.pageViewController.setViewControllers(viewControllers as [AnyObject], direction: .Forward, animated: true, completion: nil)
    }

    func viewControllerAtIndex(index: Int) -> ContentViewController
    {
        if ((self.pageTitles.count == 0) || (index >= self.pageTitles.count)) {
            return ContentViewController()
        }

        var vc: ContentViewController = self.storyboard?.instantiateViewControllerWithIdentifier("ContentViewController") as! ContentViewController

        vc.imageFile = self.pageImages[index] as! String
        vc.titleText = self.pageTitles[index] as! String
        vc.pageIndex = index

        return vc


    }


    // MARK: - Page View Controller Data Source

    func pageViewController(pageViewController: UIPageViewController, viewControllerBeforeViewController viewController: UIViewController) -> UIViewController?
    {

        var vc = viewController as! ContentViewController
        var index = vc.pageIndex as Int


        if (index == 0 || index == NSNotFound)
        {
            return nil

        }

        index--
        return self.viewControllerAtIndex(index)

    }

    func pageViewController(pageViewController: UIPageViewController, viewControllerAfterViewController viewController: UIViewController) -> UIViewController? {

        var vc = viewController as! ContentViewController
        var index = vc.pageIndex as Int

        if (index == NSNotFound)
        {
            return nil
        }

        index++

        if (index == self.pageTitles.count)
        {
            return nil
        }

        return self.viewControllerAtIndex(index)

    }

    func presentationCountForPageViewController(pageViewController: UIPageViewController) -> Int
    {
        return self.pageTitles.count
    }

    func presentationIndexForPageViewController(pageViewController: UIPageViewController) -> Int
    {
        return 0
    }



    override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
        if segue.identifier == "PageViewControllerFitta" {
            self.pageTitles = NSArray(objects: "Explore", "Today Widget")
            self.pageImages = NSArray(objects: "page1", "page2")

            let toView = segue.destinationViewController as! UIPageViewController
            toView.dataSource = self


            var startVC = self.viewControllerAtIndex(0) as ContentViewController
            var viewControllers = NSArray(object: startVC)

            toView.setViewControllers(viewControllers as [AnyObject], direction: .Forward, animated: true, completion: nil)

            toView.view.frame = CGRectMake(0, 30, self.view.frame.width, self.view.frame.size.height - 60)

            self.addChildViewController(toView)
            self.view.addSubview(toView.view)
            toView.didMoveToParentViewController(self)
        }
    }


}

ContentViewController.Swift

import UIKit

class ContentViewController: UIViewController {

    @IBOutlet weak var imageView: UIImageView!

    var pageIndex: Int!
    var imageFile: String!



    override func viewDidLoad()
    {
        super.viewDidLoad()

        self.imageView.image = UIImage(named: self.imageFile)


    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }



}
1

There are 1 answers

6
Brett On BEST ANSWER

Embedding is actually a kind of segue that takes place as soon as the containing controller is instantiated, so the way to do this is to implement prepareForSegue on your containing controller, and capture a reference to your child controller within that method. Here's an example adapted from a project I'm working on right now, it probably won't compile but should give you an idea of how to proceed—let me know if you need more information.

In order for this to work, you have to give the segue property an identifier. Assuming you are using storyboards, this is just a property on the segue itself (the little round transition icon that sits on the arrow connecting the parent and child UIViewControllers).

class ViewController: UIViewController { // <-- this is the container

    var myEmbeddedViewController: MyCustomUIViewController?

    override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {

        switch segue.identifier {
        case .Some("the-identifier-i-used-for-the-segue"):
            myEmbeddedViewController = segue.destinationViewController as? MyCustomUIViewController
        default:
            break
        }
    }

    // ... other UIViewController stuff...
}

Here's another another answer that goes into some detail and documentation, though provided in Objective-C rather than Swift: iOS Nested View Controllers view inside UIViewController's view?


Now that you have a reference to your UIPageControllerView, it needs a way to answer some questions for display purposes: how many pages are there, what page do we open to, what page do I show when the user swipes to the next and previous pages. It does this with a common UIKit/Cocoa idiom dataSource (lots of view controllers have this property).

In the sample code you link to, the viewDidLoad method contains the line self.pageViewController.dataSource = self. This works because the author has declared that his ViewController, in addition to extending UIViewController, also conforms to the UIPageViewControllerDataSource protocol. He then implements the required methods:

func pageViewController(pageViewController: UIPageViewController, viewControllerBeforeViewController viewController: UIViewController) -> UIViewController?
func pageViewController(pageViewController: UIPageViewController, viewControllerAfterViewController viewController: UIViewController) -> UIViewController? {
func presentationCountForPageViewController(pageViewController: UIPageViewController) -> Int
func presentationIndexForPageViewController(pageViewController: UIPageViewController) -> Int

See: https://developer.apple.com/library/prerelease/ios/documentation/UIKit/Reference/UIPageViewControllerDataSourceProtocolRef/index.html

(The dataSource doesn't have to be your ViewController, you can make a separate class or struct. For simple examples it's probably fine, though.)

The specific challenge for your is to implement those methods based on your app's requirements so that the embedded UIPageViewController can figure out what to display from whatever you set as its dataSource.

Suppose you want it to display a ProductImageViewController which just takes an image and a label. You could, in theory, create storyboards for each product but that isn't really going to work unless you have a small number of products that never changes. What's more likely is, so you want to create new instances in code based on your product list.

In the sample code, the author just sets up a static array of labels and images and then the pageViewController methods just grabs the appropriate one from the array based on current position and the direction of the swipe. You want to set up a dataSource that knows about your product list which you presumably get from some dynamic source like CoreData, CloudKit or some other web service.

As a simple experiment to familiarize yourself, try modifying that demo's variables self.pageTitles and self.pageImages and see how that alters the behavior of the embedded controller. Try modifying that code so that rather than using two arrays, one of text and one of image file names, you use a single array containing a struct or class that you define, which holds both the label text and the file name. Once that's working, alter so that instead of hardcoding the list of labels/images, you load it from your product catalog. And what's that's working, you're done, ship it :)