Goal

  • Compare Self, self, weak self, and unowned self in Swift side by side, using the same closure example each time to isolate what actually changes.

Project

Cheat Sheet

  • Proactively capture self as weak within the closure.
  • If a closure is escaping, you don’t want to use unowned self as you can’t control the life cycle, and it’s hard to understand the lifetime of objects, especially when they’re controlled by frameworks like UIKit.
  • Swift requires explicit self.propertyName inside closures - a built-in reminder that you might be capturing self.

Deep Dive

What is Self?

  • The keyword Self in Swift refers to the type itself, rather than a specific instance - and unlike writing the class name directly, it automatically resolves to whichever concrete type is actually in use, as shown below:
class HTMLElement {
    static let defaultName = "div"
    static let defaultContent = "Default content"

    let name: String
    let text: String?

    required init(name: String, text: String? = nil) {
        self.name = name
        self.text = text
    }

    // `Self` here means "whatever type this method is actually called on" -
    // not hardcoded to HTMLElement.
    static func makeDefault() -> Self {
        Self(name: Self.defaultName, text: Self.defaultContent)
    }
}

class ParagraphElement: HTMLElement {}

HTMLElement.makeDefault()       // returns an HTMLElement
ParagraphElement.makeDefault()  // returns a ParagraphElement - Self resolved to the subclass

Try it live - its test confirms Self resolves to HTMLElement when called on the base class, and to ParagraphElement when called on the subclass.

What is self?

  • The keyword self in Swift is used when you want to explicitly refer to a property or method of the current instance.
  • For example, if you have an HTMLElement class with properties name and text, and you want to set these properties within an instance method, you’d use self like so. This HTMLElement example is adapted from Apple’s own Swift Programming Language guide on ARC - reused here since it’s a well-known, battle-tested illustration, and extended below with a playground you can actually run and tests that prove the behavior:

final class HTMLElement: @unchecked Sendable {
    let name: String
    let text: String?
    
    // () -> String, or “a function that takes no parameters, and returns a String value”.
    lazy var asHTML: () -> String = {
        if let text = self.text {
            return "<\(self.name)>\(text)</\(self.name)>"
        } else {
            return "<\(self.name) />"
        }
    }
    
    
    init(name: String, text: String? = nil) {
        self.name = name
        self.text = text
    }
}

Strong reference cycle between HTMLElement and its closure via self

Try it live - its test proves sut (short for “system under test” - the object being tested) never deallocates here, confirming the retain cycle. That test is meant to fail - the failure is the leak being caught. The sut/trackForMemoryLeaks testing pattern used across this playground is adapted from Essential Developer’s iOS Lead Essentials course.

  • However, when it comes to closures like asHTML() and escaping functions, you need to be careful with self because it can create a strong reference cycle (also called a retain cycle) and cause memory leaks if not managed properly.
  • This strong reference cycle occurs because closures, like classes, are reference types.
  • Swift requires you to write self.someProperty or self.someMethod() (rather than just someProperty or someMethod()) whenever you refer to a member of self within a closure. This helps you remember that it’s possible to capture self by accident.
  • Swift provides an elegant solution to this problem, known as a closure capture list.
  • A capture list defines the rules to use when capturing one or more reference types within the closure’s body.
  • To prevent this, you can capture a weak or unowned reference to self within the closure. The difference between weak and unowned is that weak references can become nil, while unowned references cannot.

What is weak self?

  • The capture list [weak self] in Swift is used when you want to create a weak reference to self, preventing a strong reference cycle and also making self an Optional.
  • In the HTMLElement example above, making self weak will prevent a strong reference cycle.

final class HTMLElement: @unchecked Sendable {
    let name: String
    let text: String?
    
    // () -> String, or “a function that takes no parameters, and returns a String value”.
    lazy var asHTML: () -> String = { [weak self] in
        if let self,
           let text = self.text {
            return "<\(self.name)>\(text)</\(self.name)>"
        } else {
            return "<\(self?.name ?? "unknown") />"
        }
    }
    
    
    init(name: String, text: String? = nil) {
        self.name = name
        self.text = text
    }
}

No reference cycle - weak self breaks the strong reference

Try it live - its test proves sut deallocates cleanly here, confirming no retain cycle.

What is unowned self?

  • The capture list [unowned self] in Swift is used when you want to create an unowned reference to self preventing a strong reference cycle.
  • [unowned self] breaks the strong reference cycle just like [weak self] does, but it does not make self an Optional. That’s why, with unowned, we don’t have to unwrap self - and also why accessing self after the instance has been deallocated causes a runtime error instead of giving us nil.

final class HTMLElement: @unchecked Sendable {
    let name: String
    let text: String?
    
    // () -> String, or “a function that takes no parameters, and returns a String value”.
    lazy var asHTML: () -> String = { [unowned self] in
        if let text = self.text {
            return "<\(self.name)>\(text)</\(self.name)>"
        } else {
            return "<\(self.name) />"
        }
    }
    
    
    init(name: String, text: String? = nil) {
        self.name = name
        self.text = text
    }
}

No reference cycle - unowned self breaks the strong reference

Try it live - its test proves sut deallocates cleanly too, just like weak self - though accessing it after deallocation would crash (see below).

Important:

  • An unowned reference assumes the other instance has the same lifetime or a longer lifetime.
  • If you try to access the value of an unowned reference after that instance has been deallocated, you’ll get a runtime error.

weak self vs unowned self

  • weak self makes self an Optional inside the closure - it can become nil, and Swift requires you to unwrap it before use.
  • unowned self does not make self optional - it behaves like an implicitly unwrapped reference, with no compile-time check for whether the instance is still alive.
  • Both break the strong reference cycle the same way. The difference only shows up if the referenced instance has already been deallocated: accessing a weak reference then gives you nil, while accessing an unowned reference then causes a runtime crash.

References