1

Courtesy of Using UIPageViewController with swift and multiple view controllers, I'm embedding navigation controllers in a UIPageViewController so I can horizontally scroll thru them (like swipe nav). Problem:

When I reach the first or last nav controller, and then swipe in the opposite direction, the 2nd-to-first/last nav controller will duplicate. So I'll have 5 nav controllers. For example:

Starting at FirstNav, swipe left all the way to FourthNav. When I swipe right through the array of controllers from FourthNav, the sequence will be: ThirdNav, ThirdNav, SecondNav, FirstNav. Can anyone find out what's going on?

 class PageViewController: UIPageViewController, UIPageViewControllerDataSource, UIPageViewControllerDelegate {

        var index = 0
        var identifiers: NSArray = ["FirstNav", "SecondNav", "ThirdNav", "FourthNav"]

        override func viewDidLoad() {
            super.viewDidLoad()

            self.dataSource = self
            self.delegate = self

            let startingViewController = self.viewControllerAtIndex(self.index)
            let viewControllers: NSArray = [startingViewController]
            self.setViewControllers(viewControllers as [AnyObject], direction: UIPageViewControllerNavigationDirection.Forward, animated: false, completion: nil)

        }

        func viewControllerAtIndex(index: Int) -> UINavigationController! {

            if index == 0 {

                return self.storyboard!.instantiateViewControllerWithIdentifier("FirstNav") as! UINavigationController
            }


            if index == 1 {

               return self.storyboard!.instantiateViewControllerWithIdentifier("SecondNav") as! UINavigationController

            }

           if index == 2 {

               return self.storyboard!.instantiateViewControllerWithIdentifier("ThirdNav") as! UINavigationController
           }

           if index == 3 {

               return self.storyboard!.instantiateViewControllerWithIdentifier("FourthNav") as! UINavigationController
           }

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

        let identifier = viewController.restorationIdentifier
        let index = self.identifiers.indexOfObject(identifier!)

        //if the index is the end of the array, return nil since we dont want a view controller after the last one
        if index == identifiers.count - 1 {

            return nil
        }

        //increment the index to get the viewController after the current index
        self.index = self.index + 1
        return self.viewControllerAtIndex(self.index)

    }

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

        let identifier = viewController.restorationIdentifier
        let index = self.identifiers.indexOfObject(identifier!)

        //if the index is 0, return nil since we dont want a view controller before the first one
        if index == 0 {

            return nil
        }

        //decrement the index to get the viewController before the current one
        self.index = self.index - 1
        return self.viewControllerAtIndex(self.index)

    }


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

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


}
Community
  • 1
  • 1
slider
  • 2,736
  • 4
  • 33
  • 69
  • Would it be possible to share your project? – ndmeiri Jul 07 '15 at 04:45
  • @ndmeiri I retested the code in a new project, using the exact same code. If you wanna do the same, create one page view controller, and 4 view controllers. Embed each VC in their own nav controller, and make sure to give each nav controller a storyboard ID. Then link the page view controller to it's proper class name. – slider Jul 07 '15 at 04:47
  • Okay, I'll try to reproduce the problem right now. What version of Xcode are you using, and what version of iOS are you building for? – ndmeiri Jul 07 '15 at 04:49
  • 6.4 and 8.4, respectively :) – slider Jul 07 '15 at 04:51
  • Important to note, there are no duplicates if the page view controller is set to page curl transition style. Only a problem with scroll style. – slider Jul 07 '15 at 04:57
  • Is this the exact code you're using? I can't even scroll to the second page. I immediately get a fatal runtime error: "fatal error: unexpectedly found nil while unwrapping an Optional value." – ndmeiri Jul 07 '15 at 05:22
  • Works for me. Make sure you've set the navigation controller identifiers. Swift 1.2 – slider Jul 07 '15 at 05:26
  • I did set the storyboard identifiers for the navigation controllers. I think I have a solution to the duplicate view controller issue though. I'll investigate and report back. – ndmeiri Jul 07 '15 at 05:32
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/82559/discussion-between-ndmeiri-and-dperk). – ndmeiri Jul 07 '15 at 05:32

1 Answers1

2

The issue has to do with your calculation of the index variable in your pageViewController(_:viewControllerBeforeViewController:) and pageViewController(_:viewControllerAfterViewController:) methods. To simplify that logic and the overall logic of your page view controller, you should change your implementation to the following:

class PageViewController: UIPageViewController, UIPageViewControllerDataSource, UIPageViewControllerDelegate {

    private var pages: [UIViewController]!

