Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

PageViewController - Pass variables to child views

What I have

I have a ViewController (TutorialViewController) and a UIPageViewController (TutorialPageViewController). There are also 3 extra views on the storyboard with StoryBoard ID's:

  • GreenViewController
  • BlueViewController
  • RedViewController

I have been following this tutorial (Kudos to the author, very well written).

On the Green View Controller I have defined a variable:

var passedVariable = ""

And in the ViewDidLoad I print it out.

Here are the two controllers that have the code:

UIViewController (TutorialViewController):

class TutorialViewController: UIViewController {

@IBOutlet weak var pageControl: UIPageControl!
@IBOutlet weak var containerView: UIView!


var tutorialPageViewController: TutorialPageViewController? {
    didSet {
        tutorialPageViewController?.tutorialDelegate = self
    }
}


override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
    if let tutorialPageViewController = segue.destinationViewController as? TutorialPageViewController {
        self.tutorialPageViewController = tutorialPageViewController
    }
}

@IBAction func didTapNextButton(sender: UIButton) {
    tutorialPageViewController?.scrollToNextViewController()
}
}

extension TutorialViewController: TutorialPageViewControllerDelegate {

func tutorialPageViewController(tutorialPageViewController: TutorialPageViewController,
    didUpdatePageCount count: Int) {
    pageControl.numberOfPages = count
}

func tutorialPageViewController(tutorialPageViewController: TutorialPageViewController,
    didUpdatePageIndex index: Int) {
    pageControl.currentPage = index
}

}

UIPageViewController

class TutorialPageViewController: UIPageViewController {

weak var tutorialDelegate: TutorialPageViewControllerDelegate?

//let vc0 = GreenViewController(nibName: "GreenViewController", bundle: nil)

private(set) lazy var orderedViewControllers: [UIViewController] = {
    // The view controllers will be shown in this order
    return [self.newColoredViewController("Green"),
        self.newColoredViewController("Red"),
        self.newColoredViewController("Blue"), self.newColoredViewController("Pink")]
}()

override func viewDidLoad() {
    super.viewDidLoad()

    //self.vc0.passedVariable = "Passed Data"

    dataSource = self
    delegate = self

    if let initialViewController = orderedViewControllers.first {
        scrollToViewController(initialViewController)
    }

    tutorialDelegate?.tutorialPageViewController(self,
        didUpdatePageCount: orderedViewControllers.count)
}


/**
 Scrolls to the next view controller.
 */
func scrollToNextViewController() {
    if let visibleViewController = viewControllers?.first,
        let nextViewController = pageViewController(self,
            viewControllerAfterViewController: visibleViewController) {
                scrollToViewController(nextViewController)
    }
}

private func newColoredViewController(color: String) -> UIViewController {
    return UIStoryboard(name: "Main", bundle: nil) .
        instantiateViewControllerWithIdentifier("\(color)ViewController")
}

/**
 Scrolls to the given 'viewController' page.

 - parameter viewController: the view controller to show.
 */
private func scrollToViewController(viewController: UIViewController) {
    setViewControllers([viewController],
        direction: .Forward,
        animated: true,
        completion: { (finished) -> Void in
            // Setting the view controller programmatically does not fire
            // any delegate methods, so we have to manually notify the
            // 'tutorialDelegate' of the new index.
            self.notifyTutorialDelegateOfNewIndex()
    })
}

/**
 Notifies '_tutorialDelegate' that the current page index was updated.
 */
private func notifyTutorialDelegateOfNewIndex() {
    if let firstViewController = viewControllers?.first,
        let index = orderedViewControllers.indexOf(firstViewController) {
            tutorialDelegate?.tutorialPageViewController(self,
                didUpdatePageIndex: index)
    }
}

}

// MARK: UIPageViewControllerDataSource

