31

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.

rafalio
  • 3,928
  • 4
  • 30
  • 33

5 Answers5

19

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

hahmed
  • 645
  • 7
  • 16
  • 1
    No way around to use a `let` instead of a `var` here? – Tulleb Dec 12 '18 at 12:02
  • But if that’s the detail view controller, which is initialized by storyboard containing the UIsplitviewcontroller, when do you call your factory method? do you need to remove the detail relationship to it in the storyboard? – aneuryzm Jul 11 '19 at 09:50
5

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)

hahmed
  • 645
  • 7
  • 16
2

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.

dlemex
  • 326
  • 2
  • 6
1

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

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.

Nazariy Vlizlo
  • 778
  • 7
  • 16
0

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.

Community
  • 1
  • 1
Tulleb
  • 8,919
  • 8
  • 27
  • 55
  • See my answer http://stackoverflow.com/a/41926532/2722398. As you admitted your method is ugly. It should be removed before people start adopting this as a practice. I have had to do ugly cleanups because people started using AppDelegate as a global buffer. – hahmed Jan 29 '17 at 22:16
  • @hahmed Sorry if I offended your iOS expertise haha. I just think it's better if people have the choice between an ugly but easy solution and a complicated and longer one. – Tulleb Jan 29 '17 at 22:46
  • 1
    here is an easy and clean solution for simpler implementation: http://stackoverflow.com/a/41926924/2722398 – hahmed Jan 29 '17 at 22:59
  • @hahmed way better, thanks! I edit my answer to point on yours. – Tulleb Jan 29 '17 at 23:10
  • 1
    Just don't use storyboards – Christian Anchor Dampf Sep 20 '18 at 11:26