Properties in Depth: Ownership, Atomicity, Access Control, and Synthesis
Explain and correctly apply every major @property attribute — strong, weak, assign, copy, nonatomic, atomic, readonly, readwrite, class, and custom getter=/setter= names — describe how the compiler automatically synthesizes accessor methods and a backing ivar, and reason correctly about the tradeoffs each attribute represents.
The Class Anatomy lesson introduced @property as a compact way to declare a piece of an object's state along with automatically-generated accessor methods — @property (nonatomic, copy) NSString *name; on Person, for instance. That single line was doing more work than it looked like, and it's worth pulling apart in full, because properties are one of the most heavily used, and most heavily tested, features in everyday Objective-C code. Every property declaration is really a bundle of decisions: who owns the value (strong, weak, assign, copy), whether access is synchronized (atomic, nonatomic), whether the property can be written to from outside the class at all (readonly, readwrite), whether it belongs to instances or to the class itself (class), and what the generated accessor methods are actually named. Getting these choices right — and understanding what each one costs and buys you — is central to writing correct, non-crashing, thread-aware Objective-C.
What @property Actually Generates: Synthesis
Writing @property (nonatomic, copy) NSString *name; in an @interface is a declaration, not by itself a full implementation — but when the compiler processes the matching @implementation, it automatically performs property synthesis: unless you say otherwise, it generates a getter method (-name), a setter method (-setName:, skipped entirely if the property is readonly), and a backing instance variable to actually hold the value, by default named _name — the underscore-prefixed version of the property name, matching exactly what Class Anatomy showed you accessing directly inside -init.
This synthesis is what lets you write self.name = @"Alice"; (which really sends the setName: message) and NSString *n = self.name; (which really sends the name message) without writing a single line of accessor code yourself, while still being able to reach _name directly whenever you need the raw ivar rather than going through the accessor's logic. You can override this automatic behavior in two ways: writing your own -name and/or -setName: method bodies yourself (the compiler then skips generating whichever ones you provided, but still synthesizes the ivar and whichever accessor you didn't write), or declaring @dynamic name; instead, which tells the compiler 'accessors for this property will be provided some other way at runtime — don't synthesize anything, and don't assume _name exists' (used, for example, with Core Data-managed properties, where accessors are generated dynamically by the framework, not by the compiler).
The diagram below captures the chain synthesis sets up: a single @property line ultimately produces a getter and setter pair, backed by real per-instance storage (the ivar), and everything about *how* that storage is get-and-set — its ownership, its thread-safety guarantees — is controlled by the attributes you put inside the parentheses, which the rest of this lesson works through one at a time.
• @property triggers automatic synthesis: a getter, a setter (unless readonly), and a backing ivar (default name _propertyName). • self.name = ... calls the synthesized setter (setName:); self.name calls the synthesized getter (name). • Providing your own getter and/or setter implementation opts out of synthesizing just that accessor; the ivar is still synthesized. • @dynamic name; opts out of synthesis entirely — accessors are supplied some other way at runtime (e.g. Core Data).
Diagram: From @property to Storage
It helps to think of a single @property declaration as the top of a small pipeline, where each attribute you add configures one layer of that pipeline without changing its overall shape: a getter and setter are generated, those accessors read and write some actual backing storage, and the attributes you chose determine exactly how that storage is retained, replaced, or protected every time the accessor runs.
Ownership: strong, weak, and assign
strong is the default ownership for an object-typed property under ARC (Automatic Reference Counting, covered fully in the Memory Management module) — it means the property holds a strong, owning reference: as long as this property points at an object, ARC guarantees that object is kept alive, because this property counts as one of the things keeping its reference count above zero. @property (nonatomic, strong) Person *bestFriend; is the ordinary, safe default for most object properties, and it's what you get even if you omit an ownership keyword entirely on a modern object property.
weak is a non-owning reference: the property points at an object without keeping it alive, and — critically — if the object it points to gets deallocated because nothing else holds a strong reference to it anymore, a weak property automatically becomes nil rather than turning into a dangling pointer to freed memory. This automatic nil-ing is a real runtime service ARC provides specifically for weak references, and it's exactly why weak is the standard choice for delegate references and other back-references that would otherwise create a retain cycle (two objects strongly holding each other, so neither one is ever deallocated) — the full mechanics of retain cycles and why weak avoids them are covered in the Memory Management module.
assign performs a plain, unmanaged assignment with no ownership bookkeeping at all — historically the standard choice for non-object scalar properties like NSInteger or BOOL, where the concept of 'ownership' doesn't apply (@property (nonatomic, assign) NSInteger age;). Under older, pre-ARC manual reference counting (MRC) code, assign was also sometimes used on object properties, but doing that today is dangerous: an assign object property does not keep its referenced object alive and does not nil itself out when that object is deallocated the way weak does, so the property can be left pointing at memory that has already been freed — a dangling pointer — and using it afterward is undefined behavior, often a crash. In modern ARC code, assign should be reserved for non-object (scalar/primitive) properties; use weak, not assign, for any object property that intentionally doesn't own its value.
• strong (default for object properties under ARC): an owning reference; keeps the referenced object alive. • weak: a non-owning reference; automatically becomes nil when the referenced object is deallocated — no dangling pointer. • assign: plain unmanaged assignment, correct for scalars (NSInteger, BOOL, CGFloat); dangerous on object properties (can dangle). • Modern rule: use assign only for non-object properties; use weak, never assign, for a non-owning object reference.
copy: Defensive Copies at Assignment Time
copy behaves like strong in that the property owns its value, but with one crucial addition: at the moment a new value is assigned, the setter makes a copy of that value (by sending it -copy) and stores the copy, rather than storing a reference to the exact object the caller passed in. @property (nonatomic, copy) NSString *name; — the property Person actually used in Class Anatomy — is the textbook example of why this matters: NSString's mutable subclass, NSMutableString, can be passed anywhere an NSString * is expected, since it inherits from NSString. Without copy, if a caller handed Person a mutable string and later mutated that same string object elsewhere, Person's name would silently change too, out from under it, because both were pointing at the same mutable object.
With copy, the setter instead stores an independent, immutable snapshot of whatever was passed in at that moment — so even if the caller's original mutable string is changed afterward, Person's own _name is unaffected, because it's a genuinely different object. This is why copy is the standard, recommended attribute for any property of a type that has a mutable counterpart — most commonly NSString, NSArray, NSDictionary, and their NSMutable* siblings — anywhere you want to guarantee your object's internal state can't be changed by code outside your control after the value was handed to you.
copy only makes sense for object types that actually implement copying (conform to NSCopying) — it is meaningless, and not something you'd apply, to scalar properties like NSInteger, which have no separate 'copy' operation beyond ordinary assignment. As a practical default: reach for copy on NSString/NSArray/NSDictionary-typed properties, strong on other object properties you own and that don't have this mutable-subclass concern (like a Person *bestFriend), weak on non-owning back-references, and assign on scalars.
• copy: like strong (owns the value), but the setter stores an independent copy made at assignment time, not the caller's original object. • Protects your internal state from later external mutation — essential for NSString/NSArray/NSDictionary, which have mutable subclasses (NSMutableString, etc.). • Without copy, a caller's later mutation of an object they handed you can silently change your property's value too. • copy applies to types conforming to NSCopying; it's meaningless for scalar properties like NSInteger.
Atomicity: nonatomic vs. atomic
atomic is the default for a property if you don't specify either atomic or nonatomic — it makes the synthesized getter and setter individually thread-safe in a narrow sense: a call to the getter always returns a value that some complete call to the setter actually stored (never a half-written, corrupted value from a setter that was interrupted midway through), even if the getter and setter happen to run on different threads at the same time. This synchronization has a real runtime cost — every atomic get or set involves locking overhead — which is why atomic is not automatically 'free safety'.
nonatomic removes that synchronization: the generated getter and setter simply read and write the backing ivar directly, with no locking at all. This is faster, and it is overwhelmingly the common choice in modern Objective-C code — @property (nonatomic, copy) NSString *name; in Class Anatomy used it for exactly this reason. The near-universal preference for nonatomic even in code that does care about thread safety comes down to a specific, common misunderstanding worth stating precisely.
The misunderstanding: atomic protects a single get or a single set call from being corrupted mid-operation, but it does absolutely nothing to make a multi-step, compound operation on that property thread-safe. Consider if (self.count > 0) { self.count = self.count - 1; } — even with count declared atomic, another thread could modify count in the gap between the if check and the subsequent assignment, producing exactly the kind of race condition atomic sounds like it should prevent. Because real thread safety almost always requires protecting a whole sequence of operations together (with a lock, a dispatch queue, or another explicit synchronization mechanism — covered in the Concurrency module), atomic alone rarely delivers the safety its name implies, and most codebases pay its locking cost for a guarantee that turns out not to be the one they actually needed — which is exactly why nonatomic became the default, ordinary choice, with real synchronization built deliberately where compound thread safety is actually required.
• atomic (default if unspecified): synchronizes individual getter/setter calls, with real locking overhead; guarantees no half-written value. • nonatomic: no synchronization at all; faster; the overwhelmingly common choice in modern Objective-C code. • Common misunderstanding: atomic does NOT make compound, multi-step operations on a property thread-safe — only single get/set calls. • Real thread safety for a sequence of operations needs explicit synchronization (locks, dispatch queues) — covered in the Concurrency module.
Access Control: readonly, readwrite, and class
readonly tells the compiler to synthesize only a getter — no setter is generated at all, so self.name = ... (or any external object.name = ...) is a compiler error for a readonly property; the only ways to give it a value are inside the class's own implementation, either by assigning the backing ivar directly (_name = ...) or by providing your own custom setter method body despite the readonly declaration. readonly is the right choice for a property that reflects computed or fixed state the outside world should be able to read but never directly set — an object's unique identifier assigned once at creation, for instance.
A very common pattern pairs a public readonly property with a private readwrite redeclaration: the public header (@interface Person : NSObject) declares @property (nonatomic, copy, readonly) NSString *name; so external callers can read but never set it, while a private class extension inside the .m file (covered fully in the Class Extensions module) redeclares the same property as readwrite, giving the class's own internal implementation the ability to set it while the outside world still only sees a read-only property. This is a precise, compiler-enforced way to expose 'read from anywhere, write only from inside this class.'
readwrite is the default if you don't specify either readonly or readwrite — it generates both a getter and a setter, and it's what nearly all ordinary mutable properties use, usually left implicit. Separately, class marks a property as belonging to the class itself rather than to individual instances: @property (class, readonly) NSInteger instanceCount; generates class-level accessor methods (+instanceCount, with a + prefix, exactly like a class method from the previous lesson) instead of instance-level - accessors — useful for state that genuinely belongs to the type as a whole, such as a running count of how many instances have been created.
• readonly: synthesizes only a getter, no setter; external assignment is a compiler error. • readwrite (default if unspecified): synthesizes both getter and setter. • Common pattern: public readonly property, privately redeclared readwrite in a class extension — enforced 'read anywhere, write only internally'. • class: a class-level property, generating + (class-method-style) accessors instead of - (instance-method-style) ones.
Custom Accessor Names: getter= and setter=
By default, the synthesized getter is named exactly like the property (name for @property ... name;) and the synthesized setter is set followed by the capitalized property name (setName:). You can override either name explicitly with the getter= and setter= attributes inside the parentheses: @property (nonatomic, assign, getter=isEnabled) BOOL enabled; renames the generated getter from the default enabled to isEnabled, while the setter stays the conventional setEnabled: unless you also specify setter=.
This is overwhelmingly used for BOOL properties, where Objective-C convention strongly favors a getter that reads like a yes/no question — isEnabled, isHidden, isFinished — over a bare noun-like enabled, purely for readability at the call site: if ([view isHidden]) reads naturally as English, while if ([view hidden]) does not, even though both would work identically as compiled code. Apple's own frameworks follow this convention pervasively (UIView's hidden property has getter isHidden), so matching it in your own BOOL properties keeps your code consistent with the platform's idioms.
A custom name only changes what the accessor method is called — it does not change the property's own name for self.propertyName dot-syntax access, which still uses the property's declared name (self.enabled, not self.isEnabled) even when the underlying getter method has been renamed to isEnabled. Explicit bracket-message calls, by contrast, must use the actual (possibly renamed) method name: [view isHidden] works, [view hidden] does not, once getter=isHidden has been specified.
• getter=customName renames the synthesized getter method; setter=customName: renames the synthesized setter method. • Standard use: BOOL properties, e.g. @property (nonatomic, getter=isEnabled) BOOL enabled; for a question-reading getter. • Dot-syntax (self.enabled) still uses the declared property name regardless of a custom getter name; bracket calls must use the renamed method. • Matches Apple's own framework convention (UIView.hidden → isHidden) for readability.
A Forward Look: Nullability
One more inline property attribute you'll encounter constantly in real, modern Objective-C headers is nullability: nullable and nonnull, written directly among the other attributes — @property (nonatomic, copy, nullable) NSString *middleName; — document and have the compiler enforce whether a property is allowed to hold nil. A nonnull property is a promise, checked by the compiler with warnings, that this property should never actually be nil; a nullable property explicitly documents that nil is a legitimate, expected value. This is a substantial enough topic — including the whole-file shorthand NS_ASSUME_NONNULL_BEGIN/END and how nullability interacts with Swift interoperability — that it gets its own dedicated module (Nullability) later in this course; for now, just recognize it as one more attribute that can appear alongside the ones covered here.
• nullable / nonnull: inline property attributes documenting (and letting the compiler check) whether nil is a legitimate value. • Full treatment, including NS_ASSUME_NONNULL_BEGIN/END, is covered in the dedicated Nullability module.
Code Examples
name uses copy (defensive snapshot) and defaults to readwrite/nonatomic-implied ownership rules; mentor uses weak so it never keeps a mentor alive and auto-nils if the mentor is deallocated; age uses assign since it's a scalar; identifier is publicly readonly but privately redeclared readwrite in a class extension so only Person's own implementation can set it; active gets a custom isActive getter for natural BOOL-reading call sites; instanceCount is a class property, generating a + accessor rather than a - one.
Because name is declared copy, assigning mutableName to p.name doesn't store a reference to mutableName itself -- the setter sends -copy to it and stores an independent, immutable NSString snapshot. Mutating the original mutableString afterward has no effect on p.name, which is exactly the defensive guarantee copy exists to provide. If name had instead been declared strong, p.name would share the same object as mutableName and would print the mutated string too.
@interface Person : NSObject
@property (nonatomic, copy) NSString *name; // owning copy, no locking
@property (nonatomic, weak) Person *mentor; // non-owning, auto-nils
@property (nonatomic, assign) NSInteger age; // scalar, no ownership concept
@property (nonatomic, copy, readonly) NSString *identifier; // public: read-only
@property (nonatomic, getter=isActive) BOOL active; // custom BOOL getter name
@property (class, readonly) NSInteger instanceCount; // class-level property
@end
// Person.m — private class extension gives internal write access to identifier
@interface Person ()
@property (nonatomic, copy, readwrite) NSString *identifier;
@end
@implementation Person
static NSInteger _instanceCount = 0;
+ (NSInteger)instanceCount {
return _instanceCount;
}
- (instancetype)initWithName:(NSString *)name age:(NSInteger)age {
self = [super init];
if (self) {
_name = name;
_age = age;
_identifier = [[NSUUID UUID] UUIDString]; // internal write, via readwrite redeclaration
_instanceCount++;
}
return self;
}
@end@interface Person : NSObject
@property (nonatomic, copy) NSString *name; // copy: safe
@end
NSMutableString *mutableName = [NSMutableString stringWithString:@"Alice"];
Person *p = [[Person alloc] init];
p.name = mutableName; // setter COPIES mutableName's current contents into p.name
[mutableName appendString:@" (mutated!)"];
NSLog(@"%@", p.name); // still prints "Alice" -- unaffected by the later mutationFrom @property to Backing Storage
A single @property declaration synthesizes a getter and setter, which read and write a real backing ivar (_propertyName by default); the attributes in the parentheses (strong/weak/assign/copy, atomic/nonatomic, readonly/readwrite) configure exactly how that storage is owned, copied, and protected.
@property (nonatomic, assign) Person *bestFriend;
@property (nonatomic, weak) Person *bestFriend; // or strong, if this should own it assign on an object property performs no ownership management at all under ARC -- it neither keeps the referenced object alive nor nils itself out when that object is deallocated, so bestFriend can become a dangling pointer to freed memory. Use weak for a non-owning object reference (auto-nils safely) or strong if the property should own the value.
@property (atomic, nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *name; // pick one atomicity attribute, not both atomic and nonatomic are mutually exclusive -- a property has exactly one atomicity setting. Specifying both is a compiler error; decide deliberately (nonatomic is the common default in modern code) rather than listing both.
// assuming atomic alone makes this safe across threads: if (self.count > 0) { self.count = self.count - 1; // atomic on count does NOT protect this whole sequence }
// use explicit synchronization (a lock or dispatch queue) around the whole compound operation @synchronized(self) { if (self.count > 0) { self.count = self.count - 1; } } atomic only guarantees a single get or set call isn't corrupted mid-write -- it does nothing to protect a multi-step read-then-write sequence from being interrupted by another thread in between. Real compound thread safety requires explicit synchronization, covered in the Concurrency module.
@property (nonatomic, copy) NSInteger score;
@property (nonatomic, assign) NSInteger score;
copy only makes sense for object types conforming to NSCopying (like NSString, NSArray); NSInteger is a scalar with no separate copy operation, so copy is meaningless here and is actually a compiler error/warning on a non-object property. Use assign for scalar types.
@property (nonatomic, readonly) NSString *name; // later, from outside the class: person.name = @"Bob";
@property (nonatomic, copy) NSString *name; // omit readonly if external code needs to set it person.name = @"Bob";
readonly generates only a getter -- no setter is synthesized at all, so any external attempt to assign through dot-syntax is a compiler error. If outside code legitimately needs to set the property, declare it readwrite (the default); if only the class itself should set it, keep it publicly readonly and redeclare it readwrite in a private class extension.
What three things does the compiler automatically synthesize for @property (nonatomic, copy) NSString *name; unless you opt out? A. A getter, a setter, and a backing ivar (_name by default) B. Only a getter method C. A subclass of NSString D. A network request handler
A. A getter, a setter, and a backing ivar (_name by default) — Property synthesis automatically generates a getter (name), a setter (setName:, skipped for readonly properties), and a backing instance variable (_name by default) to actually store the value.
Why is assign dangerous on an object property in modern ARC code? A. It's actually not dangerous, just slower than strong B. assign performs no ownership management, so it neither keeps the object alive nor nils out when it's deallocated, risking a dangling pointer C. assign only works on NSString, not other object types D. assign automatically converts the object to a scalar type
B. assign performs no ownership management, so it neither keeps the object alive nor nils out when it's deallocated, risking a dangling pointer — Unlike weak, an assign object property does not become nil when its referenced object is deallocated -- it can be left pointing at freed memory, a dangling pointer, which is undefined behavior to use. weak is the correct choice for a non-owning object reference.
Why is copy the recommended attribute for an NSString property instead of strong? A. copy is always faster than strong B. Because NSString has a mutable subclass (NSMutableString); copy stores a defensive, independent snapshot at assignment time so a caller's later mutation of the original object can't change your property's value C. strong doesn't work with NSString at all D. copy and strong are actually identical for NSString
B. Because NSString has a mutable subclass (NSMutableString); copy stores a defensive, independent snapshot at assignment time so a caller's later mutation of the original object can't change your property's value — If a caller passes an NSMutableString (a subtype of NSString) and the property were strong, later mutating that same object would silently change the property's value too. copy makes an independent immutable copy at assignment time, protecting your internal state.
What is the common misunderstanding about the atomic attribute? A. That it makes the property immutable B. That it guarantees full thread safety for any multi-step operation involving the property, when it actually only protects a single get or set call from being corrupted mid-write C. That atomic properties cannot have a custom getter name D. That atomic is only valid on scalar properties
B. That it guarantees full thread safety for any multi-step operation involving the property, when it actually only protects a single get or set call from being corrupted mid-write — atomic synchronizes individual getter/setter calls so a read never returns a half-written value, but it does nothing to protect a sequence like check-then-set from a race condition -- true compound thread safety needs explicit synchronization such as a lock or dispatch queue.
What does @property (nonatomic, getter=isEnabled) BOOL enabled; change? A. It renames the property itself, so you must now write self.isEnabled instead of self.enabled B. It renames only the generated getter method to isEnabled (used via [obj isEnabled] or still self.enabled in dot-syntax), following the BOOL-as-question convention; the setter stays setEnabled: C. It makes the property read-only D. It has no effect unless setter= is also specified
B. It renames only the generated getter method to isEnabled (used via [obj isEnabled] or still self.enabled in dot-syntax), following the BOOL-as-question convention; the setter stays setEnabled: — getter= renames only the synthesized getter method, matching Objective-C's convention of BOOL getters reading like yes/no questions (isEnabled, isHidden). Dot-syntax still uses the property's declared name (self.enabled); the renamed method matters for explicit bracket calls like [obj isEnabled].
What does declaring a property readonly in a public @interface, then readwrite in a private class extension, accomplish? A. It's a syntax error -- a property can't be redeclared B. It lets external code read but never set the property, while the class's own internal implementation retains the ability to set it C. It makes the property atomic automatically D. It converts the property into a class-level property
B. It lets external code read but never set the property, while the class's own internal implementation retains the ability to set it — This is a standard, compiler-enforced pattern: the public header exposes only a getter (readonly), while a private class extension in the .m file redeclares the same property readwrite, giving only the class's own code access to the setter -- covered fully in the Class Extensions module.
@property
↓
Getter + Setter
↓
Ivar / Backing Storage
↓
Ownership Semantics