ARC
weak reference
memory management
iOS development
Swift programming

Always pass weak reference of self into block in ARC?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

The advice to "always use weak self in blocks" is an oversimplification. You should use [weak self] or [unowned self] only when the block is stored by self (or an object owned by self), creating a retain cycle. If the block is short-lived (e.g., passed to UIView.animate or DispatchQueue.main.async), capturing self strongly is safe and often preferred because it guarantees self is alive during execution. The rule is: use weak self when there is a cycle, not unconditionally.

When You Need weak self (Retain Cycle)

swift
1class ViewController: UIViewController {
2    var onComplete: (() -> Void)?
3
4    func setup() {
5        // RETAIN CYCLE: self → onComplete → self
6        onComplete = {
7            self.updateUI()  // Block captures self strongly
8        }
9    }
10
11    // FIX: break the cycle with [weak self]
12    func setupFixed() {
13        onComplete = { [weak self] in
14            self?.updateUI()  // self is optional, may be nil
15        }
16    }
17}

The cycle: self owns onComplete (stored property), and the block captures self. Neither can be deallocated.

When You Do NOT Need weak self

Short-lived blocks (no cycle)

swift
1// UIView.animate — block is NOT stored by self
2UIView.animate(withDuration: 0.3) {
3    self.view.alpha = 0  // Safe — no retain cycle
4}
5
6// DispatchQueue — block is released after execution
7DispatchQueue.main.async {
8    self.updateUI()  // Safe — GCD owns the block temporarily
9}
10
11// Array operations
12let names = users.map { user in
13    self.format(user.name)  // Safe — map block is not stored
14}

These blocks are owned by the system (UIKit, GCD, stdlib), not by self. Once the block executes, it is released, and its strong reference to self goes away. No cycle exists.

Intentionally keeping self alive

swift
1// Network request — you WANT self to stay alive until completion
2URLSession.shared.dataTask(with: url) { data, response, error in
3    // self is captured strongly — keeps the view controller alive
4    // until the request completes
5    self.handleResponse(data)
6}.resume()

Using [weak self] here would cause self to be deallocated if the user navigates away, and the response would be silently dropped. Sometimes strong capture is the desired behavior.

weak self vs unowned self

swift
1class ViewController: UIViewController {
2    var timer: Timer?
3
4    func startTimer() {
5        // weak self — self becomes nil if deallocated
6        timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
7            guard let self = self else { return }
8            self.tick()
9        }
10
11        // unowned self — crashes if self is deallocated
12        timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [unowned self] _ in
13            self.tick()  // CRASH if self was deallocated
14        }
15    }
16
17    deinit {
18        timer?.invalidate()
19    }
20}
CaptureBehavior when deallocatedUse when
[weak self]self becomes nilSelf might be deallocated before block runs
[unowned self]Crash (dangling pointer)You guarantee self outlives the block
Strong (default)Keeps self aliveBlock is short-lived with no cycle

The guard let self = self Pattern

swift
1fetchData { [weak self] result in
2    // Option 1: guard let (preferred)
3    guard let self = self else { return }
4    self.data = result
5    self.tableView.reloadData()
6
7    // Option 2: optional chaining (for simple calls)
8    // self?.data = result
9    // self?.tableView.reloadData()
10}

guard let self = self (Swift 4.2+) creates a strong local reference for the rest of the closure, preventing self from being deallocated mid-execution.

Objective-C Equivalent

objectivec
1// Objective-C: weakSelf / strongSelf pattern
2__weak typeof(self) weakSelf = self;
3self.completionHandler = ^{
4    __strong typeof(weakSelf) strongSelf = weakSelf;
5    if (!strongSelf) return;
6    [strongSelf updateUI];
7};

The two-step pattern (__weak outside, __strong inside) prevents retain cycles while ensuring self is not deallocated during block execution.

Common Retain Cycle Patterns

swift
1// 1. Stored closures
2class NetworkManager {
3    var onSuccess: ((Data) -> Void)?  // Stored → needs [weak self]
4}
5
6// 2. NotificationCenter (pre-iOS 9 or with stored observer)
7let observer = NotificationCenter.default.addObserver(
8    forName: .someNotification, object: nil, queue: .main
9) { [weak self] notification in
10    self?.handleNotification(notification)
11}
12
13// 3. KVO with closures
14observation = observe(\.someProperty) { [weak self] _, _ in
15    self?.react()
16}
17
18// 4. Delegate closures stored in child objects
19class Parent {
20    let child = Child()
21    func setup() {
22        child.onAction = { [weak self] in  // child.onAction → self → child
23            self?.respond()
24        }
25    }
26}

Decision Flowchart

 
1Is the block/closure stored by self (or an object owned by self)?
2├── YESDoes the block reference self?
3│         ├── YESUse [weak self] or [unowned self]
4│         └── NONo capture, no issue
5└── NOIs self needed after the block runs?
6          ├── YESCapture self strongly (default) — keeps self alive
7          └── NO[weak self] is optional but harmless

Common Pitfalls

  • Using [weak self] everywhere blindly: Unnecessary weak self adds optional unwrapping boilerplate and can cause self to be deallocated before the block completes. Only use it when there is an actual retain cycle or when you explicitly want to allow early deallocation.
  • Using [unowned self] when self might be deallocated: unowned is a performance optimization over weak (no optional unwrapping) but crashes if self is deallocated. Only use it when you can guarantee the object outlives the closure (e.g., a closure that is always invalidated in deinit).
  • Forgetting to invalidate timers: Timer.scheduledTimer retains its target. Even with [weak self] in the block variant, the non-block scheduledTimer(target:) retains target strongly. Always invalidate timers in deinit or viewWillDisappear.
  • Not recognizing escaping vs non-escaping closures: Non-escaping closures (like Array.map, Array.filter) execute synchronously and are released immediately — no cycle possible. Escaping closures (@escaping) can be stored and may cause cycles. The compiler marks closures as @escaping when they outlive the function call.
  • Retain cycles through intermediate objects: self → manager → completion block → self is a cycle even though self does not directly own the block. Trace the full ownership chain to identify cycles.

Summary

  • Use [weak self] when the block is stored by self or an object owned by self (retain cycle)
  • Do NOT use [weak self] for short-lived blocks (animations, GCD dispatch, map/filter) — no cycle exists
  • Use guard let self = self else { return } inside weak-self closures to prevent mid-execution deallocation
  • Prefer [weak self] over [unowned self] unless you can guarantee the object's lifetime
  • The decision depends on ownership: who stores the block, and does the block reference the owner?

Course illustration
Course illustration

All Rights Reserved.