Objective-C

Strong, Weak, Assign: The Essential Difference

By Seren  |  05 Jul, 2026  |  Leave a comment


Every iOS developer knows how to use property modifiers. But fewer can explain why assign causes dangling pointers or how weak automatically sets itself to nil. This article skips the surface-level usage and looks at the underlying implementation — and I’ll share a real crash story that cost me an afternoon of debugging.

Reference Counting Is Where It All Starts

Objective-C memory management is built on reference counting. When an object is created, its reference count is 1. Each new strong reference increases the count by 1. Each strong reference that disappears decreases the count by 1. When the count hits 0, the object is destroyed and its memory is reclaimed.

strong, weak, and assign all answer one question: does this property increment the reference count when referencing an object?

The answers are:

  • strong: Yes (it holds the object)
  • weak: No (it doesn’t hold the object, but it sets itself to nil on deallocation)
  • assign: No (it doesn’t hold the object, and the pointer stays unchanged after deallocation)

To make this concrete, here’s a quick demo showing how reference counts change with each modifier:

// Demonstrating reference count behavior
NSObject *obj = [[NSObject alloc] init];  // retainCount = 1

// strong: increments retain count
@property (strong, nonatomic) NSObject *strongProp;
self.strongProp = obj;  // retainCount = 2

// weak: does NOT increment retain count
@property (weak, nonatomic) NSObject *weakProp;
self.weakProp = obj;    // retainCount still = 2

// assign: does NOT increment retain count
@property (assign, nonatomic) NSObject *assignProp;
self.assignProp = obj;  // retainCount still = 2

The distinction is clear: only strong keeps the object alive. The other two are passive observers — but with very different safety guarantees, as we’ll see next.

The Nature of assign: A Dangling Pointer

assign does a simple thing: it points a pointer to a memory address and does nothing to the reference count. Under the hood, it’s just an assignment.

Code Example:

// The danger of assign with objects
@interface MyViewController : UIViewController
@property (assign, nonatomic) NSObject *dangerousProp;  // ⚠️ never do this for objects
@end

- (void)testAssignDanglingPointer {
    NSObject *temp = [[NSObject alloc] init];  // temp holds the only strong ref
    
    self.dangerousProp = temp;  // assign doesn't retain
    // temp goes out of scope here → object deallocated
    // self.dangerousProp now points to freed memory 💣
    
    NSLog(@"%@", self.dangerousProp);  // EXC_BAD_ACCESS!
}

The problem comes when the object pointed to by assign is deallocated. That memory might be reclaimed or reused by a new object. But the _assignProperty pointer still holds the old address. Accessing it means accessing invalid memory — a dangling pointer (EXC_BAD_ACCESS) crash.

Why can assign still be used for primitive types? Because int, float, and struct are allocated on the stack, managed automatically by the system — no reference counting is involved. When you write @property (assign, nonatomic) NSInteger count;, you’re storing a value, not a pointer.

// Safe: assign with primitive types
@property (assign, nonatomic) NSInteger count;     // ✅ value type, stack-allocated
@property (assign, nonatomic) CGFloat height;      // ✅ value type
@property (assign, nonatomic) BOOL isEnabled;      // ✅ value type
@property (assign, nonatomic) CGRect frame;        // ✅ struct, copied by value

// Dangerous: assign with object types
@property (assign, nonatomic) id delegate;         // ⚠️ should use weak
@property (assign, nonatomic) NSString *name;      // ⚠️ should use strong or copy

The Nature of weak: The Runtime Maintains a Side Table

weak has one critical feature that assign lacks: it automatically sets itself to nil when the object is deallocated.

How? The Runtime maintains a weak table (weak_table_t) under the hood. When you declare a weak reference, the Runtime registers the object’s address and the weak pointer in this table.

Code Example:

// weak: auto-nil on deallocation — the safe observer
@interface MyViewController : UIViewController
@property (weak, nonatomic) IBOutlet UILabel *titleLabel;  // weak avoids retain cycle
@property (weak, nonatomic) id<SomeDelegate> delegate;     // standard delegate pattern
@end

- (void)testWeakAutoNil {
    NSObject *obj = [[NSObject alloc] init];
    __weak NSObject *weakRef = obj;
    
    NSLog(@"Before: %@", weakRef);  // prints the object address
    
    obj = nil;  // last strong reference gone → dealloc
    
    NSLog(@"After: %@", weakRef);   // prints (null) — automatically! ✅
}

// What happens inside the Runtime (simplified):
// 1. On weak assignment: objc_storeWeak() registers &obj → &weakRef in weak_table_t
// 2. On dealloc: clearDeallocating() scans weak_table_t for this object
// 3. Every registered weak pointer → set to nil
// See: objc-weak.mm in Apple's objc4 source

When an object is deallocated (during dealloc), the Runtime calls clearDeallocating. This function looks up the object’s address in the weak table and sets every registered weak pointer to nil.

This is the fundamental difference between weak and assign: weak has a “clear pointer on deallocation” mechanism. assign does not.

There is a small performance cost to this safety: the Runtime must maintain the weak table (a hash map), and every weak assignment requires a lock on that table. For most use cases, this overhead is negligible — the safety guarantee is worth it.

The Nature of strong: It Actually Holds the Object

