Goal
- Compare
Self,self,weak self, andunowned selfin Swift side by side, using the same closure example each time to isolate what actually changes.
Project
Cheat Sheet
- Proactively capture
selfasweakwithin theclosure. - If a
closureis escaping, you don’t want to useunowned selfas 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.propertyNameinside closures - a built-in reminder that you might be capturingself.
Deep Dive
What is Self?
- The keyword
Selfin 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
selfin Swift is used when you want to explicitly refer to a property or method of the current instance. - For example, if you have an
HTMLElementclass with propertiesnameandtext, and you want to set these properties within an instance method, you’d useselflike so. ThisHTMLElementexample 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
}
}

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
closureslikeasHTML()and escaping functions, you need to be careful withselfbecause 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, likeclasses, are reference types. - Swift requires you to write
self.somePropertyorself.someMethod()(rather than justsomePropertyorsomeMethod()) whenever you refer to a member ofselfwithin aclosure. This helps you remember that it’s possible to capture self by accident. - Swift provides an elegant solution to this problem, known as a
closurecapture 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
selfwithin theclosure. The difference between weak and unowned is that weak references can becomenil, 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 toself, preventing a strong reference cycle and also makingselfan Optional. - In the
HTMLElementexample above, makingselfweakwill 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
}
}

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 toselfpreventing a strong reference cycle. [unowned self]breaks the strong reference cycle just like[weak self]does, but it does not makeselfan Optional. That’s why, with unowned, we don’t have to unwrapself- and also why accessingselfafter the instance has been deallocated causes a runtime error instead of giving usnil.
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
}
}

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 selfmakesselfan Optional inside the closure - it can becomenil, and Swift requires you to unwrap it before use.unowned selfdoes not makeselfoptional - 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
weakreference then gives younil, while accessing anunownedreference then causes a runtime crash.