Using ARC Weak References in Objective-C: A Practical Guide
A practical guide to Objective‑C ARC weak references: how they prevent retain cycles, a worked delegate/block example, common pitfalls, and verification steps using Xcode breakpoints and Instruments.
23 May 2026, 04:17 UTC

The Problem and the Takeaway
Objective‑C’s Automatic Reference Counting (ARC) eliminates manual retain/release calls, but it introduces a new responsibility: breaking retain cycles. The weak keyword creates a non‑owning reference that ARC automatically sets to nil when the referenced object is deallocated. Use weak for delegates, outward references in parent‑child hierarchies, and any reference that must not keep an object alive.
How Weak References Work Under ARC
When you declare a property or variable with __weak (or the property attribute weak), the compiler registers the address with the runtime’s weak table. At deallocation time, the runtime walks that table and zeroes out every weak pointer pointing to the dying object. This happens atomically with respect to the deallocation sequence, so a weak read after the object’s dealloc returns nil without crashing.
Key constraints:
- Only Objective‑C objects (subclasses of
NSObject) can be weak. Primitives, C structs, and non‑ARC types are compile‑time errors. - Weak is only available when the Objective‑C Automatic Reference Counting build setting is
YES(the default since Xcode 4.2). - On iOS 5 / OS X 10.7 and later,
weakis fully supported. For older targets you must fall back tounsafe_unretained, which does not zero the pointer.
Worked Example: Delegate Pattern and Block Capture
The following snippet shows a typical view controller that owns a worker object. The worker holds a weak delegate back to the controller, and the controller uses a weak reference to itself inside an asynchronous completion block.
// Worker.h
#import <Foundation/Foundation.h>
@protocol WorkerDelegate
- (void)workerDidFinish:(id)result;
@end
@interface Worker : NSObject
@property (nonatomic, weak) id<WorkerDelegate> delegate;
- (void)startWithCompletion:(void (^)(id result))completion;
@end
// Worker.m
@implementation Worker
- (void)startWithCompletion:(void (^)(id result))completion {
// Simulate async work
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)),
dispatch_get_main_queue(), ^{
id result = @"completed";
if (completion) completion(result);
if (self.delegate) [self.delegate workerDidFinish:result];
});
}
@end
// ViewController.m
#import "ViewController.h"
#import "Worker.h"
@interface ViewController ()
@property (nonatomic, strong) Worker *worker;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.worker = [[Worker alloc] init];
self.worker.delegate = self; // weak, no retain cycle
// Capture self weakly so the block doesn't extend the controller's lifetime
__weak typeof(self) weakSelf = self;
[self.worker startWithCompletion:^(id result) {
typeof(self) strongSelf = weakSelf;
if (strongSelf) {
NSLog(@"Controller received: %@", result);
} else {
NSLog(@"Controller already deallocated");
}
}];
}
- (void)workerDidFinish:(id)result {
NSLog(@"Delegate callback: %@", result);
}
// Simulate user popping the controller
- (IBAction)dismissTapped:(id)sender {
self.worker = nil; // strong reference released → Worker deallocates
// At this point worker.delegate (weak) becomes nil automatically
}
@endWhat the Example Demonstrates
- Delegate as weak –
Worker.delegateis declaredweak. WhenViewControllerreleases its strong reference toworker, the worker is deallocated and its weak delegate pointer is zeroed. No retain cycle forms. - Weak self in a block – The
__weak typeof(self) weakSelf = self;pattern ensures the completion block does not keep the view controller alive. Inside the block we promote to a strong reference (strongSelf) for the duration of the callback; if the controller is already gone,strongSelfisniland we skip the UI update. - Automatic nil assignment – After
self.worker = nilindismissTapped:, bothworker.delegateand any other weak references to that worker instance becomenilimmediately.
Common Mistakes and How to Avoid Them
1. Accidental Strong Delegate
Declaring a delegate property as strong (or omitting weak on a property that defaults to strong) creates a retain cycle: A → strong → B → strong → A. Neither object is ever released.
// WRONG
@property (nonatomic, strong) id<WorkerDelegate> delegate;
// RIGHT
@property (nonatomic, weak) id<WorkerDelegate> delegate;2. Capturing Self Strongly in a Block That Outlives the Object
If a block is stored (e.g., as a property) and captures self without a weak intermediate, the block retains self, and self retains the block → cycle.
// WRONG
self.completionBlock = ^{ [self doSomething]; };
// RIGHT
__weak typeof(self) weakSelf = self;
self.completionBlock = ^{ typeof(self) strongSelf = weakSelf; if (strongSelf) [strongSelf doSomething]; };3. Assuming a Weak Reference Is Always Valid
A weak reference can become nil at any point. Always guard access:
if (self.weakDelegate) {
[self.weakDelegate callback];
}4. Thread‑Safety Misconception
Weak reference zeroing is atomic with respect to deallocation, but reading a weak pointer while another thread is releasing the last strong reference is a race. The read may see a stale non‑nil pointer that becomes invalid moments later. For multi‑threaded code, either synchronize access or use a strong local reference:
// Safe pattern on any thread
id strongRef = self.weakProperty; // ARC retains if non‑nil
if (strongRef) {
[strongRef doWork];
}5. Using Weak on Non‑Object Types
Attempting __weak int *ptr or __weak MyStruct s produces a compile‑time error. Only Objective‑C object pointers qualify.
Verifying Correct Behavior in Xcode
Breakpoint Test
- Create a new macOS or iOS project in Xcode 14+ with ARC enabled (default).
- Add the
WorkerandViewControllerclasses from the example above. - Set a breakpoint on the line after
self.worker = nilindismissTapped:. - Run the app, trigger the dismiss action, and inspect
self.worker.delegatein the debugger. It should shownil. - Also inspect
weakSelfinside the completion block after the worker finishes; it should benilif the controller was dismissed before the async callback.
Instruments Leaks Check
- Choose Product → Profile (or ⌘I) and select the Leaks instrument.
- Run the app, perform the dismiss action several times.
- Observe the Leaks timeline: no red spikes should appear for
WorkerorViewControllerinstances. - Switch to the Allocations instrument, filter by your class names, and verify that instance counts return to zero after dismissal.
Static Analyzer
Run Product → Analyze (⇧⌘B). The Clang static analyzer flags missing weak on delegate properties and potential strong capture cycles in blocks.
Limitations and Edge Cases
| Scenario | Behavior | Workaround |
|---|---|---|
| Targeting iOS 4 / OS X 10.6 | weak unavailable | Use unsafe_unretained; manually nil the reference in dealloc. |
| Weak reference to an object that is still initializing | May be nil if accessed before init returns | Ensure the object is fully initialized before assigning to a weak variable. |
Weak property accessed during dealloc | Already zeroed by runtime | Do not rely on weak pointers in dealloc; use strong locals if needed. |
| Bridging to Swift | Swift’s weak maps directly to Objective‑C weak | No special action needed; the same zeroing semantics apply. |
Quick Reference Checklist
- ✅ Mark delegates, data sources, and outward parent pointers
weak. - ✅ Use
__weak typeof(self) weakSelf = self;before any block that may outliveself. - ✅ Promote to strong inside the block:
typeof(self) strongSelf = weakSelf;. - ✅ Guard every weak access with an
ifor local strong reference. - ✅ Verify with breakpoints, Leaks instrument, and static analyzer.
- ❌ Never use
weakon primitives, structs, or non‑ARC code. - ❌ Don’t assume a weak reference stays non‑nil across an await point.
Summary
ARC’s weak qualifier is the primary tool for breaking retain cycles in Objective‑C. It provides automatic zeroing when the target object deallocates, but it only works on Objective‑C objects under ARC, and it does not eliminate the need for careful thread‑safe access patterns. The delegate/block example above captures the two most common real‑world uses: a weak delegate property and a weak self capture in an escaping closure. Validate the behavior with a breakpoint after releasing the strong owner, and confirm the absence of leaks with Instruments.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.