extension TutorialPageViewController: UIPageViewControllerDataSource {

func pageViewController(pageViewController: UIPageViewController,
    viewControllerBeforeViewController viewController: UIViewController) -> UIViewController? {
        guard let viewControllerIndex = orderedViewControllers.indexOf(viewController) else {
            return nil
        }

        let previousIndex = viewControllerIndex - 1

        // User is on the first view controller and swiped left to loop to
        // the last view controller.
        guard previousIndex >= 0 else {
            return orderedViewControllers.last
        }

        guard orderedViewControllers.count > previousIndex else {
            return nil
        }

        return orderedViewControllers[previousIndex]
}

func pageViewController(pageViewController: UIPageViewController,
    viewControllerAfterViewController viewController: UIViewController) -> UIViewController? {
        guard let viewControllerIndex = orderedViewControllers.indexOf(viewController) else {
            return nil
        }

        let nextIndex = viewControllerIndex + 1
        let orderedViewControllersCount = orderedViewControllers.count

        // User is on the last view controller and swiped right to loop to
        // the first view controller.
        guard orderedViewControllersCount != nextIndex else {
            return orderedViewControllers.first
        }

        guard orderedViewControllersCount > nextIndex else {
            return nil
        }

        return orderedViewControllers[nextIndex]
}

}

extension TutorialPageViewController: UIPageViewControllerDelegate {

func pageViewController(pageViewController: UIPageViewController,
    didFinishAnimating finished: Bool,
    previousViewControllers: [UIViewController],
    transitionCompleted completed: Bool) {
    notifyTutorialDelegateOfNewIndex()
}

}

protocol TutorialPageViewControllerDelegate: class {

/**
 Called when the number of pages is updated.

 - parameter tutorialPageViewController: the TutorialPageViewController instance
 - parameter count: the total number of pages.
 */
func tutorialPageViewController(tutorialPageViewController: TutorialPageViewController,
    didUpdatePageCount count: Int)

/**
 Called when the current index is updated.

 - parameter tutorialPageViewController: the TutorialPageViewController instance
 - parameter index: the index of the currently visible page.
 */
func tutorialPageViewController(tutorialPageViewController: TutorialPageViewController,
    didUpdatePageIndex index: Int)

}

What I have tried

I have tried declaring the View Controller first like so:

let vc0 = GreenViewController(nibName: "GreenViewController", bundle: nil)

And then passing the data like so:

override func viewDidLoad() {
   vc0.passedVariable = "This was passed, Dance with Joy"
}

Nothing is printing out in the console.

I also tried changing the bundle above to:

bundle: NSBundle.mainBundle()

Still nada

Question

I plan to load data on the TutorialViewController from an alamofire request, I want to pass that data to one of the ViewControllers (green, blue, red)

How do I pass data that has been acquired from the TutorialViewController to one of the child views that will load?

like image 852
JamesG Avatar asked Sep 26 '22 06:09

JamesG


2 Answers

First, I want to thank you for checking out my tutorial and all of the nice things you said about it.

Second, I have a solution for you! I went ahead and committed the solution to the GitHub repo I linked in the tutorial. I will also post the code here.

(1) Create a UIViewController subclass to add custom properties to. For this example, I chose to add a UILabel since it's the easiest to view when running the app.

class ColoredViewController: UIViewController {

    @IBOutlet weak var label: UILabel!

}

(2) Inside Main.storyboard, change the custom class for each UIViewController "page" to ColoredViewController in the Identity Inspector.

Custom Class Set To ColoredViewController

(3) Add a UILabel to each "page" and constraint it however you'd like. I chose to vertically and horizontally center it in the container. Don't forget to link the UILabel to ColoredViewController's @IBOutlet weak var label: UILabel!.

UILabel On Each Page

(4) Optional: I deleted the default "Label" text in each one that way if we never set the label's text in code, we will not show "Label" to the user.

Deleted Label Default Text

(5) We need to do some TLC to TutorialPageViewController so it knows that orderedViewControllers is now a ColoredViewController array. To make things easy, I'm just going to paste the entire class:

class TutorialPageViewController: UIPageViewController {

    weak var tutorialDelegate: TutorialPageViewControllerDelegate?

    private(set) lazy var orderedViewControllers: [ColoredViewController] = {
        // The view controllers will be shown in this order
        return [self.newColoredViewController("Green"),
            self.newColoredViewController("Red"),
            self.newColoredViewController("Blue")]
    }()

