NSCopying and NSMutableCopying
Adopt NSCopying and NSMutableCopying in a custom class, implement copyWithZone: and mutableCopyWithZone: correctly, reason about deep versus shallow copying at every level of an object graph, and recognize the classic bug where a nested mutable object is not itself copied.
The 'Copy Semantics' overview lesson introduces copy and mutableCopy from the caller's side: send copy to an NSString and you get back an immutable snapshot, send mutableCopy and you get a separate NSMutableString you can edit without disturbing the original. This lesson is the deep, implementer's-side companion to that overview — it answers the question that overview leaves open: how does copy actually know how to duplicate a custom object of a class you wrote yourself? The answer is two protocols, NSCopying and NSMutableCopying, and two methods you implement yourself, copyWithZone: and mutableCopyWithZone:. Get them right and your custom objects behave exactly like Foundation's own value-like classes when copied. Get them wrong — especially around nested mutable objects — and you get one of the most common and hardest-to-track-down bugs in Objective-C: a 'copy' that silently still shares state with the original.
Why copy Needs a Protocol at All
When you send copy to any object, Foundation does not have some universal, built-in algorithm for duplicating arbitrary objects — there is no generic way to know, just from an object's memory layout, which of its ivars represent value data that should be duplicated and which represent relationships to other objects that should be shared or duplicated differently. Instead, copy is implemented, for every class that supports it, as a plain message send: copy is actually defined (by NSObject) to call copyWithZone: on the receiver and return the result. This means copy only works on objects whose class adopts the NSCopying protocol and implements copyWithZone: — if you send copy to an object whose class does not implement it, you get a runtime crash (-copyWithZone: unrecognized selector), not a silent no-op.
Adopting NSCopying is a declaration in your class's @interface: @interface Point : NSObject <NSCopying>. Adopting the protocol without implementing copyWithZone: still compiles (with a warning) but crashes the moment anyone actually sends copy to an instance, so the protocol is really a promise backed by one required method. The zone parameter passed to copyWithZone: is a holdover from an old, now-effectively-obsolete memory zone allocation system; in modern Objective-C you almost always ignore its value and pass it straight through to allocWithZone:, which is exactly what the standard implementation pattern shown below does.
It is worth being precise about the vocabulary here, because the overview lesson and this lesson both use it: NSCopying is the protocol for producing an *immutable* copy (even if the object being copied was itself mutable), and NSMutableCopying is the separate protocol for producing a *mutable* copy. A class can adopt one, the other, or both — NSString adopts only NSCopying (you cannot mutableCopy a plain NSString unless you go through NSMutableString's own initializer), while NSMutableString adopts both, because sending it either copy or mutableCopy is meaningful and produces a different kind of result.
• copy is not magic — NSObject implements it as [self copyWithZone:NULL] (conceptually); your class must adopt NSCopying and implement copyWithZone: or copy crashes at runtime. • @interface MyClass : NSObject <NSCopying> is a promise, not an implementation — the required method must actually be written. • The zone parameter is a legacy memory-zone concept; modern code passes it straight through to allocWithZone: without inspecting it. • NSCopying produces an immutable result; NSMutableCopying (a separate protocol, mutableCopyWithZone:) produces a mutable result. • A class can adopt either protocol, both, or neither — adopt exactly the ones that make sense for that class's design.
Implementing copyWithZone: Correctly
The canonical pattern for copyWithZone: is to allocate a new instance of the *receiver's actual runtime class* (not a hardcoded class name, so subclasses inherit correct copying behavior), initialize it, and copy each ivar across individually, choosing the right strategy per ivar: [[[self class] allocWithZone:zone] init...]. Using [self class] rather than writing the class name literally matters because it makes copyWithZone: behave correctly even when called on a subclass instance — if you hardcode the base class name, copying a subclass instance silently produces a base-class object instead of a subclass instance, quietly losing the subclass's identity and any extra state it adds.
For each ivar, the right strategy depends on what kind of value it holds. Primitive/value-type ivars (NSInteger, CGFloat, a C struct like CGPoint, a BOOL) are simply copied by value with a direct assignment — there is nothing to deep-copy, because the bits themselves are the entire value, and two NSInteger variables holding 5 are already completely independent. Object-type ivars that are themselves value-like (an NSString *, an NSArray *, another custom object that itself supports NSCopying) should be sent copy themselves, not just assigned by reference (newObject->_name = [_name copy];, not newObject->_name = _name;) — this is what actually produces a deep copy at that one level, ensuring the new object does not share that ivar's storage with the original. Object-type ivars that represent a relationship to something that should genuinely be shared (a delegate, a weak back-reference to a parent) are assigned directly, not copied, because copying them would be semantically wrong — you want the copy to point at the *same* delegate, not a duplicate of it.
Direct ivar access (newObject->_name = ...) rather than going through the property setter is the idiomatic choice inside copyWithZone:, for two reasons: it sidesteps any custom setter logic (like KVO notifications or validation) that might be inappropriate to fire during a low-level copy operation, and it avoids retain-cycle or self-not-fully-initialized subtleties that can arise from calling accessor methods before init has finished. It is not strictly mandatory — many real codebases do use the property setter inside copyWithZone: when the ivar is simple — but reaching for direct ivar assignment is the safer default to teach and to reach for when in doubt.
• Allocate with [[self class] allocWithZone:zone], never a hardcoded class name, so subclasses copy correctly. • Value-type ivars (NSInteger, CGFloat, structs, BOOL): copy by plain assignment — the bits are the whole value already. • Value-like object ivars (NSString, NSArray, a custom NSCopying-conforming object): send them copy, not a bare assignment, to get a deep copy at that level. • Relationship ivars meant to be shared (delegate, weak parent back-reference): assign directly, do not copy — copying would be semantically wrong. • Prefer direct ivar access (newObject->_ivar = ...) inside copyWithZone: over calling property setters, to avoid side effects and partial-init subtleties.
A Complete Custom NSCopying Implementation
Putting the pattern together: an immutable Point3D class holding three CGFloat coordinates and a NSString * label. All four ivars are value-like from the copy's perspective (three primitives, one immutable-safe string), so copyWithZone: copies each one directly or via copy, and returns a fully independent, fully initialized new instance. This example is deliberately simple so the pattern itself — allocate with [self class], initialize, assign each ivar with the right strategy, return — is unmistakable before the next example adds the complications of mutability and nested objects.
Notice that copyWithZone: here calls a custom designated initializer (initWithX:y:z:label:) rather than plain init, which is entirely normal and often the cleanest way to write it — as long as that initializer itself correctly sets every ivar, letting it do the work keeps copyWithZone: short and keeps the ivar-initialization logic in exactly one place instead of duplicated between the initializer and the copy method.
NSMutableCopying and a Mutable Subclass
mutableCopyWithZone: follows the identical shape as copyWithZone:, with one difference in intent: it must always return an instance of the *mutable* variant of the class, regardless of whether the receiver itself was mutable or immutable. This is exactly the pattern Foundation itself uses: sending mutableCopy to either an NSString or an NSMutableString always returns an NSMutableString, never a plain NSString — the mutability of the *result* is fixed by which method you called (copy → immutable result, mutableCopy → mutable result), independent of the mutability of the receiver you called it on.
The idiomatic Foundation-style design for a mutable/immutable pair is a class cluster: an immutable base class (Roster) adopting NSCopying, and a mutable subclass (MutableRoster) adopting NSMutableCopying (and typically re-adopting NSCopying too, since a mutable object still needs to answer copy with an independent immutable snapshot). copyWithZone: on MutableRoster should still return a plain, immutable Roster — copying a mutable object with copy freezes it, which is exactly the defensive-copying behavior the overview lesson relies on (an object storing _names = [names copy] is protected against the caller later mutating their own NSMutableArray out from under it, precisely because copy on an NSMutableArray returns a plain, independent NSArray).
Inside mutableCopyWithZone:, allocate an instance of the mutable subclass explicitly ([[MutableRoster allocWithZone:zone] init...]) rather than [[self class] allocWithZone:zone] — unlike copyWithZone:, you generally do want to hardcode the mutable class name here, because the whole point of mutableCopyWithZone: is to guarantee a mutable result no matter what class the receiver actually is within the cluster.
• mutableCopyWithZone: must always return the mutable variant of the class, regardless of the receiver's own mutability. • Foundation's own pattern: copy on NSMutableString or NSString both return NSString; mutableCopy on either both returns NSMutableString. • A typical design is a class cluster: an immutable base class + a mutable subclass, mirroring NSString/NSMutableString. • copyWithZone: on a mutable subclass should still return a plain immutable instance of the base class — 'copy freezes'. • mutableCopyWithZone: typically hardcodes the mutable subclass name (unlike copyWithZone:'s use of [self class]).
Object Graph Copying: Why the Top Level Isn't Enough
Real objects rarely hold only primitives and strings — a Roster might hold an NSArray of custom Player objects, and a naive copyWithZone: that only sends copy to the array itself produces a genuinely dangerous half-measure. [_players copy] does produce a brand-new, independent NSArray — the *array container* is copied — but NSArray's copy is itself shallow with respect to its contents: it copies the array (the list structure) but simply retains each element it already holds, it does not send copy to each Player inside. The result is two separate Roster objects with two separate NSArrays, but both arrays point at the exact same underlying Player objects — mutate one Player's score through either roster, and both rosters 'see' the change, because there was never actually two players, only two lists pointing at one.
A true deep copy of an object graph therefore requires every level to cooperate: Player itself must adopt NSCopying and implement its own correct copyWithZone:, and Roster's copyWithZone: must explicitly copy each element rather than relying on the array's shallow copy alone — typically by mapping over the array and sending copy to each Player: iterating _players and building a new array of [player copy] results, rather than simply writing [_players copy]. This is the core idea to internalize about 'deep' versus 'shallow' copying in Objective-C: there is no single global 'deep copy' operation that recursively duplicates an entire object graph for you automatically. Every class in the graph that needs to be truly duplicated, rather than shared, must adopt NSCopying and be explicitly asked to copy, one level at a time, by the container or containing object that holds it.
This has a practical design consequence: before deciding a copyWithZone: implementation is correct, ask specifically, for every object-type ivar, 'if the original is mutated after this copy, should the copy be affected?' If the honest answer is no for a nested collection of custom objects, a bare [collection copy] is not enough — you need the per-element copy loop, and every element class in that collection needs its own working NSCopying implementation, all the way down.
• [_players copy] copies the NSArray container but not its Player elements — NSArray's own copy is shallow with respect to element identity. • A truly deep copy requires each contained class (Player) to itself adopt NSCopying, and requires the container's copyWithZone: to explicitly copy each element. • There is no single automatic 'deep copy everything' operation in Objective-C — depth is achieved one explicit level at a time. • The right question when writing copyWithZone: for each object-type ivar: 'should mutating the original after copying still affect the copy?' — if no, that ivar needs an explicit per-element or per-object copy. • Object graph copying failures are a common source of bugs precisely because a shallow top-level copy compiles fine and often even seems to work until a shared nested object is mutated.
The copyWithZone: / mutableCopyWithZone: Pattern
Always allocate with [self class] inside copyWithZone: (not a hardcoded name) so subclasses copy as themselves. Send copy to every value-like object ivar; assign shared/relationship ivars directly without copying. For a collection of custom objects, loop and copy each element explicitly — the container's own copy is not enough. mutableCopyWithZone: follows the same shape but allocates the mutable subclass and is typically hardcoded to that class.
@interface MyClass : NSObject <NSCopying>
@property (nonatomic, copy, readonly) NSString *name;
@property (nonatomic, strong, readonly) NSArray<Item *> *items; // Item conforms to NSCopying
@end
@implementation MyClass
- (id)copyWithZone:(NSZone *)zone {
MyClass *copy = [[[self class] allocWithZone:zone] init];
if (copy) {
copy->_name = [_name copy]; // value-like: copy it
NSMutableArray *itemsCopy = [NSMutableArray arrayWithCapacity:_items.count];
for (Item *item in _items) {
[itemsCopy addObject:[item copy]]; // deep-copy each element
}
copy->_items = [itemsCopy copy]; // freeze back to immutable
}
return copy;
}
@endCode Examples
Point3D's copyWithZone: routes through the designated initializer rather than assigning ivars by hand: [[self class] allocWithZone:zone] creates a new instance of whatever the receiver's actual runtime class is, and initWithX:y:z:label: sets every ivar in one place. Because x, y, and z are CGFloat primitives, passing them as constructor arguments already copies their bit-values; the initializer's own _label = [label copy]; handles the one object-type ivar defensively, the same idiom used in any Objective-C initializer.
return [[[self class] allocWithZone:zone] initWithX:_x y:_y z:_z label:_label];
Roster adopts both protocols. Its copyWithZone: is a plain same-class copy. Its mutableCopyWithZone: hardcodes MutableRoster rather than using [self class], because the whole point of mutableCopy is to always hand back something mutable. MutableRoster overrides copyWithZone: to hardcode Roster instead — sending copy to a mutable object must still 'freeze' the result into an immutable snapshot, exactly mirroring how NSMutableArray's copy returns a plain NSArray.
return [[MutableRoster allocWithZone:zone] initWithNames:_names];
[_players copy] does create a genuinely new NSArray object, so copy->_players != original->_players is true — but NSArray's own copyWithZone: only retains each element, it does not send copy to them. Both Team instances' arrays therefore point at the very same Player objects. Mutating playerA after the 'copy' was made still shows up through the copy, because there was only ever one Player, referenced from two different arrays.
Common mistake: Assuming that copying a container (NSArray, NSDictionary, NSSet) automatically deep-copies its contents. It does not — container copy methods only duplicate the container, not the objects inside it.
This version loops over _players and sends copy to each individual Player, relying on Player having its own correct NSCopying implementation. The resulting playersCopy array holds entirely new Player instances, independent of the originals, and is itself frozen into an immutable NSArray before being stored. Now mutating playerA on the original Team has no effect on the copied Team's players — the object graph is genuinely, fully duplicated at both levels.
[playersCopy addObject:[player copy]];
@interface Point3D : NSObject <NSCopying>
@property (nonatomic, readonly) CGFloat x, y, z;
@property (nonatomic, copy, readonly) NSString *label;
- (instancetype)initWithX:(CGFloat)x y:(CGFloat)y z:(CGFloat)z label:(NSString *)label;
@end
@implementation Point3D
- (instancetype)initWithX:(CGFloat)x y:(CGFloat)y z:(CGFloat)z label:(NSString *)label {
self = [super init];
if (self) {
_x = x; _y = y; _z = z;
_label = [label copy];
}
return self;
}
- (id)copyWithZone:(NSZone *)zone {
// Primitives copy by value automatically as constructor arguments;
// the label is defensively re-copied by the initializer itself.
return [[[self class] allocWithZone:zone] initWithX:_x
y:_y
z:_z
label:_label];
}
@end@interface Roster : NSObject <NSCopying, NSMutableCopying>
@property (nonatomic, copy, readonly) NSArray<NSString *> *names;
- (instancetype)initWithNames:(NSArray<NSString *> *)names;
@end
@implementation Roster
- (instancetype)initWithNames:(NSArray<NSString *> *)names {
self = [super init];
if (self) {
_names = [names copy]; // defensive: independent of caller's array
}
return self;
}
- (id)copyWithZone:(NSZone *)zone {
// Copying an (already immutable) Roster can just return self safely,
// but writing it out explicitly keeps the pattern uniform:
return [[[self class] allocWithZone:zone] initWithNames:_names];
}
- (id)mutableCopyWithZone:(NSZone *)zone {
// Always produce the MUTABLE subclass, regardless of receiver class.
return [[MutableRoster allocWithZone:zone] initWithNames:_names];
}
@end
@interface MutableRoster : Roster
- (void)addName:(NSString *)name;
@end
@implementation MutableRoster {
NSMutableArray<NSString *> *_mutableNames;
}
- (instancetype)initWithNames:(NSArray<NSString *> *)names {
self = [super initWithNames:names];
if (self) {
_mutableNames = [names mutableCopy];
}
return self;
…@interface Team : NSObject <NSCopying>
@property (nonatomic, copy, readonly) NSArray<Player *> *players; // Player: mutable ivars, adopts NSCopying
@end
@implementation Team
- (id)copyWithZone:(NSZone *)zone {
Team *copy = [[[self class] allocWithZone:zone] init];
if (copy) {
copy->_players = [_players copy]; // NEW ARRAY, but SAME Player objects inside!
}
return copy;
}
@end
// ... elsewhere:
Team *original = [[Team alloc] initWithPlayers:@[playerA, playerB]];
Team *snapshot = [original copy];
playerA.score = 99; // mutates the Player object itself
NSLog(@"%ld", (long)snapshot.players.firstObject.score); // prints 99, not the old score!- (id)copyWithZone:(NSZone *)zone {
Team *copy = [[[self class] allocWithZone:zone] init];
if (copy) {
NSMutableArray<Player *> *playersCopy =
[NSMutableArray arrayWithCapacity:_players.count];
for (Player *player in _players) {
[playersCopy addObject:[player copy]]; // deep-copy EACH player
}
copy->_players = [playersCopy copy]; // freeze into an immutable NSArray
}
return copy;
}Where This Actually Bites in Production Code
Model objects passed into undo-management APIs, cached into an NSCache, or stored as dictionary values are extremely common candidates for adopting NSCopying, precisely because NSDictionary itself requires its keys (and sometimes wants its values treated this way for safety) to be copyable and behave like independent values, not shared, mutable references. A recurring real bug in exactly this area: a class holds an NSMutableArray or NSMutableDictionary ivar, someone adds NSCopying to the class for use as a dictionary key or a cache entry, writes copyWithZone: quickly, forgets that one mutable ivar, and ships it. The bug then only surfaces much later, intermittently, when two seemingly-unrelated parts of the app both hold what they each believe is their own private copy of an object, and a mutation in one quietly corrupts the other — exactly the kind of bug that is easy to write and painful to debug, which is why getting copyWithZone: right, ivar by ivar, the first time is worth the extra care.
• (id)copyWithZone:(NSZone *)zone { Document *copy = [[[self class] allocWithZone:zone] init]; copy->_tags = _tags; // NSMutableArray *_tags — just aliased, not copied! return copy; } - (id)copyWithZone:(NSZone *)zone { Document *copy = [[[self class] allocWithZone:zone] init]; copy->_tags = [_tags mutableCopy]; // independent mutable array return copy; }
The buggy version assigns _tags (an NSMutableArray ivar) by plain reference instead of copying it. The result 'looks' like a copy — it's a distinct Document object — but both the original and the copy share the exact same NSMutableArray instance. Adding or removing a tag through either object silently mutates the other's tags too, which is exactly the shallow-copy bug this lesson warns about: forgetting to copy a nested mutable object means the 'copy' still shares live, mutable state with the original.
What actually happens when you send copy to an object whose class does not adopt NSCopying? A. Foundation silently returns the same object unchanged B. It performs a generic byte-for-byte memory copy automatically C. It crashes at runtime with an unrecognized-selector error, because copy relies on copyWithZone: being implemented D. It is a compile-time error
C. It crashes at runtime with an unrecognized-selector error, because copy relies on copyWithZone: being implemented — NSObject's copy is implemented in terms of copyWithZone:. If the class hasn't adopted NSCopying and implemented that method, sending copy produces a runtime 'unrecognized selector' crash, not a compile-time error and not a silent no-op.
Inside copyWithZone:, why allocate with [[self class] allocWithZone:zone] rather than a hardcoded class name? A. It is required by the NSCopying protocol's method signature B. It makes the code run faster C. So that copying an instance of a subclass produces a new instance of that same subclass, not the base class D. There is no difference; either form works identically in all cases
C. So that copying an instance of a subclass produces a new instance of that same subclass, not the base class — [self class] evaluates to the receiver's actual runtime class. Hardcoding the base class name would silently downgrade a copied subclass instance into a base-class instance, losing any extra subclass state and identity.
What must mutableCopyWithZone: always return, regardless of whether the receiver itself is mutable or immutable? A. The exact same object, retained B. An instance of the mutable variant of the class C. An instance of the immutable base class D. nil, since mutableCopy is only valid on already-mutable objects
B. An instance of the mutable variant of the class — mutableCopy's contract is that the result is always mutable. Foundation's own classes follow this: NSString.mutableCopy and NSMutableString.mutableCopy both return an NSMutableString, never a plain NSString.
A Team object has an NSArray<Player *> *players ivar, and copyWithZone: does copy->_players = [_players copy];. Player also adopts NSCopying but is never explicitly asked to copy. What is the result? A. A fully independent deep copy of the entire object graph, because NSArray's copy recursively copies its elements B. A new Team with a new NSArray, but the array still references the exact same Player objects as the original — mutating a Player affects both Teams C. A compiler error, since NSArray cannot be copied without also copying its elements D. The copy is identical to the original object (copy returns self)
B. A new Team with a new NSArray, but the array still references the exact same Player objects as the original — mutating a Player affects both Teams — NSArray's own copyWithZone: duplicates the array container but only retains its elements rather than copying them. True deep copying of the object graph requires the containing class to explicitly loop over the array and send copy to each element.
A class has an NSMutableDictionary ivar that represents genuinely shared, live state that both the original and the copy are intended to observe together (not a value to duplicate). What is the correct strategy for that ivar inside copyWithZone:? A. Always send it copy, no exceptions, since copyWithZone: must deep-copy every object ivar B. Assign it directly without copying, since it represents an intentionally shared relationship, not an independent value C. Leave it nil in the copy D. It cannot be an ivar of a class that adopts NSCopying
B. Assign it directly without copying, since it represents an intentionally shared relationship, not an independent value — Not every ivar should be copied — relationship-style ivars meant to be shared (a delegate, a shared cache, a back-reference) should be assigned directly. Blindly copying every object ivar is itself a design mistake, just a different one from forgetting to copy a value-like ivar.
Write a class Invoice with ivars: an NSInteger invoiceNumber, an NSString *customerName, and an NSMutableArray<LineItem *> *lineItems (where LineItem is a separate mutable class that itself adopts NSCopying). Implement copyWithZone: so that copying an Invoice produces a fully independent object graph: the invoiceNumber is copied by value, the customerName is defensively copied, and every LineItem inside lineItems is individually copied rather than shared.
• (id)copyWithZone:(NSZone *)zone { Invoice *copy = [[[self class] allocWithZone:zone] init]; if (copy) { copy->_invoiceNumber = _invoiceNumber; copy->_customerName = [_customerName copy]; NSMutableArray<LineItem *> *itemsCopy = [NSMutableArray arrayWithCapacity:_lineItems.count]; for (LineItem *item in _lineItems) { [itemsCopy addObject:[item copy]]; } copy->_lineItems = itemsCopy; } return copy; }
Follow the Team/Player fix pattern: allocate with [[self class] allocWithZone:zone], copy the primitive directly, send copy to the string, and build a new mutable array by looping over lineItems and sending copy to each LineItem before storing the result.