paint-brush
Swift init(), Once and for Allby@kfamyn
308 reads
308 reads

Swift init(), Once and for All

by Kiryl FaminMarch 21st, 2025
Read on Terminal Reader
Read this story w/o Javascript

Too Long; Didn't Read

This article covers everything essential to Swift initializers: memberwise, designated, convenience, convenience override; required use cases; parameterless UIView() initializer; compiler assistance; failable init, enum init and more.

Company Mentioned

Mention Thumbnail
featured image - Swift init(), Once and for All
Kiryl Famin HackerNoon profile picture
0-item
1-item

Introduction

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.

Table of Contents

Basics

Structures

  • Memberwise initializer
  • Optionals
  • var vs let
  • Retaining a memberwise initializer

Classes

  • Designated initializer
  • Convenience initializer
  • Retaining superclass' convenience initializer
  • Minimizing the number of overrides
  • Compiler assistance
  • required initializer: generics, protocols, Self()final
  • UIView() without parameters

Honorable mentions

  • Failable init
  • Enums

Summary

Basics

Apple’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.

Structures

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.

Memberwise initializer

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:


Generating memberwise initializer in Xcode


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.

Optionals, 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

Retaining a memberwise initializer

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

  • All fields must be populated in an initializer
  • Optional var fields default to nil
  • Structures receive a free memberwise initializer
  • The memberwise initializer disappears if a custom initializer is declared
  • To retain structure’s memberwise initializer, define a custom one in an extension

Classes

Designated initializer

The primary initializer for a class is the designated initializer. It serves two purposes:

  1. Ensures that all fields are populated
  2. If the class is inherited, it calls the superclass initializer
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 breedproperty, 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 { } // ✅

Convenience initializer

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, designated and superclass initializers


Convenience initializers always go horizontal (self.init), and designated initializers go vertical (super.init).

Retaining Superclass’ Convenience Initializer

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


Current initializer hierarchy, only one init is declared explicitly


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) // ✅


Convenience initializers are now regained, but two initializers are declared explicitly


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) // ✅


Convenience initializers are regained, but GuideDog specifies three initializers explicitly


Minimizing the number of overrides

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) // ✅


Two Animal's convenience initializers are regained, Dog's conenience init(age:, name:) is regained and only two GuideDog's initializers are specified explicitly


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.

Intermediate summary

  • designated initializer ensures that all properties are populated and calls super.init().
  • convenience initializer simplifies initialization by calling a designated initializer.
  • 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.

Compiler assistance

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.

Required

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.

Generics

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

Protocols

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
    }
}

Self()

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

Getting rid of 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) { } // ✅
}

Intermediate summary

  • 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 called automatically in subclass initializers.
  • required initializer is needed to guarantee its presence in subclasses for use in generics, protocols, and Self().

UIView()

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().

Honorable mentions

Failable 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
    }
}

Enum

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
    }
}

Final summary

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.

  • designated initializer ensures all fields are populated and calls super.init().

  • convenience initializer simplifies initialization by calling a designated initializer.

  • Convenience initializers always go horizontal (self.init), and designated initializers go vertical (super.init).

  • 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.

  • 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:).

  • 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.

  1. The Swift Programming Language (6) / Initialization
  2. Swift in Depth by Tjeerd in 't Veen
  3. Telegram - @kfamyn