    override func viewDidLoad() {
        super.viewDidLoad()

        dataSource = self
        delegate = self

        if let initialViewController = orderedViewControllers.first {
            scrollToViewController(initialViewController)
        }

        tutorialDelegate?.tutorialPageViewController(self,
            didUpdatePageCount: orderedViewControllers.count)
    }

    /**
     Scrolls to the next view controller.
     */
    func scrollToNextViewController() {
        if let visibleViewController = viewControllers?.first,
            let nextViewController = pageViewController(self,
                viewControllerAfterViewController: visibleViewController) {
                    scrollToViewController(nextViewController)
        }
    }

    private func newColoredViewController(color: String) -> ColoredViewController {
        return UIStoryboard(name: "Main", bundle: nil) .
            instantiateViewControllerWithIdentifier("\(color)ViewController") as! ColoredViewController
    }

    /**
     Scrolls to the given 'viewController' page.

     - parameter viewController: the view controller to show.
     */
    private func scrollToViewController(viewController: UIViewController) {
        setViewControllers([viewController],
            direction: .Forward,
            animated: true,
            completion: { (finished) -> Void in
                // Setting the view controller programmatically does not fire
                // any delegate methods, so we have to manually notify the
                // 'tutorialDelegate' of the new index.
                self.notifyTutorialDelegateOfNewIndex()
        })
    }

    /**
     Notifies '_tutorialDelegate' that the current page index was updated.
     */
    private func notifyTutorialDelegateOfNewIndex() {
        if let firstViewController = viewControllers?.first as? ColoredViewController,
            let index = orderedViewControllers.indexOf(firstViewController) {
                tutorialDelegate?.tutorialPageViewController(self,
                    didUpdatePageIndex: index)
        }
    }

}

// MARK: UIPageViewControllerDataSource

extension TutorialPageViewController: UIPageViewControllerDataSource {

    func pageViewController(pageViewController: UIPageViewController,
        viewControllerBeforeViewController viewController: UIViewController) -> UIViewController? {
            guard let coloredViewController = viewController as? ColoredViewController,
                let viewControllerIndex = orderedViewControllers.indexOf(coloredViewController) else {
                return nil
            }

            let previousIndex = viewControllerIndex - 1

            // User is on the first view controller and swiped left to loop to
            // the last view controller.
            guard previousIndex >= 0 else {
                return orderedViewControllers.last
            }

            guard orderedViewControllers.count > previousIndex else {
                return nil
            }

            return orderedViewControllers[previousIndex]
    }

    func pageViewController(pageViewController: UIPageViewController,
        viewControllerAfterViewController viewController: UIViewController) -> UIViewController? {
            guard let coloredViewController = viewController as? ColoredViewController,
                let viewControllerIndex = orderedViewControllers.indexOf(coloredViewController) else {
                return nil
            }

            let nextIndex = viewControllerIndex + 1
            let orderedViewControllersCount = orderedViewControllers.count

            // User is on the last view controller and swiped right to loop to
            // the first view controller.
            guard orderedViewControllersCount != nextIndex else {
                return orderedViewControllers.first
            }

            guard orderedViewControllersCount > nextIndex else {
                return nil
            }

            return orderedViewControllers[nextIndex]
    }

}

extension TutorialPageViewController: UIPageViewControllerDelegate {

    func pageViewController(pageViewController: UIPageViewController,
        didFinishAnimating finished: Bool,
        previousViewControllers: [UIViewController],
        transitionCompleted completed: Bool) {
        notifyTutorialDelegateOfNewIndex()
    }

}

protocol TutorialPageViewControllerDelegate: class {

    /**
     Called when the number of pages is updated.

     - parameter tutorialPageViewController: the TutorialPageViewController instance
     - parameter count: the total number of pages.
     */
    func tutorialPageViewController(tutorialPageViewController: TutorialPageViewController,
        didUpdatePageCount count: Int)

    /**
     Called when the current index is updated.

     - parameter tutorialPageViewController: the TutorialPageViewController instance
     - parameter index: the index of the currently visible page.
     */
    func tutorialPageViewController(tutorialPageViewController: TutorialPageViewController,
        didUpdatePageIndex index: Int)

}

