Home > Back-end >  How to initialize ViewController from storyboard using it's Coder
How to initialize ViewController from storyboard using it's Coder

Time:02-25

I realize that this might be difficult to achieve, but I would like to be able to initialize ViewControllers from storyboard using init(coder: NSCoder) function directly. I mean not using storyboard.instantiateViewController(identifier: coder: ) - I know I could use this, but this is what I would like to skip. Instead I would prefer to have a init in ViewController like this: init(viewModel: ViewModel) and use this init to initialize this ViewController from the storyboard. Inside this init I imagine having some mechanism that would open my storyboard, and extracted it's coder somehow, so that I could write:

static let storyboard = UIStoryboard(named: "Game")

private let viewModel: ViewModel

init(viewModel: ViewModel) {
    self.viewModel = viewModel
    let identifier = String(describing: Self.self)
    let coder: NSCoder = Self.storyboard.somehowGetTheCoderForViewController(withId: identifier)
    super.init(coder: coder)
}

Problem is how to read storyboard in such a way that I was able to get the coder of particular ViewController from it. Again - I know this can be solved by using something like this

storyboard.instantiateViewController(identifier: String(describing: Self.self)) { coder in
    Self.init(coder: coder, viewModel: viewModel)
}

But Im looking for a way to not use instantiateViewController, and just be able to get the coder, so that later I could just initiate VC like this:

let viewController = ViewContorller(viewModel: viewModel)

So the question is how to unpack storyboard, and retrieve coder object for some ViewController.

CodePudding user response:

You are not supposed to do this.

As the documentation of init(nibName:bundle:) says:

This is the designated initializer for this class. When using a storyboard to define your view controller and its associated views, you never initialize your view controller class directly. Instead, view controllers are instantiated by the storyboard either automatically when a segue is triggered or programmatically when your app calls the instantiateViewController(withIdentifier:) method of a storyboard object.

When initialising a VC using an initialiser, that initialiser is what you should use, not init(coder:). If you use storyboards, then you should use instantiateViewController, or use segues to get new VCs.

So to achieve this syntax:

let viewController = ViewContorller(viewModel: viewModel)

You can put your VCs in xib files, and do:

init(viewModel: ViewModel) {
    self.viewModel = viewModel
    let identifier = String(describing: Self.self)
    let coder: NSCoder = Self.storyboard.somehowGetTheCoderForViewController(withId: identifier)
    super.init(nibName: String(describing: Self.self), bundle: nil)
}

If you must use storyboards, then you can't use an initialiser syntax for initialising VCs. You can instead make a factory method, such as:

let viewController = ViewContorller.from(viewModel: viewModel)

And implement it using UIStoryboard.instantiateViewController.

CodePudding user response:

This is why the initializer instantiateViewController(identifier:creator:) exists.

https://developer.apple.com/documentation/uikit/uistoryboard/3213989-instantiateviewcontroller

You receive the coder and use it to call a custom initializer that itself calls the coder initializer but also does other custom initialization.

To illustrate, suppose we have a ViewController class with a message String property to be presented to the user in a UILabel when the view appears. So we've given ViewController an init(coder:message:) initializer:

class ViewController: UIViewController {
    @IBOutlet var lab : UILabel!
    var message: String = ""
    convenience init(coder:NSCoder, message:String) {
        self.init(coder:coder)!
        self.message = message
    }
    override func viewDidLoad() {
        super.viewDidLoad()
        self.lab.text = self.message
    }
}

Left to its own devices, however, the runtime will never call our init(coder:message:) initializer; it knows nothing of it! The only way the runtime knows to instantiate a view controller from a storyboard is by calling init(coder:). But we can call init(coder:message:) when we instantiate the view controller from the storyboard.

Suppose this is the storyboard's initial view controller, and we're calling it in the scene delegate at launch time:

func scene(_ scene: UIScene, 
    willConnectTo session: UISceneSession, 
    options connectionOptions: UIScene.ConnectionOptions) {
        guard let scene = (scene as? UIWindowScene) else { return }
        let message = "Howdy, world!" // or whatever
        self.window = UIWindow(windowScene: scene)
        let sb = UIStoryboard(name: "Main", bundle: nil)
        self.window?.rootViewController = sb.instantiateInitialViewController {
            return ViewController(coder: $0, message: message)
        }
        self.window?.makeKeyAndVisible()
}

We call instantiateViewController(identifier:creator:) and the creator: function calls init(coder:message:) — which, in turn, calls init(coder:). Thus our use of the creator: function is legal, and the view appears correctly.

  • Related