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 ofinit?
, 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:
- 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 wordconvenience
to indicate it's a secondary, publicly accessible initializer. - The failable initializers are marked with a
+
symbol and a?
after theinit
keyword. - The implicitly unwrapped failable initializer is marked with a
+
symbol and a!
after theinit
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:
- Please consider clapping and following the writer! 👏
- Follow us X | LinkedIn | YouTube | Discord
- Visit our other platforms: In Plain English | CoFeed | Venture | Cubed
- More content at Stackademic.com