Swift iOS: how to trigger next page using buttons

Kim Stacks picture Kim Stacks · Oct 29, 2015 · Viewed 25.6k times · Source

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)
    }
}

Answer

matt picture matt · Nov 1, 2015

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.