Understanding Automatic Reference Counting (ARC)

Automatic Reference Counting (ARC) manages memory automatically in Swift by tracking how many references point to each object. When the reference count reaches zero, ARC deallocates the object.


However, ARC can't handle reference cycles automatically. This is where understanding different reference types becomes crucial for preventing memory leaks and crashes.



1. Strong References (Default)

Strong references are the default in Swift. They increase the reference count and prevent the object from being deallocated while the reference exists.


Basic Strong Reference Example

class Person {
    let name: String
    
    init(name: String) {
        self.name = name
        print("\(name) is being initialized")
    }
    
    deinit {
        print("\(name) is being deinitialized")
    }
}

// Strong reference example
var person1: Person? = Person(name: "John") // Reference count: 1
var person2: Person? = person1              // Reference count: 2

person1 = nil                               // Reference count: 1
person2 = nil                               // Reference count: 0, object is deallocated




When to Use Strong References

  • Parent-to-child relationships
  • Owner-to-owned object relationships
  • When you need to ensure the object stays alive
  • Default choice for most properties

2. Weak References

Weak references don't increase the reference count and automatically become nil when the referenced object is deallocated. They must always be optional variables.


Weak Reference Example

class Apartment {
    let number: Int
    weak var tenant: Person?  // weak reference
    
    init(number: Int) {
        self.number = number
    }
    
    deinit {
        print("Apartment \(number) is being deinitialized")
    }
}

class Person {
    let name: String
    var apartment: Apartment?  // strong reference
    
    init(name: String) {
        self.name = name
    }
    
    deinit {
        print("\(name) is being deinitialized")
    }
}

// Usage
var john: Person? = Person(name: "John")
var apartment: Apartment? = Apartment(number: 73)

john?.apartment = apartment    // Person strongly references Apartment
apartment?.tenant = john       // Apartment weakly references Person

john = nil                     // Person is deallocated
print(apartment?.tenant)       // nil - weak reference automatically set to nil




Common Weak Reference Patterns

// Delegate pattern
protocol NetworkManagerDelegate: AnyObject {
    func didReceiveData(_ data: Data)
    func didFailWithError(_ error: Error)
}

class NetworkManager {
    weak var delegate: NetworkManagerDelegate?  // Prevent retain cycle
    
    func fetchData() {
        // Network request logic
        delegate?.didReceiveData(responseData)
    }
}

// Closure capture lists
class ViewController: UIViewController {
    var networkManager = NetworkManager()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // ❌ Strong reference cycle
        networkManager.completion = { data in
            self.updateUI(with: data)  // self is captured strongly
        }
        
        // ✅ Weak reference prevents cycle
        networkManager.completion = { [weak self] data in
            self?.updateUI(with: data)  // self is captured weakly
        }
    }
    
    private func updateUI(with data: Data) {
        // Update UI
    }
}




When to Use Weak References

  • Delegate properties
  • Child-to-parent references
  • Breaking retain cycles in closures
  • Observer patterns
  • When the referenced object might be deallocated

3. Unowned References

Unowned references don't increase the reference count but assume the referenced object will always be available. They're not optional and will crash if accessed after the object is deallocated.


Unowned Reference Example

class Customer {
    let name: String
    var creditCard: CreditCard?
    
    init(name: String) {
        self.name = name
    }
    
    deinit {
        print("\(name) is being deinitialized")
    }
}

class CreditCard {
    let number: UInt64
    unowned let customer: Customer  // unowned reference
    
    init(number: UInt64, customer: Customer) {
        self.number = number
        self.customer = customer
    }
    
    deinit {
        print("Card #\(number) is being deinitialized")
    }
}

// Usage
var john: Customer? = Customer(name: "John Appleseed")
john?.creditCard = CreditCard(number: 1234_5678_9012_3456, customer: john!)

john = nil  // Both Customer and CreditCard are deallocated




Unowned vs Weak Decision Tree

// Use weak when the reference might become nil
class Parent {
    var children: [Child] = []
}

class Child {
    weak var parent: Parent?  // Child might outlive parent
}

// Use unowned when the reference should never become nil
class View {
    unowned let controller: ViewController  // View should never outlive controller
    
    init(controller: ViewController) {
        self.controller = controller
    }
}

// Real-world example: Network request with callback
class APIClient {
    func fetchUser(completion: @escaping (User?) -> Void) {
        // Network request
    }
}

class UserViewController: UIViewController {
    let apiClient = APIClient()
    
    func loadUser() {
        // Use weak because ViewController might be deallocated before request completes
        apiClient.fetchUser { [weak self] user in
            self?.updateUI(with: user)
        }
    }
    