    override func viewDidLoad() {
        super.viewDidLoad()

        self.dataSource = self
        self.delegate = self

        self.pages = [
            self.storyboard!.instantiateViewControllerWithIdentifier("FirstNav") as! UINavigationController,
            self.storyboard!.instantiateViewControllerWithIdentifier("SecondNav") as! UINavigationController,
            self.storyboard!.instantiateViewControllerWithIdentifier("ThirdNav") as! UINavigationController,
            self.storyboard!.instantiateViewControllerWithIdentifier("FourthNav") as! UINavigationController
        ]

        let startingViewController = self.pages.first! as UIViewController
        self.setViewControllers([startingViewController], direction: .Forward, animated: false, completion: nil)
    }

    func pageViewController(pageViewController: UIPageViewController, viewControllerAfterViewController viewController: UIViewController) -> UIViewController? {
        let index = (self.pages as NSArray).indexOfObject(viewController)

        // if currently displaying last view controller, return nil to indicate that there is no next view controller
        return (index == self.pages.count - 1 ? nil : self.pages[index + 1])
    }

    func pageViewController(pageViewController: UIPageViewController, viewControllerBeforeViewController viewController: UIViewController) -> UIViewController? {
        let index = (self.pages as NSArray).indexOfObject(viewController)

        // if currently displaying first view controller, return nil to indicate that there is no previous view controller
        return (index == 0 ? nil : self.pages[index - 1])
    }

}

You don't even need to maintain an index instance variable, and this implementation does not do so. But you could, if you wanted. This solution also instantiates a single instance of each UINavigationController instead of instantiating a new one every time the user attempts to scroll to a different page, which conserves memory and preserves the state of the view controllers as the user scrolls between them.

Please excuse the non-descriptive pages variable name. I didn't want to create a conflict with UIPageViewController's viewControllers property.

ndmeiri
  • 4,979
  • 12
  • 37
  • 45
  • 1
    You, sir...you're good. Also, memory preservation was a huge issue in my previous method. Pages were reloading on every appearance. This works perfectly. – slider Jul 07 '15 at 06:24
  • How would I change the index of the current page programmatically? Let's say I have a bar button icon that transitions to a different navigation controller in the `self.pages` array. Can I do that? – slider Jul 09 '15 at 08:11
  • Yes. Call the UIPageViewController method `setViewControllers(_:direction:animated:completion:)` on `self` and pass in an array of a single view controller: `[self.pages[newIndex]]`. Either pass `.Forward` or `.Reverse` for the direction, depending on if `newIndex` is greater than or less than the current index (which would require maintaining a `currentIndex` variable). You could also factor this out into a separate method. Let me know if you'd like more clarification. – ndmeiri Jul 09 '15 at 16:24
  • I'm setting the function `func showDiscover() { PageViewController.setViewControllers([PageViewController.pages[1]], direction: .Forward, animated: false, completion: nil) }` in my view controller but am getting the error: `PageViewController.type does not have a member named 'pages'`. Did I implement this incorrectly? – slider Jul 09 '15 at 18:33
  • You need to put this method in your `PageViewController` class, or maintain a reference to your "parent" `PageViewController` instance in each of the "child" view controllers. If you choose the first option, replace `PageViewController.pages[1]` with `self.pages[1]`. If you choose the second option, replace `PageViewController.pages[1]` with `self.parentPageViewController.pages[1]`. – ndmeiri Jul 09 '15 at 18:39
  • The first method requires less code, so I prefer that. How would you implement the method in `PageViewController` using the above code? The VC's don't load for me. – slider Jul 10 '15 at 02:22
  • I've been working on this for 3 days, but haven't tackled it. Would you please be so kind as to advise? – slider Jul 11 '15 at 18:31
  • You need to clearly explain what you are trying to accomplish. I know that you're trying to change the page programmatically. But I do not know from which class you are trying to do that. Is it one of the child view controllers or from the `PageViewController`? – ndmeiri Jul 11 '15 at 18:40
  • I have bar button icons on every view controller that correspond to a different page. When I click those bar buttons, they should change the index of the page view controller as if segueing to another VC. – slider Jul 11 '15 at 18:42
  • Can you not put all those bar button items in the main `UIPageViewController`? – ndmeiri Jul 11 '15 at 18:44
  • Don't think so? There are different buttons on each page. I was planning on writing separate functions for each button in their own view controllers. – slider Jul 11 '15 at 18:48
  • Okay, I understand. First, you need to obtain a reference to the `PageViewController` that is housing the child view controllers. This could be done by adding a weak `parentPageViewController` var to each of your child view controllers. And then, you need to set this property to your `PageViewController` instance for every view controller in `self.pages`. That could be done in `viewDidLoad()`. – ndmeiri Jul 11 '15 at 18:55
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/83043/discussion-between-dperk-and-ndmeiri). – slider Jul 11 '15 at 19:07