Swift iOS: how to trigger next page using buttons
Asked Answered
P

6

10

enter image description here

enter image description here

I have a QuizViewController which extends UIViewController, UIPageControllerDelegate, and a UIPageViewControllerDataSource.

Inside QuizViewController.swift

private var pageViewController: UIPageViewController?

private func createPageViewController() {
       let pageController = self.storyboard!.instantiateViewControllerWithIdentifier("QuizPageViewController") as! UIPageViewController
       pageController.dataSource = self

       if pageInfo.count > 0 {
           let firstController = getItemController(0)!
           let startingViewControllers: NSArray = [firstController]
           pageController.setViewControllers(startingViewControllers as? [UIViewController], direction: UIPageViewControllerNavigationDirection.Forward, animated: false, completion: nil)
       }

       pageViewController = pageController
       addChildViewController(pageViewController!)
       self.view.addSubview(pageViewController!.view)
       pageViewController!.didMoveToParentViewController(self)
   }

   private func getItemController(itemIndex: Int) -> QuizPageItem? {
       if itemIndex < pageInfo.count {
           let CurrentQuizPageItem = self.storyboard!.instantiateViewControllerWithIdentifier("QuizPageItem") as! QuizPageItem
           CurrentQuizPageItem.itemIndex = itemIndex
           CurrentQuizPageItem.thisPageInfo = pageInfo[itemIndex]
           return CurrentQuizPageItem
       }

       return nil
   }

   /*
       PageView Delegates
   */
   func pageViewController(pageViewController: UIPageViewController, viewControllerBeforeViewController viewController: UIViewController) -> UIViewController? {

       let CurrentQuizPageItem = viewController as! QuizPageItem

       if CurrentQuizPageItem.itemIndex > 0 {
           return getItemController(CurrentQuizPageItem.itemIndex - 1)
       }

       return nil
   }

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

       let CurrentQuizPageItem = viewController as! QuizPageItem

       if CurrentQuizPageItem.itemIndex + 1 < pageInfo.count {
           return getItemController(CurrentQuizPageItem.itemIndex + 1)
       }

       return nil
   }

My question is how do I get the next and back buttons to work properly?

Right now the pages can be changed via swipe. I don't want to use swipe. I want to control using the next and back buttons.

UPDATE

enter image description here

This is on the Main.storyboard

PageViewController is the middle one in the storyboard.

PageViewController has the storyboard id as QuizPageViewController.

QuizViewController is the left one in the storyboard.

QuizViewController instantiates a PageViewController using the storyboard id QuizPageViewController

which is done by this block

       let pageController = self.storyboard!.instantiateViewControllerWithIdentifier("QuizPageViewController") as! UIPageViewController
       pageController.dataSource = self

When this instantiation happens, it also creates the front page for the QuizPageItem.

QuizPageItem is the right most view in the Main.storyboard.

So if you see the 2 mockups, they are both QuizPageItems.

The first mockup should have a itemIndex of 0.

The second mockup should have itemIndex of 1.

       let CurrentQuizPageItem = self.storyboard!.instantiateViewControllerWithIdentifier("QuizPageItem") as! QuizPageItem
       CurrentQuizPageItem.itemIndex = itemIndex

Most of the answers I have received suggests solving it via the QuizViewController aka the left most view in my Main.storyboard.

However, the buttons are in the QuizPageItem and not accessible via the QuizViewController.

I want to know how I can connect the back/next buttons in the QuizPageItem to execute the pagination which is controlled in the QuizViewController assuming the way I am wiring up the various views in the Main.storyboard is correct

At the same time, I allow the possibility that the current way I am wiring up the various views in the Main.storyboard is not ideal.

If so, please advise an alternative way.

This is the tutorial I follow to get to where I am currently at.

http://shrikar.com/ios-swift-tutorial-uipageviewcontroller-as-user-onboarding-tool/

UPDATE 2 I apologise that I am seen as arguing. I genuinely want to learn how to do this. I am pretty sure I am lacking some fundamental knowledge hence I am unable to understand Michael Dautermann's answer.

I assume there is a function in the QuizViewController that will trigger the page turning.

I am not sure what that function is.

These are the functions that I know will get triggered when the buttons are pressed.

