When to use weak vs unowned references in Swift

Jul 07, 2024#swift#xcode#debug

Automatic Reference Counting (ARC) is a memory management feature in Swift that automatically tracks and manages the memory usage of your app. It only applies to reference types, specifically instances of classes. It does not apply to value types such as structures, enumerations, or basic data types.

ARC keeps track of how many strong references an object has. When an object’s reference count drops to zero, ARC automatically deallocates that object to free up memory. This helps prevent memory leaks and ensures efficient memory usage.

When two objects reference each other strongly, they can create a retain cycle, preventing either from being deallocated. Both weak and unowned help break these cycles by allowing one object to reference the other without increasing its retain count.

The deinit method of a class is called when an instance of that class is deallocated from memory. If there is a memory leak and an object is not deallocated properly, the deinit method of that object will not be called. In following examples, monitoring deinit messages is a useful diagnostic tool in Swift to ensure proper memory management and to detect and fix memory leaks effectively.

When to use weak references?

A weak reference does not increase the reference count and can be set to nil. Always declared as optional (?) because they can become nil if the referenced object is deallocated. Often used when the reference might become invalid at some point, like a delegate or observer.

The delegate pattern is a common design pattern in Swift that allows one object to act on behalf of, or in coordination with, another object. It often uses a weak reference to the delegate to avoid retain cycles. Here’s an example:

protocol DataFetcherDelegate: AnyObject {
    func didFetchData(_ data: String)
}

class DataFetcher {
    weak var delegate: DataFetcherDelegate? // Use weak to avoid retain cycle
    
    func fetchData() {
        // Simulate data fetching
        let data = "Fetched Data"
        print("Data fetched")
        
        // Notify the delegate
        delegate?.didFetchData(data)
    }
    
    deinit {
        print("DataFetcher is being deinitialized")
    }
}

class DataManager: DataFetcherDelegate {
    func didFetchData(_ data: String) {
        print("DataManager received data: \(data)")
    }
    
    deinit {
        print("DataManager is being deinitialized")
    }
}

var fetcher: DataFetcher? = DataFetcher()
var manager: DataManager? = DataManager()

fetcher?.delegate = manager
fetcher?.fetchData()

fetcher = nil
manager = nil

// Output:
// Data fetched
// DataManager received data: Fetched Data
// DataFetcher is being deinitialized
// DataManager is being deinitialized

It’s a recommended practice to use weak self when dealing with closures that may outlive the object capturing them. This is common when working with asynchronous operations, such as network requests or animations.

class NetworkManager {
    func fetchData(completion: @escaping (String?) -> Void) {
        // Simulate a network request
        DispatchQueue.global().asyncAfter(deadline: .now() + 2) { [weak self] in
            // Check if self (NetworkManager instance) still exists
            guard let self = self else {
                completion(nil) // Return nil if self is deallocated
                return
            }
            
            // Simulate fetching data
            let data = "Fetched data from the network"
            
            DispatchQueue.main.async {
                completion(data)
            }
        }
    }
    
    deinit {
        print("NetworkManager is being deinitialized")
    }
}

// Usage
var manager: NetworkManager? = NetworkManager()

manager?.fetchData { [weak manager] result in
    guard let manager = manager else {
        print("Manager is nil, cannot process result")
        return
    }
    
    if let result = result {
        print("Data received: \(result)")
    } else {
        print("Failed to fetch data")
    }
}

// Simulate manager going out of scope
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
    manager = nil
}

// Keep the playground running to see the output
RunLoop.main.run(until: Date().addingTimeInterval(5))

// Expected Output:
// Data received: Fetched data from the network
// NetworkManager is being deinitialized

When to use unowned references?

An unowned reference does not increase the reference count and is assumed to never be nil. This allows direct access to the referenced object without needing optional unwrapping. Accessing an unowned reference is slightly faster than accessing a weak reference because you don’t need to check and unwrap an optional value.

Using an unowned reference is risky because it can lead to crashes if the referenced object is deallocated before the unowned reference. Only use it when you are absolutely certain the referenced object will always exist, like a closure capturing a reference to its own enclosing class.

class Device {
    let model: String
    unowned var owner: User
    
    init(model: String, owner: User) {
        self.model = model
        self.owner = owner
        print("\(model) device is initialized")
    }
    
    deinit {
        print("\(model) device is being deinitialized")
    }
}

class User {
    let name: String
    var device: Device?
    
    init(name: String) {
        self.name = name
        print("User \(name) is initialized")
    }
    
    func assignDevice(model: String) {
        self.device = Device(model: model, owner: self)
    }
    
    deinit {
        print("User \(name) is being deinitialized")
    }
}

// Usage
var alice: User? = User(name: "Alice")
alice?.assignDevice(model: "iPhone")

alice = nil

// Output:
// User Alice is initialized
// iPhone device is initialized
// User Alice is being deinitialized
// iPhone device is being deinitialized

You should use unowned self when a closure is captured by a property of the same class that holds self. In essence, they have a circular reference.

class Job {
    let name: String
    var task: (() -> Void)?
    
    init(name: String) {
        self.name = name
        print("Job \(name) is initialized")
    }
    
    func start() {
        task = { [unowned self] in
            print("Starting job \(self.name)")
            self.processData()
        }
    }
    
    func processData() {
        print("Processing data for job \(name)")
    }
    
    deinit {
        print("Job \(name) is being deinitialized")
    }
}

// Usage
var job: Job? = Job(name: "Data Processing Job")

// Start the job, which captures self in task closure
job?.start()

// Execute the task closure
job?.task?()

job = nil

// Expected Output:
// Job Data Processing Job is initialized
// Starting job Data Processing Job
// Processing data for job Data Processing Job
// Job Data Processing Job is being deinitialized

You can generally use weak instead of unowned in many cases where unowned is also valid. While unowned is useful in certain scenarios where you can guarantee object lifetime, weak is generally safer and more flexible. It’s recommended to use weak unless you have a specific reason and can ensure the safety of using unowned references.