Shallow Copy vs Deep Copy in Swift: What Every iOS Developer Should Understand
You assign an object to another variable.

Shallow Copy vs Deep Copy in Swift: What Every iOS Developer Should Understand
You assign an object to another variable.
You modify the “copy.”
Then suddenly the original object also changes.
This is one of the most common sources of confusing bugs in iOS development.
And almost every time, the root cause is:
misunderstanding shallow copy vs deep copy.
Most explanations stop at:
Struct = copy
Class = reference
But the real story becomes much more interesting once objects start referencing other objects internally.
In this article, we’ll deeply understand:
- What shallow copy actually means
- What deep copy means internally
- How object graphs work
- Why shared mutable state becomes dangerous
- Why structs containing classes can still share data
- How Swift’s Copy-On-Write optimization relates to all this
Along the way, we’ll inspect actual memory addresses to verify behavior ourselves.
First, Let’s Clear Up A Common Misconception
You’ve probably heard this before:
Structs live on the stack.
Classes live on the heap.
This explanation is incomplete and often misleading.
The important distinction in Swift is NOT:
stack vs heap
The real distinction is:
value semantics vs reference semantics
Classes are reference types.
Class instances are heap allocated, and variables store references to those heap objects.
Structs are value types.
Their storage location depends on compiler/runtime optimization. Swift may:
- store them inline
- stack promote them
- heap allocate them
- or optimize copies away entirely
So instead of thinking:
Struct = stack
Class = heap
a much more accurate mental model is:
Struct = value semantics
Class = reference semantics
That distinction explains shallow copy and deep copy far better.
The Core Problem
Let’s start with two simple classes.
final class Address {
var city: String
init(city: String) {
self.city = city
}
func copy() -> Address {
Address(city: city)
}
}
final class Person {
var name: String
var address: Address
init(name: String, address: Address) {
self.name = name
self.address = address
}
}
Now create an object:
let address = Address(city: "Delhi")
let person1 = Person(
name: "Anu",
address: address
)
Conceptually, memory looks like this:
person1 ───► Person Object
│
├── name = "Anu"
│
└── address ───► Address Object
│
└── city = "Delhi"
This connected structure is called an:
Object Graph
Because objects are connected to other objects through references.
What Is Shallow Copy?
A shallow copy duplicates only the top-level object.
Nested references are still shared.
Let’s implement a shallow copy.
extension Person {
func shallowCopy() -> Person {
Person(
name: name,
address: address
)
}
}
Notice something important here:
address: address
We are NOT creating a new Address object.
We are copying only the reference to the same address object.
Memory After Shallow Copy
let person2 = person1.shallowCopy()
Now memory conceptually becomes:
person1 ───► Person A ───► Address A
person2 ───► Person B ───► SAME Address A
Important:
Personobject was duplicatedAddressobject was NOT duplicated- both persons still share same nested address object
Verifying With Memory Addresses
We can inspect heap object addresses directly.
print(
Unmanaged.passUnretained(person1.address).toOpaque()
)
print(
Unmanaged.passUnretained(person2.address).toOpaque()
)
Example output:
0x0000600001c0c000
0x0000600001c0c000
Same address.
That means: both variables reference the same heap object
Why Shallow Copy Becomes Dangerous
Now mutate:
person2.address.city = "Mumbai"
Then:
print(person1.address.city)
Output:
Mumbai
Even though we modified only person2.
Why?
Because both objects still share the same nested Address object.
Shared Mutable State
This is one of the biggest sources of complexity in software systems.
When multiple objects share the same mutable reference:
mutation in one place
can unexpectedly affect another part of the application
This is called:
Shared Mutable State
And this single idea explains many difficult software problems:
- race conditions
- unexpected UI updates
- threading bugs
- aliasing issues
- defensive copying complexity
- ownership problems
Modern language design increasingly tries to reduce this complexity.
What Is Deep Copy?
Deep copy duplicates the entire object graph.
That means:
- top-level object copied
- nested referenced objects also copied
- no shared mutable references remain
Implementing Deep Copy
extension Person {
func deepCopy() -> Person {
Person(
name: name,
address: address.copy()
)
}
}
Notice this line:
address.copy()
Now we create a completely new Address object.
Memory After Deep Copy
let person2 = person1.deepCopy()
Memory now becomes:
person1 ───► Person A ───► Address A
person2 ───► Person B ───► Address B
Everything is now independent.
Verifying Again With Memory Addresses
print(
Unmanaged.passUnretained(person1.address).toOpaque()
)
print(
Unmanaged.passUnretained(person2.address).toOpaque()
)
Example output:
0x0000600001c0c000
0x0000600001c0d000
Different addresses.
That means:
completely separate heap objects exist now
Mutation After Deep Copy
Now:
person2.address.city = “Mumbai”
will NOT affect:
person1.address.city
because the nested address object was also duplicated.
The Real Meaning Of “Copying The Object Graph”
When developers say:
“Deep copy duplicates the object graph”
what actually happens conceptually is:
The program recursively follows references
and recreates equivalent objects
at different memory locations.
Example:
Original Graph:
Person A ─► Address A
Copied Graph:
Person B ─► Address B
Same structure. Same values. Different identities.
Important Clarification
Deep copy copies:
values + structure
NOT:
memory addresses
Memory addresses must differ.
Otherwise objects are still shared.
Why Deep Copy Becomes Difficult In Real Systems
Real applications rarely contain only two objects.
A real object graph may look like this:
User
├── Address
├── Orders
│ ├── Product
│ ├── Payment
│ └── Shipping
└── Settings
Deep copying this graph may become:
- recursive
- memory expensive
- difficult with cyclic references
- slow for large systems
For example:
A ─► B
▲ │
└────┘
Naive recursive deep copy could infinitely recurse.
Real deep-copy systems often require:
- identity tracking
- visited-object maps
- graph reconstruction logic
This is one reason modern systems increasingly prefer:
- immutability
- value semantics
- Copy-On-Write
- structs where possible
Structs Reduce Many Of These Problems
Now compare with structs.
struct AddressStruct {
var city: String
}
struct PersonStruct {
var name: String
var address: AddressStruct
}
Now:
var p1 = PersonStruct(
name: "Anu",
address: AddressStruct(city: "Delhi")
)
var p2 = p1
When:
p2.address.city = “Mumbai”
p1 remains unchanged.
Because structs provide:
Value Semantics
Each copy becomes logically independent.
Important Clarification About Structs
Many developers incorrectly assume:
Struct automatically means deep copy.
That’s NOT always true.
Consider:
struct PersonStruct {
var address: Address
}
Even though PersonStruct is a value type, Address is still a class.
That means nested references can still be shared.
Struct guarantees only:
Top-level value semantics
Nested class references still preserve reference semantics unless:
- deep copied
- immutable
- or managed through Copy-On-Write
This distinction is extremely important.
Copy-On-Write: Swift’s Optimization Strategy
Swift collections like:
- Array
- String
- Dictionary
- Set
use a clever optimization called:
When assigned:
var arr1 = [1, 2, 3]
var arr2 = arr1
Swift does NOT immediately duplicate all array elements.
Initially, storage is shared internally.
Only when mutation happens:
arr2.append(4)
Swift creates separate storage.
This gives us:
Reference semantics internally
+
Value semantics externally
which provides both:
- memory efficiency
- safe value behavior
Final Thoughts
Shallow copy and deep copy are fundamentally about:
whether nested mutable references are shared or duplicated
Shallow copy:
Copies outer container
Shares nested references
Deep copy:
Copies entire object graph
Creates fully independent objects
And once applications become larger, understanding this distinction becomes extremely important for:
- memory behavior
- concurrency
- architecture
- debugging unexpected mutations
- ownership reasoning
Key Takeaway
The real danger is not sharing itself.
The real danger is:
shared mutable state
That single idea explains why:
- value semantics matter
- immutable architectures are popular
- Copy-On-Write exists
- and why Swift strongly encourages structs where possible.
메타데이터
- post_id
- 620fbae881ce
- slug
- shallow-copy-vs-deep-copy-in-swift-what-every-ios-developer-should-understand-620fbae881ce
- url
- https://medium.com/@anugoyal0210/shallow-copy-vs-deep-copy-in-swift-what-every-ios-developer-should-understand-620fbae881ce
- canonical_url
- https://medium.com/@anugoyal0210/shallow-copy-vs-deep-copy-in-swift-what-every-ios-developer-should-understand-620fbae881ce
- author_url
- https://medium.com/@anugoyal0210
- status
- ok
- fetched_at
- 2026-06-09 14:34:10