avatarCong Le

Summary

This text provides an in-depth explanation of various types of initializers in Swift, including designated, convenience, failable, and required initializers, as well as two-phase initialization and deinitialization.

Abstract

In the context of iOS development with Swift, initializers are crucial for creating instances of classes, structs, or enums. This text delves into the different types of initializers, starting with the primary designated initializer that ensures a class's storage is fully initialized. Convenience initializers provide additional convenience and simplify the initialization process with default values. Failable initializers can return nil after initialization, indicating a failure in the initialization process. Implicitly unwrapped failable initializers return an implicitly unwrapped optional instance of a class, assuming the initialization will not fail after successful performance. Required initializers must be implemented by every subclass of a class, ensuring that certain initialization logic is always performed. The text also explains the two-phase initialization process and deinitialization, which deallocates an instance of a class when it is no longer needed.

Opinions

  • Designated initializers are the primary initializers for a class, fully initializing all properties introduced by the class and calling an appropriate superclass initializer if the class is a subclass.
  • Convenience initializers are secondary initializers that must call another initializer of the same class, often a designated initializer, and are used to provide additional convenience or simplify the initialization process with default values.
  • Failable initializers are useful when there is a possibility that the initialization cannot succeed with the provided parameters.
  • Implicitly unwrapped failable initializers should be used cautiously since they assume the initialization will not fail after it's been successfully performed once.
  • Required initializers are used when a class part of a library needs to ensure that certain initialization logic is always performed.
  • The two-phase initialization process in Swift ensures that all stored properties of an instance receive an initial value before the instance is considered fully initialized.
  • Deinitializers are used to perform any manual cleanup, such as closing file handles or freeing up any other manually allocated resources before the instance is destroyed.

Types of Initializers in Swift

Mastering Designated, Convenience, Failable, and Required Initializers for Robust iOS App Development

In iOS development with Swift, initializers are special methods used to create instances of a class, struct, or enum. Here’s an explanation of each type of initializer

1. Designated Initializer

  • The primary initializer for a class.
  • It must fully initialize all properties introduced by the class and call an appropriate superclass initializer if the class is a subclass.
  • Designated initializers ensure that the class’s storage is fully initialized.
  • Every class must have at least one designated initializer, which forms the basis for the initialization process of that class.
  • By default, initializers in Swift are designated initializers unless they are explicitly marked as convenience initializers.

Example:

class SomeClass {
    var name: String
    var age: Int
    
    init(name: String, age: Int) { // Designated initializer
        self.name = name
        self.age = age
    }
}

2. Convenience Initializer

  • A secondary initializer must call another initializer (often a designated initializer) of the same class.
  • It is used to provide additional convenience or to simplify the initialization process with default values.
  • Convenience initializers are prefixed with the convenience keyword.
  • They are often used to create “shortcut” ways to initialize an object when the full flexibility of the designated initializer is not required.

Example:

class SomeClass {
    var name: String
    var age: Int
    
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
    
    convenience init(name: String) {
        self.init(name: name, age: 0) // Default age is 0
    }
}

3. Failable Initializer

  • An initializer that can return nil after initialization, indicating that the initialization process has failed.
  • Failable initializers are indicated by the init? syntax.
  • They are useful when there is a possibility that the initialization cannot succeed with the provided parameters.

Example:

struct User {
    var username: String
    
    init?(username: String) {
        guard !username.isEmpty else { return nil }
        self.username = username
    }
}

let user = User(username: "") // user is nil because the initialization failed

4. Implicitly Unwrapped Failable Initializer

  • A failable initializer marked with init! instead of init?, which returns an implicitly unwrapped optional instance of the class.
  • This initializer allows the instance to be used without optional unwrapping once initialized, but it still can return nil during initialization if certain conditions are not met.
  • It should be used cautiously since it assumes the initialization will not fail after it’s been successfully performed once.

Example:

class SomeClass {
    var name: String
    
    init!(name: String) {
        if name.isEmpty {
            return nil
        }
        self.name = name
    }
}

let instance: SomeClass = SomeClass(name: "") // This can be nil, but if not, it's used without needing to unwrap.

5. Required Initializer

  • An initializer that every subclass of the class must implement.
  • The required modifier ensures that subclasses provide an implementation of this initializer.
  • Required initializers are used when a class part of a library needs to ensure that certain initialization logic is always performed.

Example:

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

class Subclass: SomeClass {
    var extraProperty: String
    
    required init(name: String) {
        self.extraProperty = "Extra"
        super.init(name: name)
    }
}

6. Two-Phase Initialization

  • A safety-check process that Swift uses during class initialization to ensure that all stored properties of an instance receive an initial value before the instance is considered fully initialized.
  • The first phase initializes all stored properties within a class and its superclass chain.
  • The second phase gives each class a chance to customize its stored properties further after the superclass’s initializers have been called.
  • This process prevents property values from being accessed before they are initialized and prevents them from being set to a different value by another initializer unexpectedly.

Example:

class SomeClass {
    var name: String
    var age: Int
    
