Swift ARC (Automatic Reference Counting) – A Complete Guide


Introduction

In Swift, Automatic Reference Counting (ARC) is a memory management system that automatically tracks and manages the memory used by objects. It ensures that objects are deallocated when they are no longer needed, preventing memory leaks and optimizing performance.

Unlike manual memory management in older languages, Swift's ARC automatically allocates and deallocates memory for class instances, making it more efficient and reducing developer effort. Understanding ARC is crucial for writing high-performance, memory-efficient Swift applications.



Why is ARC Important in Swift?

  • Prevents Memory Leaks: ARC ensures memory is released when objects are no longer in use.
  • Optimized Performance: Automatically deallocates unused objects, improving app efficiency.
  • No Manual Memory Management: Unlike languages like C++, Swift handles memory automatically.
  • Reduces Retain Cycles: ARC helps prevent strong reference cycles that lead to memory leaks.

How ARC Works in Swift?

ARC keeps track of how many references point to an object. When an object’s reference count reaches zero, Swift automatically deallocates it.

Reference Counting Example

import Foundation

class Person {
  let name: String

  init(name: String) {
    self.name = name
    print("\(name) is initialized")
  }

  deinit {
    print("\(name) is being deallocated")
  }
}

var person1: Person? = Person(name: "John")
person1 = nil // Reference count becomes zero, so object is deallocated

Output

John is initialized
John is being deallocated


Types of References in ARC


1. Strong References (Default Reference Type)

By default, references in Swift are strong references, meaning the object remains in memory as long as a reference exists.

Example of a Strong Reference Retain Cycle

class Student {
  let name: String
  var teacher: Teacher?

  init(name: String) {
    self.name = name
  }

  deinit {
    print("\(name) is being deallocated")
  }
}

class Teacher {
  let name: String
  var student: Student?

  init(name: String) {
    self.name = name
  }

  deinit {
    print("\(name) is being deallocated")
  }
}

var student1: Student? = Student(name: "Alice")
var teacher1: Teacher? = Teacher(name: "Mr. Smith")

student1?.teacher = teacher1
teacher1?.student = student1

// Even when we set both to nil, the objects are not deallocated due to strong reference cycle
student1 = nil
teacher1 = nil

Output (No deallocation due to memory leak)

No output appears, meaning the objects remain in memory due to strong reference cycle.

How to Fix Strong Reference Cycles in Swift?



2. Weak References (weak Keyword)

A weak reference does not increase the reference count, breaking the strong reference cycle.

Example Using weak to Prevent Memory Leak

class Student {
  let name: String
  weak var teacher: Teacher? // weak reference to avoid retain cycle

  init(name: String) {
    self.name = name
  }

  deinit {
    print("\(name) is being deallocated")
  }
}

class Teacher {
  let name: String
  var student: Student?

  init(name: String) {
    self.name = name
  }

  deinit {
    print("\(name) is being deallocated")
  }
}

var student1: Student? = Student(name: "Alice")
var teacher1: Teacher? = Teacher(name: "Mr. Smith")

student1?.teacher = teacher1
teacher1?.student = student1

student1 = nil
teacher1 = nil

Output (Objects successfully deallocated)

Alice is being deallocated
Mr. Smith is being deallocated

3. Unowned References (unowned Keyword)

An unowned reference works like weak, but it must always have a value and cannot be nil. It is used when the referenced object has the same or longer lifetime than the object holding the reference.


Example Using unowned to Prevent Retain Cycle


class CreditCard {
  let number: String
  unowned let holder: Person // Unowned reference to prevent retain cycle

  init(number: String, holder: Person) {
    self.number = number
    self.holder = holder
  }

  deinit {
    print("Credit card \(number) is being deallocated")
  }
}

class Person {
  let name: String
  var card: CreditCard?

  init(name: String) {
    self.name = name
  }

  deinit {
    print("\(name) is being deallocated")
  }
}

var person: Person? = Person(name: "Mark")
person?.card = CreditCard(number: "1234-5678-9101-1121", holder: person!)

person = nil // Both objects will be deallocated

Output (Objects successfully deallocated)


Credit card 1234-5678-9101-1121 is being deallocated
Mark is being deallocated

Best Practices for Using ARC in Swift

  • Use weak references when an object can be nil to prevent retain cycles.
  • Use unowned references when an object is guaranteed to exist throughout its lifetime.
  • Avoid unnecessary strong references between objects.
  • Use optionals (?) for weak references to prevent crashes due to early deallocation.
  • Monitor memory usage using Xcode’s Memory Graph Debugger to detect strong reference cycles.


Common Mistakes in Swift ARC

❌ Using strong references inside closures without [weak self], leading to memory leaks.
✅ Always use [weak self] or [unowned self] inside closures.

class DataFetcher {
  var completion: (() -> Void)?

  func fetchData() {
    completion = { [weak self] in
      print("Data fetched successfully")
    }
  }
}