(6) Inside TutorialViewController: let's set the label.text. I chose to use viewDidLoad, but feel free to stuff this logic inside a network request completion block.

override func viewDidLoad() {
    super.viewDidLoad()

    if let greenColoredViewController = tutorialPageViewController?.orderedViewControllers.first {
        greenColoredViewController.label.text = "Hello world!"
    }
}

Hope this helps!

like image 138
Jeff Avatar answered Nov 17 '22 21:11

Jeff


Obviously, according the comments, there's still confusion how this can be solved.

I'll try to introduce one approach and explain way this might make sense. Note though, that there are a few other viable approaches which can solve this problem.

The root view controller

First we take a look at the "root" controller which is an instance of TutorialViewController. This one is responsible to fetch/get/obtain/retrieve a "model". The model is and instance of pure data. It must be appropriate to define and initialise the page view controllers. Since we have a number of pages, it makes sense this model is some kind of array or list of some kind of objects.

For this example, I use an array of strings - just in order to illustrate how this can be implemented. A real example would obtain an array of likely more complex objects, where each of it will be rendered in its own page. Possibly, the array has been fetched from a remote resource with a network request.

In this example, the strings happen to be the "colour" of the page view controllers. We create an appropriate property for class TutorialViewController:

class TutorialViewController: UIViewController {

    @IBOutlet weak var pageControl: UIPageControl!
    @IBOutlet weak var containerView: UIView!


    private let model = ["Red", "Green", "Blue"]

    ...

Note that the property has private access: nobody else than the class itself should fiddle around with it.

Passing the Model from the Root Controller to its Embedded View Controller

The embedded view controller is an instance of TutorialPageViewController.

The root view controller passes the model to the embedded view controller in the method prepareForSegue. The embedded view controller must have an appropriate property which is suitable for its view of the model.

Note: A model may have several aspects or views. The model which has been initialised by the root view controller may not be appropriate to be passed as is to any of its presented view controllers. Thus, the root view controller may first filter, copy, reorder, or transform its model in order to make it suitable for the presented view controller.

Here, in this example, we take the model as is:

In class TutorialViewController:

override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
    if let tutorialPageViewController = segue.destinationViewController as? TutorialPageViewController {
        self.tutorialPageViewController = tutorialPageViewController
        self.tutorialPageViewController!.model = self.model
    }
}

Note that the TutorialViewController has itself a property (here model) which is set by the presenting view controller.

Here, the model is an array of strings. It should be obvious that the number of elements in the array should later become the number of pages in the page view controller. It should also be clear that each element is rendered on the corresponding page in a content view controller. Thus, we can say an element in the array serves as the "model" for each page.

We need to provide the property model in the TutorialPageViewController:

class TutorialPageViewController: UIPageViewController {

    internal var model: [String]?

Note that the access is either public or internal, so that any presenting view controller can set it.

Passing the Model from the TutorialViewController to each Content View Controller

A page view controller (TutorialViewController) is responsible to create an array of content view controllers whose view render the page.

An easy approach to create the array of view controllers utilising a lazy property is shown below:

class TutorialPageViewController: UIPageViewController {

    internal var model: [String]?

    private(set) lazy var orderedViewControllers: [UIViewController] = {
        // The view controllers will be shown in this order
        assert(self.model != nil)
        return self.model!.map {
            self.newColoredViewController($0)
        }
    }()

The important part is here:

        return self.model!.map {
            self.newColoredViewController($0)
        }

Here, we create N view controllers passing it the model (a String) in its factory function.

map returns an array of view controllers - suitable for the page view controller.

Once this has been implemented, the example works as in its original form.

You might now change the "factory" function which creates a view controller given a string as argument. For example you might set a label:

private func newColoredViewController(color: String) -> UIViewController {
    let vc = UIStoryboard(name: "Main", bundle: nil).instantiateViewControllerWithIdentifier("MyContentViewController") as! MyContentViewController
    vc.label = color 
    return vc
}

Here, again label is the "model" of the view controller. It's totally up the view controller how label will be rendered - if at all.

like image 25
CouchDeveloper Avatar answered Nov 17 '22 21:11

CouchDeveloper