Skip to main content

Swift Strong References

In Swift, understanding memory management is essential for writing efficient and bug-free applications. This guide explores strong references, which are a fundamental concept in Swift's Automatic Reference Counting (ARC) system.

Introduction to Strong References

A strong reference in Swift is the default relationship between objects. When you create an instance of a class and assign it to a variable or constant, you're creating a strong reference to that instance. This strong reference tells ARC to keep the instance alive in memory as long as that reference exists.

Think of a strong reference as saying: "Keep this object in memory because I need it."

How Strong References Work

When you create an instance of a class in Swift, memory is allocated to store that instance. By default, any variable or constant that holds that instance maintains a strong reference to it.

swift
class Person {
let name: String

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

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

// Creating a strong reference
var john: Person? = Person(name: "John")
// Output: John is being initialized

In this example, the variable john holds a strong reference to a new Person instance. Because of this strong reference, ARC ensures the Person instance remains in memory.

Reference Counting in Action

ARC works by counting the number of strong references to each class instance:

swift
var reference1: Person? = Person(name: "Taylor")
// Output: Taylor is being initialized
// Reference count: 1

var reference2 = reference1
// Reference count: 2

var reference3 = reference1
// Reference count: 3

reference1 = nil
// Reference count: 2

reference2 = nil
// Reference count: 1

reference3 = nil
// Output: Taylor is being deinitialized
// Reference count: 0

The Person instance is only deallocated when the reference count reaches zero, which happens when the last strong reference (reference3) is set to nil.

Strong Reference Cycles

One of the challenges with strong references is that they can create reference cycles (also known as retain cycles), where two or more objects strongly reference each other, preventing ARC from deallocating them even when they're no longer needed.

swift
class Student {
let name: String
var course: Course?

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

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

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

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

deinit {
print("\(name) course is being deinitialized")
}
}

// Creating instances
var student: Student? = Student(name: "Alex")
var course: Course? = Course(name: "Swift Programming")

// Creating a reference cycle
student?.course = course
course?.student = student

// Attempting to break the references
student = nil
course = nil

// No deinitialization messages are printed!
// Memory leak occurs because of the reference cycle

In this example, even though we set both student and course to nil, neither object is deallocated because they still have strong references to each other. This creates a memory leak.

Breaking Strong Reference Cycles

To solve strong reference cycles, Swift provides two solutions:

  1. Weak references: Using the weak keyword
  2. Unowned references: Using the unowned keyword

These special reference types allow one object to refer to another without keeping it alive. We'll cover these in separate tutorials.

When to Use Strong References

Strong references are appropriate in many scenarios:

  1. When the referenced object should remain in memory as long as the referencing object exists
  2. For parent-child relationships where the parent controls the child's lifecycle
  3. When the referenced object is essential for the referencing object to function

Real-World Example: View Controllers and Views

In iOS development, a common use of strong references is between a view controller and its views:

swift
class ProfileViewController {
// Strong reference to UI elements
let profileImageView: UIImageView
let nameLabel: UILabel

// These UI elements should exist as long as the view controller exists
init() {
profileImageView = UIImageView()
nameLabel = UILabel()
print("ProfileViewController initialized")
}

deinit {
print("ProfileViewController deinitialized")
}
}

// Using the view controller
var viewController: ProfileViewController? = ProfileViewController()
// Output: ProfileViewController initialized

// When we're done with the view controller
viewController = nil
// Output: ProfileViewController deinitialized

In this example, the view controller strongly references its UI elements because they need to exist throughout the view controller's lifecycle.

Best Practices for Using Strong References

  1. Be conscious of ownership: Think about which objects should "own" other objects
  2. Watch out for cycles: Be vigilant about potential strong reference cycles
  3. Use weak/unowned references: When appropriate, use alternatives to strong references
  4. Clean up references: Set references to nil when they're no longer needed
  5. Use value types: When possible, use structs and enums (value types) instead of classes

Summary

Strong references are the default reference type in Swift and a key component of ARC. They ensure that objects remain in memory as long as they're needed. However, careless use of strong references can lead to reference cycles and memory leaks.

Understanding when to use strong references and when to opt for alternatives like weak or unowned references is crucial for efficient memory management in your Swift applications.

Additional Resources

Exercises

  1. Create a class hierarchy with three levels (e.g., UniversityDepartmentProfessor) using appropriate strong references.
  2. Identify and fix a strong reference cycle in the following code:
    swift
    class Car {
    let model: String
    var driver: Driver?

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

    class Driver {
    let name: String
    var car: Car?

    init(name: String) {
    self.name = name
    }
    }
  3. Experiment with ARC by adding print statements to init and deinit methods of a class, then track when instances are created and destroyed as you create and nil out references.


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)