I have the following inside the QuizPageItem class

 @IBAction func pageBackButton(sender: AnyObject) {

 }

 @IBAction func pageNextButton(sender: AnyObject) {
 }

However, they are not in the QuizViewController class but in the QuizPageItem class.

Am I supposed to put the setViewController method in these two functions inside QuizPageItem class?

And if so, how do I even access the QuizViewController instance from inside the QuizPageItem class?

UPDATE 3:

My file structure is

  • QuizViewController.swift
  • QuizPageItem.swift

The QuizViewController controls which QuizPageItem you see. A QuizPageItem represents a different view as designed by the mockup.

UPDATE4:

matt's answer helped me a lot with understanding this FirstResponder which I was totally unfamiliar with in the first place.

When I tried to implement it, I was faced with this error.

enter image description here

I have googled around and I have tried to remedy it to no avail.

I kept triggering this error.

Attached is the code snippet for the QuizViewPageItemController.swift

import UIKit
class QuizPageItemViewController: UIViewController, CheckboxDelegate {

    @IBOutlet weak var pageHeadingLabel: UILabel!
    @IBOutlet weak var pageInstructionLabel: UILabel!
    @IBOutlet weak var pageProgressView: UIProgressView!
    @IBOutlet weak var pageQuestionLabel: UILabel!
    @IBOutlet weak var pageAnswerView: UIView!
    @IBOutlet weak var pageBackButton: UIButton!
    @IBOutlet weak var pageNextButton: UIButton!
    let pageNo: Int 
    let maxPageNo: Int
    let thisPageInfo: [String]

    let interestsList = [
        "Blue Chips", "Small Caps", "Pharmaceuticals", "Agriculture",
        "Telecommunications", "Manufacturing", "Finance", "Banks",
        "Retail", "Travel", "Airlines", "Tourism"]

    init(pageNo: Int, maxPageNo: Int, thisPageInfo: [String]) {
        self.pageNo = pageNo
        self.maxPageNo = maxPageNo
        self.thisPageInfo = thisPageInfo

        super.init(nibName: nil, bundle: nil)
    }
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        self.pageBackButton.hidden = pageNo == 0
        self.pageNextButton.hidden = pageNo == maxPageNo
        pageHeadingLabel.text = thisPageInfo[0]
        pageInstructionLabel.text = thisPageInfo[1]
        pageQuestionLabel.text = thisPageInfo[2]

        if thisPageInfo[0] == "Welcome" {
            createCheckboxes()
            pageProgressView.setProgress(0.33, animated: true)
        } else if thisPageInfo[0] == "Awesome!" {
            createSlider()
            pageProgressView.setProgress(0.67, animated: true)
        } else if thisPageInfo[0] == "Almost there..." {
            createSlider()
            pageProgressView.setProgress(0.95, animated: true)
        }
    }

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

    func createCheckboxes() {
        let fWidth = (self.view.frame.width - 40) / 2
        var frame = CGRectMake(0, 0, fWidth, 40)
        var contentSize = CGRectMake(0, 0, self.view.frame.width - 40, 0)

        for (var counter = 0; counter < interestsList.count; counter++) {
            let checkbox = Checkbox(frame: frame, title: interestsList[counter], selected: false)
            checkbox.mDelegate = self
            checkbox.tag = counter
            checkbox.backgroundColor = UIColor.redColor()

            if counter % 2 == 0 {
                frame.origin.x += fWidth
            } else{
                frame.origin.x -= fWidth
                frame.origin.y += frame.size.height
                contentSize.size.height += frame.size.height
            }

            checkbox.titleLabel?.adjustsFontSizeToFitWidth = true
            pageAnswerView.addSubview(checkbox)
        }
    }

    func didSelectCheckbox(state: Bool, identifier: Int, title: String) {
        print("checkbox '\(title)' has state \(state)")
    }

    func createSlider() {
        let slider = UISlider(frame:CGRectMake(0, 20, self.view.frame.width - 40, 20))
        slider.minimumValue = 0
        slider.maximumValue = 10
        slider.continuous = true
        slider.tintColor = ChatQColours().chatQBlue
        slider.value = 5
        //        slider.addTarget(self, action: "sliderValueDidChange:", forControlEvents: .ValueChanged)
        pageAnswerView.addSubview(slider)

        let leftlabel = UILabel(frame: CGRectMake(0, 40, 0, 0))
        leftlabel.text = "Strongly Avoid"
        leftlabel.sizeToFit()
        pageAnswerView.addSubview(leftlabel)

        let rightlabel = UILabel(frame: CGRectMake(0, 40, 0, 0))
        rightlabel.text = "Strongly Prefer"
        rightlabel.sizeToFit()
        rightlabel.frame.origin.x = slider.frame.width - rightlabel.frame.width
        pageAnswerView.addSubview(rightlabel)
    }
}
Proportioned answered 29/10, 2015 at 6:51 Comment(1)
#7209371 @Yogesh answer may be helpfulDatary
E
17

