Every Swift developer runs into this error at some point.
Class 'ViewController' has no initializers
To resolve this error, you need to understand an important aspect of classes. Swift requires that the stored properties of a class receive an initial value during initialization. That includes the stored properties the class inherits from its superclass.
Swift requires that the stored properties of a class receive an initial value during initialization.
Take a look at this example. We define a class with name ViewController
, a UIViewController
subclass. The ViewController
class declares a stored property, note
, of type Note
. The note
property is a constant and it doesn't have an initial value.
import UIKit
internal final class ViewController: UIViewController {
// MARK: - Properties
let note: Note
}
With the above in mind, it isn't surprising that the compiler throws an error. The error is a bit vague, though.
The compiler throws an error because the ViewController
class doesn't define an initializer that fully initializes a ViewController
instance. None of the initializers of the ViewController
class assigns a value to the note
property. This violates the above requirement.
We know what the error means and what the underlying problem is. How can we resolve the error? There are several options.
Using Optionals
The simplest option is to declare note
as a variable of type Note?
, an optional. This works fine and it is a pattern I use frequently when I use the Model-View-ViewModel pattern. By declaring note
as a variable of type Note?
, we can create an instance of the ViewController
class and set the note
property after initialization.
You may be wondering why this solution doesn't violate the above requirement. Swift automatically assigns nil
to any variable property that has an optional type. That is why the error disappears if we define note
as an optional.
import UIKit
internal final class ViewController: UIViewController {
// MARK: - Properties
var note: Note?
}
This is how we would set the note
property of a ViewController
instance. First, we initialize a ViewController
instance. Second, we assign a Note
object to the note
property of the ViewController
instance.
// Initialize View Controller
let viewController = ViewController()
// Set Note
viewController.note = Note()
This is also known as property injection, a form of dependency injection. We inject the Note
object into the ViewController
instance. Property injection is a useful and common pattern in software development.
Initializer Injection
There are three downsides to the previous solution. The note
property of the ViewController
class:
- has an optional type
- cannot be declared privately
- cannot be declared as a constant
We can use initializer injection to resolve these problems. Initializer injection is another form of dependency injection. It may sound complex, but it really isn't. It simply means that we pass the value for the note
property to the initializer of the ViewController
class. Take a look at this example.
import UIKit
internal final class ViewController: UIViewController {
// MARK: - Properties
private let note: Note
// MARK: - Initialization
init(note: Note) {
self.note = note
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("Use `init(note:)` to initialize a `ViewController` instance.")
}
}
While this is quite a bit more code, we have addressed the downsides of the previous solution. The note
property is a constant, private property and it no longer has an optional type.
Because init(coder:)
is a required initializer of the UIViewController
class, we also need to implement it in the ViewController
class. We have no other option but to throw a fatal error since it should not be used to initialize a ViewController
instance. Why is that? The reason simple. It doesn't and cannot accept a Note
object, which means we would be back to square one.
This solution seems great, but keep in mind that this only works if you are not using storyboards. I have good news, though. There is a third solution that combines the best of both solutions.
Best of Both Worlds
As of iOS 13 and tvOS 13, it is possible to use initializer injection in combination with storyboards. You can even use this API in combination with segues. The implementation involves two steps.
We first define a designated initializer that accepts an NSCoder
instance. The initializer is similar to the one we encountered earlier. Let's update the implementation of the ViewController
class.
import UIKit
internal final class ViewController: UIViewController {
// MARK: - Properties
private let note: Note
// MARK: - Initialization
init?(coder: NSCoder, note: Note) {
self.note = note
super.init(coder: coder)
}
required init?(coder: NSCoder) {
fatalError("Use `init(note:)` to initialize a `ViewController` instance.")
}
}
Notice that the init(coder:note:)
initializer defines two parameters, coder
of type NSCoder
and note
of type Note
. In the initializer, the vale of the note
parameter is assigned to the note
property and the init(coder:)
initializer of the superclass, UIViewController
, is invoked.
With the init(coder:note:)
initializer in place, we can put it to use. We load the storyboard and invoke the instantiateViewController(identifier:creator:)
instance method. This method accepts the storyboard identifier of the view controller as its first argument and a closure as its second argument. The magic happens in the closure. The closure accepts an NSCoder
instance and returns an object of type UIViewController?
. The implementation isn't difficult as you can see below.
// Create Note
let note = Note()
// Load Storyboard
let storyboard = UIStoryboard(name: "Main", bundle: .main)
// Initialize View Controller
let viewController = storyboard.instantiateViewController(identifier: "ViewController") { (coder) -> UIViewController? in
ViewController(coder: coder, note: note)
}
What's Next?
The next time you encounter this error, you should be able to resolve it without a problem. You now understand what the error means and why the compiler throws it.