Hello! My name is Kiryl Famin, and I am an iOS developer.
Today, I want to thoroughly examine such a simple topic as initializers in Swift. Despite its apparent simplicity, sometimes the lack of a complete understanding of this topic leads to frustrating errors that one wants to fix quickly without delving into the details.
In this article, we will cover everything related to initializers, including:
How to retain structure’s memberwise initializer while defining a custom one
Why it is not always necessary to write an initializer in classes
Why calling super.init
is not always required in a designated initializer
Why all fields of a subclass must be populated before calling super.init
How to access all parent initializers with minimal overrides in subclasses
When exactly a required
initializer is needed
Why UIView.init()
is always called without parameters, but init(frame:)
and init(coder:)
are overridden
...and more. But let’s take it step by step.
var
vs let
required
initializer: generics, protocols, Self()
, final
UIView()
without parametersApple’s guide The Swift Programming Language (6) (which, by the way, is surprisingly detailed for initializers) states:
Initialization is the process of preparing an instance of a class, structure, or enumeration for use. This process involves setting an initial value for each stored property on that instance and performing any other setup or initialization that’s required before the new instance is ready for use.
You implement this initialization process by defining initializers, which are like special methods that can be called to create a new instance of a particular type. Unlike Objective-C initializers, Swift initializers don’t return a value. Their primary role is to ensure that new instances of a type are correctly initialized before they’re used for the first time.
Well, i guess i don’t have to add anything here.
Let’s start by discussing structure initializers. This is quite simple since there is no inheritance, but there are still some rules you must know about.
Let’s write a simple structure:
struct BankAccount {
let amount: Double
let isBlocked: Bool
}
let bankAccount = BankAccount(amount: 735, isBlocked: Bool)
Notice that we were able to initialize the structure without explicitly declaring an initializer. This happens because structures receive a memberwise initializer generated by the compiler. This works only for structures.
By selecting Refactor → Generate memberwise initializer, you can see how it looks:
init(amount: Double, isBlocked: Bool) {
self.amount = amount
self.isBlocked = isBlocked
}
From the signature, it’s easy to see that failing to provide values for all parameters will result in a compilation error:
let bankAccount = BankAccount(amount: 735) // ❌ Missing argument for parameter 'isBlocked' in call
However, if you want to reduce the number of required arguments, you can define a custom initializer:
init(amount: Double, isBlocked: Bool = false) {
self.amount = amount
isBlocked = isBlocked
}
let bankAccount = BankAccount(amount: 735) // ✅
Note that if isBlocked
is not populated, this will result in a compilation error because all structure properties must be populated in an initializer.
var
vs let
The only case where a field does not need to be populated explicitly is when it is an optional (?
) variable (var
). In such cases, the field will default to nil
:
struct BankAccount {
let amount: Double
var isBlocked: Bool?
init(amount: Double) {
self.amount = amount
}
}
let bankAccount = BankAccount(amount: 735) // ✅
However, if we try to use the memberwise initializer in this case, we will get a compilation error:
let bankAccount = BankAccount(
amount: 735,
isBlocked: false
) // ❌ Extra argument 'isBlocked' in call
This happens because declaring a custom initializer removes the memberwise initializer. It is still possible to define it explicitly, but it will not be available automatically.
However, there is a small trick to retain the memberwise initializer: declare the custom initializer in an extension
.
struct BankAccount {
let amount: Double
var isBlocked: Bool?
}
extension BankAccount {
init(amount: Double) {
self.amount = amount
}
}
let barclaysBankAccount = BankAccount(amount: 735) // ✅
let revolutBankAccount = BankAccount(amount: 812, isBlocked: false) // ✅
print(barclaysBankAccount.isBlocked) // nil
print(barclaysBankAccount.isBlocked) // false
Summary for structures
var
fields default to nil
extension
The primary initializer for a class is the designated initializer. It serves two purposes:
class Animal {
var name: String
init(name: String) {
self.name = name
}
}
class Dog: Animal {
var breed: String
var name: String
init(breed: String, name: String) {
self.breed = breed
super.init(name: name)
}
}
All fields must be populated before calling super.init
. This is because the superclass initializer may call methods overridden by the subclass, which could access unpopulated subclass properties.
class Animal {
var age: Int
init(age: Int) {
self.age = age
getInfo()
}
func getInfo() {
print("Age: ", age)
}
}
class Dog: Animal {
var breed: String
init(breed: String, age: Int) {
self.breed = breed // imagine we haven't done this
super.init(age: age)
}
override func getInfo() {
print("Age: ", age, ", breed: ", breed)
}
}
Thus, if we had not set self.breed = breed
, we would have encountered a runtime error because the Animal
initializer would have called the overridden getInfo()
method from the Dog
class. This method attempts to access the breed
property, which would not yet be populated.
Unlike structures, classes do not receive an implicit memberwise initializer. If there are uninitialized properties, a compilation error occurs:
class Animal { // ❌ Class 'Animal' has no initializers
var age: Int
}
class Animal { // ✅
var age: Int = 0
}
class Animal { // ✅
var age: Int?
}
class Animal { } // ✅
Classes can also have a convenience initializer. Unlike designated initializers, they do not create an object from scratch but simplify the initialization process by reusing the logic of other initializers.
class Rectangle {
var width: Double
var height: Double
init(width: Double, height: Double) {
self.width = width
self.height = height
}
convenience init(side: Double) {
self.init(width: side, height: side) // uses a designated initializer of self
}
}
Convenience initializers can call either designated initializers or other convenience initializers. Ultimately, a designated initializer will always be called.
Convenience initializers always go horizontal (self.init), and designated initializers go vertical (super.init).
As soon as a subclass declares new properties, it loses access to all of the superclass’s convenience initializers.
class Animal {
var age: Int
var name: String
init(age: Int, name: String) {
self.age = age
self.name = name
}
convenience init(age: Int) {
self.init(age: age, name: "Default")
}
convenience init(name: String) {
self.init(age: 0, name: name)
}
}
class Dog: Animal {
var breed: String
init(age: Int, name: String, breed: String) {
self.breed = breed
super.init(age: age, name: name)
}
}
let dog = Dog(age: 3) // ❌ Missing arguments for parameters 'breed', 'name' in call
This can be fixed by overriding all of the superclass’s designated initializers.
class Dog: Animal {
// ...
override init(age: Int, name: String) {
self.breed = "Mixed"
super.init(age: age, name: name)
}
}
let dog = Dog(age: 3) // ✅
It is easy to see that, in this way, to use a convenience initializer in the next subclass, you need to override two initializers.
class GuideDog: Dog {
var isTrained: Bool
override init(age: Int, name: String) {
self.isTrained = false
super.init(age: age, name: name)
}
override init(age: Int, name: String, breed: String) {
self.isTrained = false
super.init(age: age, name: name, breed: breed)
}
init(age: Int, name: String, breed: String, isTrained: Bool) {
self.isTrained = isTrained
super.init(age: age, name: name, breed: breed)
}
}
let dog = GuideDog(age: 3) // ✅
However, this can be avoided by using a convenience override initializer.
class Dog: Animal {
var breed: String
convenience override init(age: Int, name: String) {
self.init(age: age, name: name, breed: "Mixed") // self, not super
}
init(age: Int, name: String, breed: String) {
self.breed = breed
super.init(age: age, name: name)
}
}
class GuideDog: Dog {
var isTrained: Bool
// override init(age: Int, name: String) {
// self.isTrained = false
//
// super.init(age: age, name: name, breed: "Mixed")
// }
convenience override init(age: Int, name: String, breed: String) {
self.init(age: age, name: name, breed: breed, isTrained: false) // self, not super
}
init(age: Int, name: String, breed: String, isTrained: Bool) {
self.isTrained = isTrained
super.init(age: age, name: name, breed: breed)
}
}
let dog = GuideDog(age: 3) // ✅
Now we have only 2 explicitly specified initializers in each subclass.
Notice how convenience override initializers call self
designated init instead of super.init
.
This trick is thoroughly explained in Chapter 5 of Swift in Depth by Tjeerd in 't Veen, a book I highly recommend.
super.init()
.We have already discussed that if a subclass does not introduce new parameters, it automatically inherits all of the superclass’s initializers.
class Base {
let value: Int
init() {
value = 0
}
init(value: Int) {
self.value = value
}
}
class Subclass: Base { }
let subclass = Subclass() // ✅
let subclass = Subclass(value: 3) // ✅
However, there is another important point: if the superclass has only one designated initializer and it is parameterless (init()
without arguments), then explicitly declared initializers in the subclass do not need to call super.init()
. In this case, the Swift compiler automatically inserts the call to the available super.init()
without arguments.
class Base {
init() { }
}
class Subclass: Base {
let secondValue: Int
init(secondValue: Int) {
self.secondValue = secondValue // ✅ without explicit super.init()
}
}
The code compiles because super.init()
is implicitly called. This is crucial for some of the following examples.
A required
initializer is used in all cases where a subclass must have the same initializer as the base class. It must also call super.init()
. Below are examples where a required
initializer is necessary.
Calling init
on a generic type is only possible by declaring it as a required init
.
class Base { }
class Subclass: Base { }
struct Factory<T: Base> {
func initInstance() -> T { // ❌ Constructing an object of class
T() // type 'T' with a metatype value
} // must use a 'required' initializer
}
This code does not compile because Factory
does not know anything about the subclasses of Base
. Although in this particular case, Subclass
does have an init()
without parameters, imagine if it introduced a new field:
class Subclass: Base {
let value: Int
init(value: Int) {
self.value = value
}
}
Here, it no longer has an empty init
, so it must be declared as required
.
class Base {
required init() { }
}
class Subclass: Base { }
struct Factory<T: Base> {
static func initInstance() -> T { // ✅
T()
}
}
let subclass = Factory<Subclass>.initInstance()
Notice that even though we did not explicitly declare required init
in Subclass
, the compiler generated it for us. This was discussed in Compiler Assistance. The required init
was automatically inherited and called super.init()
.
class Subclass: Base {
required init() {
super.init()
}
}
All initializers declared in protocols must be required
:
protocol Initable {
init()
}
class InitableObject: Initable {
init() { // ❌ Initializer requirement 'init()' can only
// be satisfied by a 'required' initializer
} // in non-final class 'InitableObject'
}
Again, this is necessary so that the compiler ensures that the subclass implements the protocol initializer. As we already know, this does not always happen—if init
is not required
, the subclass is not obligated to override it and may define its own initializer.
class IntValue: InitableObject {
let value: Int
init(value: Int) {
self.value = value
}
}
let InitableType: Initable.Type = IntValue.self
let initable: Initable = InitableType.init()
Of course, the following code will not compile because Base.init()
is not required
.
class InitableObject: Initable {
required init() { } // ✅
}
class IntValue: InitableObject {
let value: Int
required init() {
self.value = 0
}
init(value: Int) {
self.value = value
}
}
A similar situation occurs when calling the Self()
initializer in static methods.
class Base {
let value: Int
init(value: Int) {
self.value = value
}
static func instantiate() -> Self {
Self(value: 3) // ❌ Constructing an object of class type 'Self'
} // with a metatype value must use a 'required' initializer
}
As always, the issue lies in inheritance:
class Subclass: BaseClass {
let anotherValue: Int
init(anotherValue: Int) {
self.anotherValue = anotherValue
}
}
let subclass = Subclass.instantiate() // ❌
class BaseClass {
let value: Int
required init(value: Int) { // ✅
self.value = value
}
static func instantiate() -> Self {
Self(value: 3)
}
}
required
: final
Since the purpose of required
is to enforce the implementation of an initializer in subclasses, naturally, prohibiting inheritance using the final
keyword removes the need to mark an initializer as required
.
protocol Initable {
init()
}
final class InitableObject: Initable { } // ✅
protocol ValueInitable {
init(value: Int)
}
final class ValueInitableObject: ValueInitable {
init(value: Int) { } // ✅
}
init()
without parameters, it is called automatically in subclass initializers.required
initializer is needed to guarantee its presence in subclasses for use in generics, protocols, and Self()
.A brief mention of the UIView()
initializer without parameters, which cannot be found in the documentation but is mysteriously used everywhere.
The reason is that UIView
inherits from NSObject
, which has an init()
without parameters. Therefore, this initializer is not explicitly declared in the UIView
interface, yet it is still available:
@available(iOS 2.0, *)
@MainActor open class UIView : UIResponder, NSCoding, UIAppearance, UIAppearanceContainer, UIDynamicItem, UITraitEnvironment, UICoordinateSpace, UIFocusItem, UIFocusItemContainer, CALayerDelegate {
open class var layerClass: AnyClass { get }
public init(frame: CGRect)
public init?(coder: NSCoder)
open var isUserInteractionEnabled: Bool
// no init()
However, under the hood, this initializer calls init(frame:)
when initialized in code or init(coder:)
when initialized via Interface Builder. This happens because UIView
provides its own implementation of NSObject.init()
, which can be confirmed by the fact that method_getImplementation
returns different addresses for NSObject.init()
and UIView.init()
.
A failable init is just one that returns an optional
final class Failable {
let positiveValue: Int
init?(value: Int) {
guard value > 0 else { return nil }
positiveValue = value
}
}
Enums with a raw value get a free init?(rawValue:)
enum Direction: String {
case north
case west
case south
case east
}
let north = Direction(rawValue: "north")
You can also create a custom init for enums. All enum inits must assign self
.
enum DeviceType {
case phone
case tablet
init(screenWidth: Int) {
self = screenWidth > 800 ? .tablet : .phone
}
}
We have covered all the essential aspects of initializers in Swift:
In an initializer, all fields must be populated.
Optional var
properties default to nil
.
Structures receive a free memberwise initializer.
The memberwise initializer disappears when a custom initializer is defined.
A designated initializer ensures all fields are populated and calls super.init()
.
A convenience initializer simplifies initialization by calling a designated initializer.
Convenience initializers always go horizontal (self.init
), and designated initializers go vertical (super.init
).
A convenience initializer becomes unavailable to subclasses if they declare new properties.
To restore a superclass’s convenience initializer, all of its designated initializers must be overridden.
To minimize the number of overrides, a convenience override initializer can be used.
If a subclass does not introduce new parameters, it automatically inherits all initializers from its superclass.
If the superclass only has init()
without parameters, it is automatically called in subclass initializers.
A required initializer ensures its presence in subclasses for use in generics, protocols, and Self()
.
UIView.init()
calls either UIView.init(frame:)
or UIView.init(coder:)
.
A failable initializer returns an optional.
Enums with a raw value get a free init?(rawValue:)
.
All enum initializers must assign self
.
I hope you found something useful in this article. If anything remains unclear or you find inaccuracy, feel free to contact me for a free explanation on Telegram: @kfamyn.