Michael Dautermann's answer is perfectly correct, as this screencast shows:

enter image description here

What you're seeing is a page view controller with multiple pages (numbered so you can see the order), each page containing a Next button, and I'm repeatedly pressing the Next button to navigate to the next page.

Like yours, my project, illustrated in the screencast above, has a view controller hierarchy:

  • UITabBarController

  • ViewController

  • UIPageViewController

  • Page (which has a main view, and the label and buttons are subviews of that)

It appears that the heart of your question is not so much what method causes a page view controller to navigate to its next or previous page — that, as you have already been told, is simply setViewControllers:... — but how the button communicates up the view controller hierarchy. In my example, that means sending a message from the button inside Page's view, past the Page view controller, past the UIPageViewController, and up to the ViewController, which then tells th UIPageViewController what to do.

I can think of numerous ways to do that:

  • The button posts a notification for which the ViewController is registered

  • The button sends a nil-targeted action for which the ViewController has a handler

  • The button sends a message to the tab bar controller (its tabBarController property), which then sends a message down to its currently selected view controller, the ViewController

  • The button sends a message to its view controller (configured in the nib or storyboard as an action), which sends a message to its parentViewController!.parentViewController!, which is the ViewController.

Which do I prefer? Personally, I like the nil-targeted action best, because it requires no extra code. The only func pageNextButton() implementation is in ViewController. That is beauty of a nil-targeted action: it walks up the responder chain, looking for a recipient, automatically. The Page view controller and the UIPageViewController have no code at all in this regard.

I like this much better than parentViewController!.parentViewController!, because the latter requires ultimately that the Page view controller knows the name of a method in the ViewController that it can call, and it must cast down to a ViewController — which is not very portable and gives the Page view controller too much knowledge about its environment, in my opinion. With a nil-targeted action, on the other hand, the sender is totally agnostic about who the actual target will turn out to be! So I like nil-target action best, and notification second best.

Essam answered 1/11, 2015 at 3:40 Comment(8)
I apologise that I am seen as arguing. I genuinely want to learn how to do this. I am pretty sure I am lacking some fundamental knowledge hence I am unable to understand Michael Dautermann's answer. I have added more info to my question. Thank you for trying to get me to the 'aha' moment.Proportioned
Okay, so what it is that you still don't know? Your question has been repeatedly answered. The button in the QuizPageItem tells the QuizViewController (or the QuizPageViewController) that it has been tapped, and the QuizViewController (or the QuizPageViewController) goes to the next or previous page accordingly, by calling setViewControllers:... as you've already been told multiple times. So you should do that and accept one of the answers.Essam
Look, I made my screencast look even more like your spec drawings. I'm doing exactly what you're describing, and I'm doing it exactly as Michael Dautermann and the others have said. The answers you've been are the answer. What more proof do you need?Essam
Thanks for your response. My follow up question is how does the button in the QuizPageItem tell the QuizViewController that it has been tapped? As far as I can tell, I only know that the @IBAction func pageNextButton(sender: AnyObject) has been triggered when the button is tapped. And that func is inside the QuizPageItem and not inside the QuizViewController. I genuinely don't grok it yet. I apologise for any frustration caused.Proportioned
Very well, I've added some further discussion. If you don't understand the responder chain and nil-targeted actions, you might want to read my book: apeth.com/iOSBook/ch11.html#_nil_targeted_actions If you don't know what notifications are: apeth.com/iOSBook/ch13.html#_notifications_2 If you don't know about parentViewController and view controller hierarchy, you might need to read the whole chapter on view controllers: apeth.com/iOSBook/ch19.htmlEssam
In case you still don't get it, I've posted the example project on github for you: github.com/mattneub/pageViewControllerUsingInternalButtonsEssam
Thank you. Your github repo helped me to better do this. However, I have run into problems when I followed the nil-targeted actions method, I keep triggering this fatal error about bad instruction inside the required init?(coder aDecoder: NSCoder) { method. I have googled around and tried other remedies suggested in other StackOverflow questions. None of them worked. I have left the code snippet up in the question under Update 4. What do you suggest?Proportioned
That's a completely different matter, @KimStacks. Please don't come back and back asking new questions in the comments; that is called being a "leech" on Stack Overflow. If you have a new question, ask a new question!Essam
S
14

Swift 3.0

Very simple solution is here

To change page (UIViewController) from a UIViewController, first get the instance of Parent ViewController (which will be UIPageViewController) and then set its current ViewController

In my case I have a UIPageViewController named "UserDetailsPVC" and it contains 4 pages (UIViewControllers) which are as follows

  PageOneVC:UIViewController
  PageTwoVC:UIViewController
  PageThreeVC:UIViewController
  PageFourVC:UIViewController

In the UIPageViewController lets define an array of pages

 var pages:[UIViewController] = [
        UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "PageOneVC"),
        UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "PageTwoVC"),
        UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "PageThreeVC"),
        UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "PageFourVC")
]

