Skip to main content

Swift Designated Initializers

Introduction

When creating objects in Swift, initializers are special methods that prepare an instance of a class, structure, or enumeration for use. They ensure all properties have initial values and that the object is properly configured before it can be used. In Swift classes, designated initializers play a crucial role in the initialization process.

A designated initializer is a primary initializer that fully initializes all properties introduced by a class and calls an appropriate superclass initializer to continue the initialization process up the superclass chain. Think of it as the "main" initializer that takes full responsibility for initializing an instance of a class.

In this tutorial, we'll explore designated initializers in depth, understand how they work with inheritance, and learn best practices for implementing them in your Swift code.

Basic Concept of Designated Initializers

Every class in Swift must have at least one designated initializer. The designated initializer is responsible for:

  1. Initializing all properties introduced by the class
  2. Calling a designated initializer from its immediate superclass (if any)

Let's start with a simple example:

swift
class Vehicle {
var numberOfWheels: Int

// This is a designated initializer
init(numberOfWheels: Int) {
self.numberOfWheels = numberOfWheels
}
}

// Creating an instance using the designated initializer
let bicycle = Vehicle(numberOfWheels: 2)
print("A bicycle has \(bicycle.numberOfWheels) wheels")

Output:

A bicycle has 2 wheels

In this example, the Vehicle class has a designated initializer that sets the numberOfWheels property. This initializer takes full responsibility for ensuring that the Vehicle instance is properly initialized.

Designated Initializers and Inheritance

When working with class inheritance, designated initializers become even more important. They ensure that the entire class hierarchy is properly initialized. Here's how it works:

swift
class Vehicle {
var numberOfWheels: Int

init(numberOfWheels: Int) {
self.numberOfWheels = numberOfWheels
}
}

class Car: Vehicle {
var brand: String

// This is a designated initializer for Car
init(brand: String) {
self.brand = brand
super.init(numberOfWheels: 4) // Call to superclass's designated initializer
}
}

let myCar = Car(brand: "Tesla")
print("My \(myCar.brand) has \(myCar.numberOfWheels) wheels")

Output:

My Tesla has 4 wheels

Notice the critical order of operations in the Car initializer:

  1. First, it initializes its own properties (brand)
  2. Then, it calls the superclass's designated initializer (super.init(numberOfWheels: 4))

This order is mandatory in Swift. You must initialize all properties of the current class before calling a superclass initializer.

Rules for Designated Initializers

Swift has specific rules regarding designated initializers:

  1. A designated initializer must call a designated initializer from its immediate superclass
  2. A designated initializer must initialize all properties introduced by its class before calling a superclass initializer
  3. A designated initializer must call a superclass initializer before assigning a value to an inherited property

Let's see a more complex example that demonstrates these rules:

swift
class Vehicle {
var numberOfWheels: Int
var color: String

init(numberOfWheels: Int, color: String) {
self.numberOfWheels = numberOfWheels
self.color = color
}
}

class Car: Vehicle {
var brand: String
var model: String

init(brand: String, model: String, color: String) {
// First: Initialize properties introduced by this class
self.brand = brand
self.model = model

// Then: Call superclass's designated initializer
super.init(numberOfWheels: 4, color: color)

// Now we can modify inherited properties if needed
// self.color = color + " metallic" // This would be valid here
}
}

let myCar = Car(brand: "Toyota", model: "Corolla", color: "Blue")
print("\(myCar.color) \(myCar.brand) \(myCar.model) with \(myCar.numberOfWheels) wheels")

Output:

Blue Toyota Corolla with 4 wheels

Multiple Designated Initializers

Classes can have multiple designated initializers. Each designated initializer must still follow all the rules we've discussed. Here's an example:

swift
class Person {
var name: String
var age: Int

// First designated initializer
init(name: String, age: Int) {
self.name = name
self.age = age
}

// Second designated initializer
init(name: String) {
self.name = name
self.age = 0 // Default age
}
}

let adult = Person(name: "John", age: 30)
let baby = Person(name: "Emma")

print("\(adult.name) is \(adult.age) years old")
print("\(baby.name) is \(baby.age) years old")

Output:

John is 30 years old
Emma is 0 years old

Designated Initializers vs. Convenience Initializers

Swift also has another type of initializer called a convenience initializer. It's important to understand the difference:

  • Designated initializers are the primary initializers that fully initialize all properties and call a superclass initializer
  • Convenience initializers are secondary initializers that call another initializer within the same class

Here's a comparison:

swift
class Food {
var name: String
var calories: Int

// Designated initializer
init(name: String, calories: Int) {
self.name = name
self.calories = calories
}

// Convenience initializer
convenience init(name: String) {
self.init(name: name, calories: 100) // Calls the designated initializer
}
}

let pizza = Food(name: "Pizza", calories: 300)
let apple = Food(name: "Apple") // Uses the convenience initializer

print("\(pizza.name): \(pizza.calories) calories")
print("\(apple.name): \(apple.calories) calories")

