121

Apologies if this has been asked before, I've searched around a lot and many answers are from earlier Swift betas when things were different. I can't seem to find a definitive answer.

I want to subclass UIViewController and have a custom initializer to allow me to set it up in code easily. I'm having trouble doing this in Swift.

I want an init() function that I can use to pass a specific NSURL I'll then use with the view controller. In my mind it looks something like init(withImageURL: NSURL). If I add that function it then asks me to add the init(coder: NSCoder) function.

I believe this is because it's marked in the superclass with the required keyword? So I have to do it in the subclass? I add it:

required init(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
}

Now what? Is my special initializer considered a convenience one? A designated one? Do I call a super initializer? An initializer from the same class?

How do I add my special initializer onto a UIViewController subclass?

Doug Smith
  • 29,668
  • 57
  • 204
  • 388
  • 2
    Initializers are good for programmatically created viewControllers, but for viewcontrollers created through storyboard [you're out of luck and have to work your way around it](https://stackoverflow.com/a/39400793/5175709) – mfaani Aug 27 '19 at 20:29

5 Answers5

211
class ViewController: UIViewController {
        
    var imageURL: NSURL?
    
    // this is a convenient way to create this view controller without a imageURL
    convenience init() {
        self.init(imageURL: nil)
    }
    
    init(imageURL: NSURL?) {
        self.imageURL = imageURL
        super.init(nibName: nil, bundle: nil)
    }
    
    // if this view controller is loaded from a storyboard, imageURL will be nil
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }
}
mfaani
  • 33,269
  • 19
  • 164
  • 293
linimin
  • 6,239
  • 2
  • 26
  • 31
  • can imageURL be a constant instead? ("let imageURL: NSURL?) – Van Du Tran Feb 12 '15 at 00:18
  • Sure @VanDuTran, we can initiailze a constant in initiailzers. – linimin Feb 12 '15 at 01:40
  • You don't need to call any designated initializers in your new default initializer? – siege_Perilous May 11 '15 at 04:06
  • 2
    not working xCode 7, the required constructor is failing to initialize the class level property, in my case an Int, not NSURL? – Chris Hayes Mar 26 '16 at 23:52
  • How to load this kind of viewController ? – Nikola Lukic Sep 28 '16 at 13:03
  • 1
    @NikolaLukic - We can creates a new instance of the class by calling `ViewController()`, `ViewController(imageURL: url)`, or loading it from a storyboard. – linimin Sep 30 '16 at 01:46
  • Now i get it , i can make manipulation with class instance before i put it in the present view ! Nice... – Nikola Lukic Sep 30 '16 at 09:02
  • I get this error Convenience initializer for 'NamCls' must delegate (with 'self.init') rather than chaining to a superclass initializer (with 'super.init') – Sujay U N Mar 27 '17 at 21:13
  • @SujayUN - A convenience initializer can only call another initializer from the same class. – linimin Mar 28 '17 at 00:27
  • 3
    I had to write mine as `required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") }` otherwise with `super.init(coder: aDecoder)` I was getting the error of **Property `self.someProperty` not initialized at `super.init` call** – mfaani Nov 25 '17 at 00:56
  • @Honey - All stored property must be assigned an initial value before calling `super.init`. In the sample code above, `imageURL` is an optional variable, so it's nil by default. – linimin Nov 25 '17 at 02:32
  • 2
    Not sure how that's related. If you're doing pure code, then you don't need to populate your properties inside the `init(coder: aDecoder)`. The `fatalError` will suffice – mfaani Nov 25 '17 at 03:13
  • @Honey - Yes, that's totally fine. – linimin Nov 25 '17 at 03:19
  • @others This answer is slightly misleading. I put a different answer [here](https://stackoverflow.com/a/58017901/5175709) which addresses the comments I made before. – mfaani Sep 19 '19 at 19:56
50

For those who write UI in code

class Your_ViewController : UIViewController {

    let your_property : String

    init(your_property: String) {
        self.your_property = your_property
        super.init(nibName: nil, bundle: nil)
    }

    override func viewDidLoad() {
        super.viewDidLoad()

    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) is not supported")
    }
}
Sasan Soroush
  • 857
  • 8
  • 16
31

This is very similar to the other answers, but with some explanation. The accepted answer is misleading because its property is optional and doesn't expose the fact that your init?(coder: NSCoder) MUST initialize each and every property and the only solution to that is having a fatalError(). Ultimately you could get away by making your properties optionals, but that doesn't truly answer the OP’s question.

// Think more of a OnlyNibOrProgrammatic_NOTStoryboardViewController
class ViewController: UIViewController {

    let name: String

    override func viewDidLoad() {
        super.viewDidLoad()
    }

    // I don't have a nib. It's all through my code.
    init(name: String) {
        self.name = name

        super.init(nibName: nil, bundle: nil)
    }

    // I have a nib. I'd like to use my nib and also initialze the `name` property
    init(name: String, nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle? ) {
        self.name = name
        super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
    }

    // when you do storyboard.instantiateViewController(withIdentifier: "ViewController")
    // The SYSTEM will never call this!
    // it wants to call the required initializer!

    init?(name: String, coder aDecoder: NSCoder) {
        self.name = "name"
        super.init(coder: aDecoder)
    }

    // when you do storyboard.instantiateViewController(withIdentifier: "ViewController")
    // The SYSTEM WILL call this!
    // because this is its required initializer!
    // but what are you going to do for your `name` property?!
    // are you just going to do `self.name = "default Name" just to make it compile?!
    // Since you can't do anything then it's just best to leave it as `fatalError()`
    required init?(coder aDecoder: NSCoder) {
        fatalError("I WILL NEVER instantiate through storyboard! It's impossible to initialize super.init?(coder aDecoder: NSCoder) with any other parameter")
    }
}

You basically have to ABANDON loading it from storyboard. Why?

Because when you call a viewController storyboard.instantiateViewController(withIdentifier: "viewController") then UIKit will do its thing and call

required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
}