strong‘s setter calls objc_storeStrong under the hood:

Code Example:

// strong: the default owner — keeps objects alive
@interface Person : NSObject
@property (strong, nonatomic) NSString *name;      // Person owns this string
@property (strong, nonatomic) NSArray *friends;     // Person owns this array
@end

- (void)testStrongLifecycle {
    Person *person = [[Person alloc] init];  // retainCount = 1
    
    NSString *name = [[NSString alloc] initWithFormat:@"Seren"];
    person.name = name;  // retainCount of name string → 2
    // name local variable + person.name both hold strong refs
    
    name = nil;  // local ref gone, but person.name still holds → retainCount = 1
    // The string is still alive because person owns it
    
    person = nil;  // last strong ref gone → string deallocated too
}

// Under the hood, strong setter is roughly:
// - (void)setName:(NSString *)newName {
//     [newName retain];      // hold the new value first
//     [_name release];       // release the old value
//     _name = newName;
// }
// This retain-before-release ordering prevents deallocation if new == old.

Every time a strong property is set, the new object is retained and the old object is released. When all strong references to an object disappear, its reference count drops to 0 and it’s deallocated.

strong is the default modifier for object properties under ARC, and the most conventional way to manage memory.

A Real-World Crash: When I Used assign Instead of weak for a Delegate

I learned the assign-vs-weak distinction the hard way. In an early iOS project, I was building a custom scroll view that notified its delegate about page changes. Coming from a C background, I declared the delegate property as assign — after all, I just needed to store a pointer, right?

// ❌ What I Wrote (Wrong)
@interface MyCustomScrollView : UIScrollView
@property (assign, nonatomic) id<MyScrollViewDelegate> delegate;
@end

// This worked fine... until the delegate (a UIViewController) was popped
// from the navigation stack. The view controller deallocated, but
// MyCustomScrollView's delegate pointer still pointed to its memory.
// Next scroll event → -[MyScrollViewDelegate respondsToSelector:] → CRASH 💥
//
// The stack trace showed EXC_BAD_ACCESS deep inside objc_msgSend.
// It took me an afternoon of debugging with NSZombie to find the root cause.

The fix was one keyword, but the lesson was huge:

// ✅ The Fix (One Word Changed)
@property (weak, nonatomic) id<MyScrollViewDelegate> delegate;
// Now when the view controller deallocates, delegate → nil automatically.
// No crash. No dangling pointer. No wasted afternoon.

This experience taught me a rule I now follow in every project: every object pointer property must be either strong (ownership) or weak (non-ownership with safety). assign is only for primitive types — period. The one exception is when you’re working with legacy MRC code, but even then, you should migrate to ARC and use weak.

I’ve since seen the same pattern in code reviews — developers using assign for delegate-like patterns “because it works.” It does work… until it doesn’t. And when it fails, it fails silently for weeks, then crashes in production on a customer’s device with no clear breadcrumb trail.

Why assign for Objects Is Obsolescent

In the MRC era, assign was commonly used for objects. But under ARC, using assign for objects is almost completely obsolete. Besides the dangling pointer risk, there’s a deeper reason:

Under ARC, local variables are automatically annotated with __strong. When you assign a value to an assign property, the assigned object might be held only by a temporary strong reference. Once that assignment ends, the temporary strong reference disappears, and the object is deallocated — but the assign property still points to that memory.

ARC handles most memory management issues for you. But the assign trap for objects is one you need to avoid yourself.

// ARC + assign = a recipe for subtle crashes
- (void)setupDataManager {
    DataManager *manager = [[DataManager alloc] init];  // temporary strong ref
    self.assignManager = manager;  // assign — no retain
    // manager strong ref goes out of scope → DataManager deallocated
    // self.assignManager = dangling pointer 💀
}

- (void)handleButtonTap {
    [self.assignManager fetchData];  // 💥 EXC_BAD_ACCESS — often intermittent
    // Sometimes the memory hasn't been reused yet, so it "works."
    // Other times it crashes. Good luck reproducing it.
}

Practical Recommendations

  • Object properties: Use strong in most cases — it’s the default and the safest ownership model.
  • Delegates: Use weak to avoid retain cycles. assign for delegates is a legacy MRC pattern — under ARC, always use weak.
  • IBOutlets: Use weak (the view hierarchy already holds a strong reference). Xcode’s interface builder defaults to weak for this reason.
  • Primitive types: Use assign (no reference counting involved). This includes NSInteger, CGFloat, BOOL, CGRect, and other C types.
  • Block properties: Use copy or strong (under ARC, they behave the same for blocks). copy is the historical convention from the MRC era.
  • NSString properties: Use copy to prevent external mutation of NSMutableString values passed in.

Quick Decision Table

One final word on assign for objects: avoid it whenever possible. It’s not that it doesn’t work — it does, but you’re responsible for ensuring the object stays alive. In an ARC environment, that’s harder to guarantee than it seems. I spent an afternoon debugging one crash caused by this mistake — don’t make the same one. Use weak and let the Runtime handle the safety for you.

Seren
Seren

A developer exploring iOS, Objective-C, Swift, and other tech stacks. Here to document my learning process, pitfalls, and growing journey across new technologies.

Your email address will not be published. Required fields are marked *