← Back to list

Shallow Copy vs Deep Copy in Swift: What Every iOS Developer Should Understand

You assign an object to another variable.

Anu Agrawal · 2026-05-25 17:21 · 2 claps · 4.9 min read
#swift #ios-development #memory-management #internal-system #runtime
Open on Medium ↗
Wiki topics: BIZ · Business Strategy 📱 · Mobile Development

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:

  • Person object was duplicated
  • Address object 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:

*Copy-On-Write (COW)*

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