Now to change page of UIPageViewController from any of the UIViewController

    // get parent view controller
    let parentVC = self.parent as! UserDetailsPVC

    // change page of PageViewController
    parentVC.setViewControllers([parentVC.pages[1]], direction: .forward, animated: true, completion: nil)
Solis answered 22/6, 2017 at 5:42 Comment(0)
C
8

You can use setViewControllers:direction:animated:completion: to turn your pages programatically.

Just pass along the previous or next page's view controller and you should be all set.

Cottier answered 29/10, 2015 at 7:6 Comment(3)
Hi there, I have updated the question. I couldn't use your answer because they are all the same ViewController (QuizPageItem). I cannot just pass in the next view controller.Proportioned
@KimStacks Your question has been answered. The way to turn pages programmatically is to tell the page view controller to setViewControllers:direction:animated:completion:. You need to stop arguing and start listening.This has nothing to do with the button being inside the page inside the page view controller. You can still send the message.Essam
I apologise that I am seen as arguing. I genuinely want to learn how to do this. I am pretty sure I am lacking some fundamental knowledge hence I am unable to understand your answer. I have added more info to my question. Thank you for trying to get me to the 'aha' moment.Proportioned
H
3

You can use:

For next page

pageViewController.setViewControllers(startingViewControllers,
                                                direction: UIPageViewControllerNavigationDirection.Forward,
                                                animated: true,
                                                completion: nil)

For previous page

pageViewController.setViewControllers(startingViewControllers,
                                                direction: UIPageViewControllerNavigationDirection.Reverse,
                                                animated: true,
                                                completion: nil)
Heiner answered 29/10, 2015 at 7:9 Comment(1)
Hi there, I have updated the question. I couldn't use your answer because the buttons are inside QuizPageItem. My updated question will shed more light.Proportioned
D
1

You can use this function:

func movePageToIndex(pageIndex NSInteger)->() {
    var viewControllers = []
    var direction : UIPageViewControllerNavigationDirection! 
    if (pageIndex < currentIndex) {
        direction = UIPageViewControllerNavigationDirection.Reverse
    } else {
        direction = UIPageViewControllerNavigationDirection.Forward
    }
    viewControllers = @[[self.getItemController(pageIndex)]]

    WEAKSELF weakSelf = self;
    self.pageViewController.setViewControllers:viewControllers direction:direction animated:false completion:nil];
}
Dorita answered 29/10, 2015 at 8:13 Comment(1)
Hi there, I have updated the question. I couldn't use your answer because the buttons are inside QuizPageItem. My updated question will shed more light.Proportioned
O
0

Swift 4.2 Very easy way to move on your desire View Controller

//MARK:- Programatically move to Next Page or Previous Page
//Your Desire index in Your UIViewController Array
    let arr : [UIViewController] = [arrvc[1]] 
    pagecontroller.setViewControllers(arr,
                                    direction: UIPageViewController.NavigationDirection.reverse,
                                    animated: true,
                                    completion: nil)
Otherwhere answered 2/3, 2019 at 7:22 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.