rafalio
rafalio

Reputation: 3946

Custom init of UIViewController from storyboard

I tried finding some relevant questions but couldn't get anything, hope someone can help.

I set up some UIViewController's on a storyboard. I then want to load one of the view controllers in code and push it onto the navigation stack. I figure out the right way to do this is to use

instantiateViewControllerWithIdentifier

This calls init(coder: NSCoder) and all is well, my program works, but I want to be able to have a custom initializer that sets up some variables for my view controller. Consider the following:

class A : UIViewController {   

  let i : Int

  required init(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
    // property self.i not initialized at super.init call   
  }

}

I obviously get an error since i needs to be specified at time of object creation. Any solutions to this? I am not interested in declaring i as var and configuring it later as that defeats the point and I no longer have a compiler guarantee that i is immutable.

Clarification edit

Suppose I have a currently loaded ViewController that has some variable i. This value is variable and can change. Now suppose from this ViewController I want to present another one, and initialize it with i.

class ViewController: UIViewController {

   var i : Int

   // ... other things

  // in response to some button tap...
  @IBAction func tappedButton(sender: AnyObject) {
    let st = UIStoryboard(name: "Main", bundle: nil)
    let vc = st.instantiateViewControllerWithIdentifier("AControllerID") as! A
    // How do I initialize A with i ?
    self.presentViewController(vc, animated: true, completion: nil)
  }

}

I don't seem to be able to do this and keep i immutable by using let instead of var.

Upvotes: 32

Views: 22873

Answers (5)

Nazariy Vlizlo
Nazariy Vlizlo

Reputation: 987

Starting from iOS 13, you can use newly updated API, while instantiating UIViewController from Storyboard.

First, let's create custom initializer for UIViewController, that uses NSCoder:

final class ViewController: UIViewController {
    var someProperty: Int
    
    init(coder: NSCoder, someProperty: Int) {
        self.someProperty = someProperty
        super.init(coder: coder)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

Later on, you can create ViewController, using updated API:

let viewController = UIStoryboard.yourStoryboard.instantiateViewController(identifier: String(describing: ViewController.self)) { creator in
    let viewController = UIViewController(coder: creator, someProperty: someValue)
    return viewController
}

As you can see, in the closure, we're passing our new initializer. This method of viewController creating is powerful, as we can(finally!) use initializers, that depends on required init(coder: NSCoder), when viewController are creating with UIStoryboard. For more information, see here.

Upvotes: 7

dlemex
dlemex

Reputation: 326

As of iOS 13, there is a new feature in storyboards. If you use segue action, it is very simple to pass the data to a storyboard view controller as it is being instantiated. In interface builder, control-drag from the segue to the view controller that will be performing the segue. You then implement the additional arguments similar to:

In source VC:

@IBSegueAction func showChooser(_ coder: NSCoder, sender: Any?) -> ChooserTableViewController? {
    
    return ChooserTableViewController(coder: coder, useMap: true)
}

In the destination VC:

let useMap: Bool

init?(coder: NSCoder, useMap: Bool) {
        self.useMap = useMap
        super.init(coder: coder)
}

If you need to instantiate the VC without using a segue, there are new instantiate functions for storyboards which allow you to call the custom coder init during the view controller creation. See the UIStoryboardViewControllerCreator type for details.

Upvotes: 2

hahmed
hahmed

Reputation: 655

A simplification of my prior answer which is quick and avoids alternative hacky fixes:

Here is a detail view controller you may want to instantiate from storyboard with an objectID set:

import UIKit

class DetailViewController: UIViewController {

    var objectID : Int!

    internal static func instantiate(with objectID: Int) -> DetailViewController {

        let vc = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "DetailViewController") as DetailViewController
        vc.objectID = objectID
        return vc
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        if((objectID) != nil){
            print("Here is my objectID: \(objectID)")
        }
    }
}

Here is how you would use it to push onto a navigation controller with objectID set to 1:

self.navigationController.pushViewController(DetailViewController.instantiate(1), animated: true)

Added a blog post: https://theswiftcook.wordpress.com/2017/02/17/how-to-initialize-a-storyboard-viewcontroller-with-data-without-segues-swift-3-0git/

Link to example on GitHub: https://github.com/hammadzz/Instantiate-ViewController-From-Storyboard-With-Data

Upvotes: 19

Tulleb
Tulleb

Reputation: 9206

An (ugly) way to solve this issue:

You can set your let i from an external buffer in your code (AppDelegate variable in this example)

required init?(coder aDecoder: NSCoder) {
    self.i = UIApplication.shared().delegate.bufferForI

    super.init(coder: aDecoder)
}

And when you initiate your UIViewController through Storyboard:

UIApplication.shared().delegate.bufferForI = myIValue
self.navigationController!.pushViewControllerFading(self.storyboard!.instantiateViewController(withIdentifier: "myViewControllerID") as UIViewController)

EDIT: You don't have to pass the value through the AppDelegate. Better answer here.

Upvotes: 0

hahmed
hahmed

Reputation: 655

Below are two helpers, one is a Storyboard enum, add each and every storyboard in your project as a case under this enum. The name must match the {storyboard_name}.storyboard file. Each view controller in your storyboard should have its storyboard identifier set to the name of the class. This is pretty standard practice.

import UIKit

public enum Storyboard: String {
    case Main
    case AnotherStoryboard
    //case {storyboard_name}

    public func instantiate<VC: UIViewController>(_ viewController: VC.Type) -> VC {
        guard
            let vc = UIStoryboard(name: self.rawValue, bundle: nil)
                .instantiateViewController(withIdentifier: VC.storyboardIdentifier) as? VC
            else { fatalError("Couldn't instantiate \(VC.storyboardIdentifier) from \(self.rawValue)") }

        return vc
    }

    public func instantiateInitialVC() -> UIViewController {

        guard let vc = UIStoryboard(name: self.rawValue, bundle: nil).instantiateInitialViewController() else {
            fatalError("Couldn't instantiate initial viewcontroller from \(self.rawValue)")
        }

        return vc
    }
}

extension UIViewController {
    public static var defaultNib: String {
        return self.description().components(separatedBy: ".").dropFirst().joined(separator: ".")
    }

    public static var storyboardIdentifier: String {
        return self.description().components(separatedBy: ".").dropFirst().joined(separator: ".")
    }
}

Here is how you can instantiate from storyboard with a value set in your view controller. Here is the magic:

import UIKit

class DetailViewController: UIViewController {

    var objectID : Int!
    var objectDetails: ObjectDetails = ObjectDetails()        

    internal static func instantiate(with objectID: Int) -> DetailViewController {

        let vc = Storyboard.Main.instantiate(DetailViewController.self)
        vc.objectID = objectID
        return vc
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        if((objectID) != nil){
            // In this method I use to make a web request to pull details from an API
            loadObjectDetails()
        }
    }
}

(Architecture influenced/copies Kickstarter's open source iOS project)

Upvotes: 5

Related Questions