Jun 17, 2026

Swift Initializers Part 2: Classes

Swift Initializers Part 2: Classes

Swift Initializers: Understanding Initializers in Classes

Introduction

In the previous article Swift Initializers: Understanding Initializers in Structs, we explored initializers in structs.

Structs are relatively straightforward because they do not support inheritance.

Classes introduce additional complexity because they participate in inheritance hierarchies. Swift must ensure that both a class and all of its superclasses are fully initialized before an object can be used.

The other important and significant difference between structs and classes is that classes do not provide a memberwise initializer. All classes must include an initializer if any stored property does not have a default value.

In this article, you'll learn how class initializers work, the difference between designated and convenience initializers, initializer inheritance, required initializers, failable initializers, and Swift's two-phase initialization process.


Why Class Initializers Are Different

If you create a class with properties that have default values and you do not provide an initializer, you cannot create a new instance of that class and specify different values for the properties at the time of creation.

class ClassUser {
    var name: String = "Stewart"
    var isPremium: Bool = false
}
let user1 = ClassUser()

If there were a struct, however, you still get a memberwise initializer by default and have all of the different options for initializers.

struct StructUser {
    var name: String = "Stewart"
    var isPremium: Bool = false
}
let user2 = StructUser()
let user3 = StructUser(isPremium: true)
let user4 = StructUser(name: "Emily")
let user5 = StructUser(name: "Aidan", isPremium: true)

Classes with no default property values

All stored properties in the class must be initialized so if there are no default values, these must be addressed in the initializer.

An initializer is a special type of member of a class or struct whose job is to create and prepare a new instance for use.

The init keyword is identifies the initializer and the initializer parameters are the values and types that callers can provide during initialization. So if your properties do not have default values assigned, the initializer must have a parameter that is used in the initializer body assign the provided values to the corresponding object properties.

If the parameter names are the same as the property names, then in the initializer body, self.is used in front of the property name to refer to the property and avoid ambiguity.

class User {
    let name: String
    let isPremium: Bool

    init(name: String, isPremium: Bool) {
        self.name = name
        self.isPremium = isPremium
    }
}

Constant Properties and Initialization

The rules for constant properties in classes are the same as those for constant properties in structs. They must receive values before initialization completes and they cannot be changed afterwards

class ClassUser {
    let name: String
    let isPremium: Bool
    init(name: String, isPremium: Bool) {
        self.name = name
        self.isPremium = isPremium
    }
}
let user = ClassUser(name: "Stewart", isPremium: false)
user.isPremium = true // Error

Initializer Parameter Labels

You can add additional external parameter labels to that are different from the property names. The purpose of external initializer parameter labels is to make code more readable and self-documenting at the point where an instance is created.

class ClassUser {
    var nom: String
    var estPremiume: Bool
    init(name nom: String, isPremium estPremiume: Bool) {
        self.nom = nom
        self.estPremiume = estPremiume
    }
}

let user = ClassUser(name: "Stewart", isPremium: true)

Default Parameter Values

You can provide default parameter values for one or more of your properties in the initializer parameters to make an initializer easier to use by providing sensible defaults for parameters that callers don’t always need to specify.

Each default parameter value will add an additional initializer option upon creation.

class User {
    let name: String
    let isPremium: Bool

    init(name: String, isPremium: Bool = false) {
        self.name = name
        self.isPremium = isPremium
    }
}

let user = User(name: "Stewart")  // isPremium is false
let user2 = User(name: "Stewart", isPremium: true)

Designated Initializers

A designated initializer is the primary initializer of a class. Its responsibility is to ensure that all of the class’s stored properties receive values and that initialization continues up the inheritance chain by calling a designated initializer in the superclass as you will see. It is responsible for fully initializing the class.

class User {
    let name: String
    let isPremium: Bool

    init(name: String, isPremium: Bool) {
        self.name = name
        self.isPremium = isPremium
    }
}

Convenience Initializers

A convenience initializer is an initializer that provides an easier or more specialized way to create an instance, while delegating the actual work of initialization to a designated initializer.

If a designated initializer is the master initializer, a convenience initializer is a shortcut.

class Video {
    let title: String
    let duration: Int
    init(title: String, duration: Int) {
        self.title = title
        self.duration = duration
    }

    convenience init(title: String) {
        self.init(
            title: title,
            duration: 0
        )
    }
}
let video = Video(title: "Understanding Initializers") // Duration = 0
let video2 = Video(title: "Understanding Initializers", duration: 40)