Output:

Pizza: 300 calories
Apple: 100 calories

Notice how the convenience initializer calls the designated initializer using self.init(...). This is required - convenience initializers must ultimately call a designated initializer of the same class.

Practical Example: Building a Banking System

Let's see a real-world example of designated initializers in a simple banking system:

swift
class BankAccount {
var accountNumber: String
var accountHolder: String
var balance: Double

init(accountNumber: String, accountHolder: String, initialDeposit: Double) {
self.accountNumber = accountNumber
self.accountHolder = accountHolder
self.balance = initialDeposit
}
}

class SavingsAccount: BankAccount {
var interestRate: Double

init(accountNumber: String, accountHolder: String, initialDeposit: Double, interestRate: Double) {
// First initialize properties of this class
self.interestRate = interestRate

// Then call superclass initializer
super.init(accountNumber: accountNumber, accountHolder: accountHolder, initialDeposit: initialDeposit)
}

func calculateInterest() -> Double {
return balance * interestRate
}
}

class CheckingAccount: BankAccount {
var overdraftLimit: Double

init(accountNumber: String, accountHolder: String, initialDeposit: Double, overdraftLimit: Double) {
// First initialize properties of this class
self.overdraftLimit = overdraftLimit

// Then call superclass initializer
super.init(accountNumber: accountNumber, accountHolder: accountHolder, initialDeposit: initialDeposit)
}
}

// Create accounts
let savings = SavingsAccount(accountNumber: "S12345", accountHolder: "Jane Smith", initialDeposit: 1000, interestRate: 0.05)
let checking = CheckingAccount(accountNumber: "C67890", accountHolder: "John Doe", initialDeposit: 500, overdraftLimit: 200)

// Display account information
print("Savings Account: \(savings.accountHolder) has $\(savings.balance) with \(savings.interestRate * 100)% interest")
print("Yearly interest: $\(savings.calculateInterest())")
print("Checking Account: \(checking.accountHolder) has $\(checking.balance) with $\(checking.overdraftLimit) overdraft limit")

Output:

Savings Account: Jane Smith has $1000.0 with 5.0% interest
Yearly interest: $50.0
Checking Account: John Doe has $500.0 with $200.0 overdraft limit

In this example, both SavingsAccount and CheckingAccount have designated initializers that first initialize their own properties and then call the superclass's designated initializer.

Common Mistakes and Best Practices

Common Mistakes to Avoid

  1. Initializing properties in the wrong order: Always initialize all properties of your current class before calling a superclass initializer.

    swift
    // INCORRECT
    init(brand: String) {
    super.init(numberOfWheels: 4)
    self.brand = brand // Error: Property 'self.brand' not initialized at super.init call
    }

    // CORRECT
    init(brand: String) {
    self.brand = brand
    super.init(numberOfWheels: 4)
    }
  2. Modifying inherited properties before calling super.init: You can only assign values to inherited properties after calling the superclass initializer.

    swift
    // INCORRECT
    init(brand: String) {
    self.brand = brand
    self.numberOfWheels = 4 // Error: Cannot assign to property before super.init call
    super.init()
    }

    // CORRECT
    init(brand: String) {
    self.brand = brand
    super.init()
    self.numberOfWheels = 4 // OK after super.init()
    }

Best Practices

  1. Keep initializers focused: Each initializer should have a clear purpose and initialize properties with sensible defaults.

  2. Use default values when appropriate: You can simplify initializers by providing default values for properties.

    swift
    class Vehicle {
    var numberOfWheels: Int
    var color: String = "Black" // Default value

    init(numberOfWheels: Int) {
    self.numberOfWheels = numberOfWheels
    }
    }
  3. Consider using convenience initializers for providing alternate ways to initialize your class while maintaining a single source of truth in your designated initializer.

  4. Document your initializers: Use comments to explain what each initializer does, especially if your class has multiple initializers.

Summary

Designated initializers are a fundamental concept in Swift's initialization process. They ensure that all properties of a class are properly initialized and that the class inheritance chain is correctly set up. Here's what we've learned:

  • Designated initializers are the primary initializers of a class
  • They must initialize all properties introduced by the class
  • They must call a designated initializer from the immediate superclass
  • You must initialize all properties before calling the superclass initializer
  • A class can have multiple designated initializers

By understanding and correctly implementing designated initializers, you ensure that your objects are properly set up and ready to use, creating more stable and predictable code.

Additional Resources

Exercises

  1. Create a Shape class with properties for color and position. Then create subclasses for Circle, Rectangle, and Triangle with appropriate designated initializers.

  2. Design a MediaItem class hierarchy with different types of media (books, movies, songs) that each have their own properties but share common attributes.

  3. Modify the banking example to add a CreditAccount class with properties for creditLimit and interestRate.

  4. Create a class hierarchy for a game with different character types, each with its own designated initializers that properly initialize properties and call superclass initializers.



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)