    init(name: String, age: Int) {
        self.name = name // Phase 1: Initial value is set
        // Superclass's initializer would be called here
        // Phase 2: Customization is possible after superclass initialization
        if age < 0 {
            self.age = 0
        } else {
            self.age = age
        }
    }
}

7. Deinitialization

  • The process of deallocating an instance of a class when it is no longer needed.
  • Deinitializers are written using the deinit keyword and do not take any parameters or return any values.
  • They are called automatically, just before an instance’s memory is deallocated.
  • Deinitializers are used to perform any manual cleanup, such as closing file handles or freeing up any other manually allocated resources before the instance is destroyed.

Example:

class SomeClass {
    var fileHandle: FileHandle?
    
    init() {
        let fileURL = ... // URL to a file
        do {
            fileHandle = try FileHandle(forReadingFrom: fileURL)
        } catch {
            print("Error opening file")
        }
    }
    
    deinit {
        fileHandle?.closeFile() // Cleanup before deinitialization
    }
}

// When an instance of SomeClass is no longer referenced, its deinitializer is called, and fileHandle is closed.

A comprehensive code example

Below is a comprehensive example that demonstrates all types of initializers and deinitialization in a single Swift class:

class Product {
    var name: String
    var price: Double
    var description: String?

    // Designated Initializer
    init(name: String, price: Double, description: String?) {
        self.name = name
        self.price = price
        self.description = description
    }

    // Convenience Initializer
    convenience init(name: String) {
        self.init(name: name, price: 0.0, description: nil)
    }

    // Failable Initializer
    init?(name: String, price: Double) {
        guard price >= 0 else { return nil }
        self.name = name
        self.price = price
        self.description = nil
    }

    // Implicitly Unwrapped Failable Initializer
    init!(name: String, price: Double, description: String) {
        guard !name.isEmpty else { return nil }
        self.name = name
        self.price = price
        self.description = description
    }

    // Two-Phase Initialization
    // Phase 1: Setting initial values
    init(rawData: [String: Any]) {
        self.name = rawData["name"] as? String ?? "Unknown"
        self.price = rawData["price"] as? Double ?? 0.0
        // Superclass's initializer would be called here if there was one
        // Phase 2: Further customization
        if let description = rawData["description"] as? String, !description.isEmpty {
            self.description = description
        }
    }

    // Deinitialization
    deinit {
        print("Deinitializing product named: \(name)")
    }
}

// Required Initializer in a subclass
class DiscountedProduct: Product {
    let discountPercentage: Double

    required init(discountPercentage: Double) {
        self.discountPercentage = discountPercentage
        super.init(name: "Discounted", price: 0.0, description: "A product with a discount")
    }

    // Override the designated initializer
    override init(name: String, price: Double, description: String?) {
        self.discountPercentage = 10 // Default discount
        super.init(name: name, price: price, description: description)
    }
}

// Usage
let product = Product(name: "Widget", price: 19.99, description: "A fancy widget")
let simpleProduct = Product(name: "Simple Widget") // Convenience
let noPriceProduct = Product(name: "No Price Widget", price: -10.0) // Failable, returns nil
let specialProduct = Product(name: "", price: 29.99, description: "Special") // Implicitly unwrapped failable, returns nil
let productFromRawData = Product(rawData: ["name": "Raw Widget", "price": 15.99]) // Two-Phase Initialization
let discountedProduct = DiscountedProduct(name: "Cheap Widget", price: 9.99, description: "A cheaper widget") // Required Initializer

In this example, Product is a base class that includes a designated initializer, a convenience initializer, a failable initializer, an implicitly unwrapped failable initializer, and an initializer that demonstrates two-phase initialization. The DiscountedProduct subclass includes a required initializer. Additionally, the deinit method is implemented to demonstrate deinitialization.

UML diagram illustration

Here’s how you might illustrate the various types of initializers using a UML class diagram:

UML diagram of initializers
  • Each initializer is represented as a method within the class compartment.
  • The designated initializer is marked with a # symbol, indicating it's a protected method (which is the closest visibility level in UML to indicate it's meant for internal use within the class and its subclasses).
  • The convenience initializer is marked with a + symbol and the word convenience to indicate it's a secondary, publicly accessible initializer.
  • The failable initializers are marked with a + symbol and a ? after the init keyword.
  • The implicitly unwrapped failable initializer is marked with a + symbol and a ! after the init keyword.
  • The two-phase initialization is represented as a regular initializer but can be annotated or described to indicate its two-phase nature.
  • The deinitialization method is indicated by the deinit keyword.

The required initializer in the subclass DiscountedProduct is marked with the keyword required, and the overridden designated initializer is indicated with the override keyword.

Please note that UML diagrams do not have built-in syntax to represent Swift-specific features like failable initializers or two-phase initialization, so these are represented using naming conventions and annotations for clarity.

Stackademic 🎓

Thank you for reading until the end. Before you go:

Swift
iOS App Development
Initializer
Convenience Initializer
Required Initializers
Recommended from ReadMedium