Now you may be wondering how this differs from the example in Default Parameter Values above and this example there is really no difference. Why not just write it like this?

class Video {
    let title: String
    let duration: Int
    init(title: String, duration: Int = 0) {
        self.title = title
        self.duration = duration
    }
}
let video = Video(title: "Understanding Initializers") // Duration = 0
let video2 = Video(title: "Understanding Initializers", duration: 40)

However, you can take advantage of convenience initializers to communicate intent. For example, in the convenience initializer for this class, we can change the parameter label to clarify what this initializer is intended for.

class Video {
    let title: String
    let duration: Int
    init(title: String, duration: Int) {
        self.title = title
        self.duration = duration
    }
    convenience init(draftTitle: String) {
        self.init(
            title: draftTitle,
            duration: 0
        )
    }
}
let video = Video(draftTitle: "Understanding Initializers") // Duration = 0
let video2 = Video(title: "Understanding Initializers", duration: 40)

Designated vs Convenience Initializers

Designated initializers fully initialize the class.

Convenience initializers must call another initializer in the same class using self.init(...).


Class Inheritance

In the article on structs, initialization was relatively straightforward because structs do not support inheritance.

Classes are different.

A class can inherit properties and behavior from another class.

Consider this example:

class Person {
    let name: String
  
    init(name: String) {
        self.name = name
    }
}

Now let’s create a subclass:

class Employee: Person {
    let employeeID: Int

    init(name: String, employeeID: Int) {
        self.employeeID = employeeID
        super.init(name: name)
    }
}

An Employee contains everything that belongs to a Person plus its own additional information.

Person

​ └─ name

Employee

├─ name

└─ employeeID

When an Employee is created, Swift must ensure that:

  1. employeeID receives a value.
  2. name receives a value.
  3. Both the Employee portion and the Person portion are properly initialized.

This is where initialization becomes more complicated than it was for structs.

class Employee: Person {
    let employeeID: Int

    init(name: String, employeeID: Int) {
        self.employeeID = employeeID
    }
}

What value should name have?

The superclass has not been initialized.

Swift prevents situations like this by enforcing a set of initialization rules.

Building an Object from the Bottom Up

The subclass initializes its own stored properties first:

self.employeeID = employeeID

Then it asks the superclass to initialize its properties

super.init(name: name)

Only when every class in the hierarchy has finished initialization is the object considered ready to use.

Without these rules, Swift could allow partially initialized objects to exist

let employee = Employee(...)

might contain a valid employeeID but an uninitialized name.

Swift’s initialization system prevents that from ever happening.

This focus on safety is the reason Swift introduces designated initializers, convenience initializers, and the delegation rules that we’ll examine next.

Understanding Swift's Three Delegation Rules

When Swift introduced designated and convenience initializers, it also introduced three rules that govern how they interact.

Rule 1

A Designated Initializer Must Call a Designated Initializer from Its Immediate Superclass
class Person {
    let name: String

    init(name: String) {
        self.name = name
    }
}

class Employee: Person {
    let employeeID: Int

    init(name: String, employeeID: Int) {
        self.employeeID = employeeID
        super.init(name: name) // This calls the Person designated initializer
    }
}
  • Subclass properties must be initialized before calling super.init().
  • Designated initializers move up the inheritance chain.

Rule 2

A Convenience Initializer Must Call Another Initializer in the Same Class
class User {
    let name: String
    let isPremium: Bool

    init(name: String, isPremium: Bool) {
        self.name = name
        self.isPremium = isPremium
    }

    convenience init(name: String) {
        self.init(name: name, isPremium: false) // this calls the User initializer
    }
}
  • Convenience initializers move across the same class.

Rule 3

A Convenience Initializer Must Ultimately End Up Calling a Designated Initializer

Every initialization path must eventually reach a designated initializer.

Remember
Convenience → Across
Designated → Up

Overriding Initializers

A subclass can override an initializer of the superclass.

Overriding an initializer allows a subclass to customize how an inherited initialization process works.

In this example, the Employee class overrides the initializer inherited from Person.

Notice the use of the override keyword. This tells Swift that we’re replacing the implementation provided by the superclass.

The override initializer in the Employee class first initializes the employeeID, giving every user a unique ID during initialization and only then can the initializer delegate to the superclass