    func loadCriticalData() {
        // Use unowned only if you're certain ViewController will exist
        // when the closure executes (generally not recommended for async operations)
        apiClient.fetchUser { [unowned self] user in
            self.updateUI(with: user)  // Will crash if self is deallocated
        }
    }
}




When to Use Unowned References

  • When the referenced object will always outlive the current object
  • One-to-one relationships where objects have the same lifetime
  • When you want to avoid optional unwrapping
  • Performance-critical code (slight performance advantage over weak)

Memory Leaks in Real Applications

Common Retain Cycle Scenarios

// 1. Timer retain cycles
class TimerViewController: UIViewController {
    var timer: Timer?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // ❌ Timer strongly retains target
        timer = Timer.scheduledTimer(timeInterval: 1.0, 
                                   target: self, 
                                   selector: #selector(timerFired), 
                                   userInfo: nil, 
                                   repeats: true)
    }
    
    // ✅ Use weak reference or invalidate timer
    deinit {
        timer?.invalidate()  // Essential to break the cycle
    }
    
    @objc func timerFired() {
        // Update UI
    }
}

// 2. Notification center retain cycles
class NotificationViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // ❌ Can create retain cycle with certain notification patterns
        NotificationCenter.default.addObserver(
            self, 
            selector: #selector(handleNotification), 
            name: .init("CustomNotification"), 
            object: nil
        )
    }
    
    // ✅ Always remove observers
    deinit {
        NotificationCenter.default.removeObserver(self)
    }
    
    @objc func handleNotification() {
        // Handle notification
    }
}

// 3. URLSession retain cycles
class NetworkViewController: UIViewController {
    func fetchData() {
        // ❌ Potential retain cycle
        URLSession.shared.dataTask(with: url) { data, response, error in
            self.processData(data)  // Strong capture of self
        }.resume()
        
        // ✅ Use weak capture
        URLSession.shared.dataTask(with: url) { [weak self] data, response, error in
            self?.processData(data)
        }.resume()
    }
}




Advanced Memory Management Patterns

// Custom weak reference wrapper
class WeakWrapper {
    weak var value: T?
    
    init(_ value: T) {
        self.value = value
    }
}

// Weak array for observers
class EventEmitter {
    private var observers: [WeakWrapper] = []
    
    func addObserver(_ observer: EventObserver) {
        observers.append(WeakWrapper(observer))
    }
    
    func removeObserver(_ observer: EventObserver) {
        observers.removeAll { $0.value === observer }
    }
    
    func emit() {
        // Clean up nil references
        observers = observers.filter { $0.value != nil }
        
        // Notify active observers
        observers.forEach { $0.value?.handleEvent() }
    }
}

// Lazy initialization with unowned
class ExpensiveResource {
    unowned let owner: ResourceOwner
    
    init(owner: ResourceOwner) {
        self.owner = owner
        // Expensive setup
    }
}

class ResourceOwner {
    lazy var resource: ExpensiveResource = ExpensiveResource(owner: self)
}





Debugging Memory Issues

Using Instruments

  1. Leaks Instrument: Detects retain cycles and memory leaks
  2. Allocations Instrument: Tracks memory usage patterns
  3. VM Tracker: Monitors virtual memory usage

Debug Techniques

// Add deinit methods to verify deallocation
class MyViewController: UIViewController {
    deinit {
        print("MyViewController deallocated")
        // This should print when the VC is dismissed
        // If it doesn't, you have a retain cycle
    }
}

// Use memory debugging tools
// In Xcode: Product → Profile → Leaks
// Runtime debugging: Environment Variables → MallocStackLogging = 1

// Weak reference debugging
class DebuggableWeakRef {
    weak var value: T? {
        didSet {
            if value == nil {
                print("Weak reference became nil")
            }
        }
    }
    
    init(_ value: T) {
        self.value = value
    }
}





Best Practices Summary

Reference Type Use When Characteristics
Strong Parent-to-child, ownership Default, increases retain count
Weak Delegates, child-to-parent Optional, becomes nil automatically
Unowned Equal lifetimes, performance critical Non-optional, crashes if accessed after dealloc

Quick Decision Guide

  1. Default to strong unless you have a specific reason
  2. Use weak for delegates and back-references
  3. Use weak in closures that might outlive the object
  4. Use unowned sparingly and only when you're certain of lifetimes
  5. Always test memory management with Instruments

Common Mistakes to Avoid

  • Using unowned when the object might be deallocated (causes crashes)
  • Forgetting to use weak in delegate properties
  • Creating retain cycles with closures
  • Not invalidating timers in deinit
  • Assuming ARC handles all memory management automatically

Production Experience

These patterns and debugging techniques are documented in my iOS-Notes repository, including examples of memory leaks I've encountered and fixed in production applications.