You can never redirect that call to another init method.

Docs on instantiateViewController(withIdentifier:):

Use this method to create a view controller object to present programmatically. Each time you call this method, it creates a new instance of the view controller using the init(coder:) method.

Yet for programmatically created viewController or nib created viewControllers you can redirect that call as shown above.

mfaani
  • 33,269
  • 19
  • 164
  • 293
  • 4
    This isn't strictly true any more, you can "redirect" the call to another init method by using the `instantiateViewController(identifier:creator:)` method that's available in iOS 13+ – Peter Kovacs Aug 19 '21 at 13:38
  • How to use here: https://www.hackingwithswift.com/example-code/uikit/how-to-use-dependency-injection-with-storyboards – alegelos Nov 23 '22 at 17:37
5

Convenience initializers are secondary, supporting initializers for a class. You can define a convenience initializer to call a designated initializer from the same class as the convenience initializer with some of the designated initializer’s parameters set to default values. You can also define a convenience initializer to create an instance of that class for a specific use case or input value type.

They are documented here.

Vatsal Manot
  • 17,695
  • 9
  • 44
  • 80
0

If you need a custom init for a popover for example you can use the following approach:

Create a custom init that uses the super init with nibName and bundle and after that access the view property to force the load of the view hierarchy.

Then in the viewDidLoad function you can configure the views with the parameters passed in the initialization.

import UIKit

struct Player {
    let name: String
    let age: Int
}

class VC: UIViewController {


@IBOutlet weak var playerName: UILabel!

let player: Player

init(player: Player) {
    self.player = player
    super.init(nibName: "VC", bundle: Bundle.main)
    if let view = view, view.isHidden {}
}

override func viewDidLoad() {
    super.viewDidLoad()
    configure()
}

func configure() {
    playerName.text = player.name + "\(player.age)"
}
}

func showPlayerVC() {
    let foo = Player(name: "bar", age: 666)
    let vc = VC(player: foo)
    present(vc, animated: true, completion: nil)
}
rockdaswift
  • 9,613
  • 5
  • 40
  • 46