class Person {
    let name: String

    init(name: String) {
        self.name = name
    }
}

class Employee: Person {
    let employeeID: Int

    override init(name: String) {
        self.employeeID = Int.random(in: 1000...9999)
        super.init(name: name)
    }
}

Swift follows these steps.

Employee.init(name:)

​ │

​ ▼

employeeID initialized

​ │

​ ▼

Person.init(name:)

​ │

​ ▼

name initialized

​ │

​ ▼

Object ready to use


Required Initializers

Sometimes a superclass needs to ensure that every subclass provides a particular initializer.

The purpose of required is not to force a subclass to call super.init(name:).

The purpose of required is to force a subclass to provide an initializer with the same signature.

Consider this class

class Person {
    let name: String

    required init(name: String) {
        self.name = name
    }
}

Now consider this subclass

class Employee: Person {
    let employeeID: Int
    init(name: String,employeeID: Int) {
        self.employeeID = employeeID
        super.init(name: name)
    }
}

This fails because the required superclass init must be provided by the Employee subclass.

class Employee: Person {
    let employeeID: Int
    required init(name: String) {
           self.employeeID = 0
           super.init(name: name)
       }
    init(name: String,employeeID: Int) {
        self.employeeID = employeeID
        super.init(name: name)
    }
}

Failable and Throwing Initializers

Failable and throwing initializers work exactly the same in classes as they do in structs. The primary difference is that subclasses must also account for failures or errors that may occur when calling superclass initializers. If a superclass initializer can fail or throw, subclasses must handle that behavior as part of their own initialization process.

class Person {
    let name: String
    init?(name: String) {
        guard !name.isEmpty else {
            return nil
        }
        self.name = name
    }
}

class Employee: Person {
    let employeeID: Int
    init?(
        name: String,
        employeeID: Int
    ) {
        self.employeeID = employeeID
        super.init(name: name)
    }
}

let employee = Employee(name: "",employeeID: 1001) // nil

In this example, super.init(name: name) fails because name is empty. Since the superclass initializer is failable, the subclass initializer must also be failable


Final Example

Here is an example of a Course class with a VideoCourse subclass

The Course has a throwing initializer as well as a convenience initializer.

Similarly, VideoCourse has a throwing initializer and a convenience initializer o its own.

enum CourseError: Error {
    case emptyTitle
    case invalidDuration
}

class Course {
    let title: String
    let lessonCount: Int
    let isPublished: Bool

    init(
        title: String,
        lessonCount: Int,
        isPublished: Bool
    ) throws {
        guard !title.isEmpty else {
            throw CourseError.emptyTitle
        }
        self.title = title
        self.lessonCount = lessonCount
        self.isPublished = isPublished
    }

    convenience init(title: String) throws {
        try self.init(
            title: title,
            lessonCount: 1,
            isPublished: false
        )
    }

}
class VideoCourse: Course {
    let durationInMinutes: Int

    init(
        title: String,
        lessonCount: Int,
        isPublished: Bool,
        durationInMinutes: Int
    ) throws {
        guard durationInMinutes > 0 else {
            throw CourseError.invalidDuration
        }
        self.durationInMinutes = durationInMinutes
        try super.init(
            title: title,
            lessonCount: lessonCount,
            isPublished: isPublished
        )
    }

    convenience init(title: String) throws {
        try self.init(
            title: title,
            lessonCount: 1,
            isPublished: false,
            durationInMinutes: 60
        )
    }
}

// Uses the videoCourse initializer
let swiftUICourse = try VideoCourse(
    title: "Mastering SwiftUI",
    lessonCount: 42,
    isPublished: true,
    durationInMinutes: 540
)

// Uses the VideoCourse convenience initializer
let draftCourse = try VideoCourse(
    title: "Introduction to Swift"
)

// Fails the validation in VideoCourse where the duration must be > 0
let course = try VideoCourse(
    title: "SwiftUI",
    lessonCount: 20,
    isPublished: true,
    durationInMinutes: 0
)

// Fails the validation where a Course title cannot be empty
let course2 = try VideoCourse(
    title: "",
    lessonCount: 20,
    isPublished: true,
    durationInMinutes: 10
)

Conclusion

Class initialization builds on the concepts you learned with structs but adds inheritance, designated initializers, convenience initializers, required initializers, and two-phase initialization.

Remember:

Convenience goes across. Designated goes up.