When learning Swift, we often hear:
struct is a Value Type, while class is a Reference Type.
That statement is correct, but it doesn't explain what actually happens when we copy, mutate, or share data.
In real-world iOS development, understanding Value Semantics, Reference Semantics, Copy-on-Write (CoW), and storage is much more useful than simply remembering "struct = stack, class = heap".
Let's break it down.
1. Value Semantics
A type has Value Semantics when copying it creates an independent value.
var a = 10
var b = a
b = 20
print(a) // 10
print(b) // 20
Changing b doesn't affect a.
The same applies to a struct:
struct User {
var name: String
}
var user1 = User(name: "Alice")
var user2 = user1
user2.name = "Bob"
print(user1.name) // Alice
print(user2.name) // Bob
Conceptually:
user1 → User("Alice")
user2 → User("Bob")
The important idea is:
A copy behaves as an independent value.
2. Reference Semantics
A class has Reference Semantics.
final class User {
var name: String
init(name: String) {
self.name = name
}
}
let user1 = User(name: "Alice")
let user2 = user1
user2.name = "Bob"
-------------------
Now:
print(user1.name) // Bob
print(user2.name) // Bob
-------------------
Why?
Because `user1` and `user2` reference the same object:
user1 ─────┐
▼
User Object
name = "Bob"
▲
│
user2 ─────┘
Copying a reference type copies the reference, not the object.
This is useful when we actually want shared identity and shared state—for example, a ViewModel, service, coordinator, or resource owner.
3. Don't Think struct = Stack and class = Heap
A common explanation is:
struct → Stack
class → Heap
This is too simplistic.
Stack and Heap describe storage implementation, while Value and Reference Semantics describe behavior.
For example:
struct Point {
let x: Int
let y: Int
}
A small, fixed-size value like this can be stored inline. If it's a local variable, that representation may be on the stack, but the compiler is free to optimize it differently.
On the other hand:
struct RingtoneStore {
var ringtones: [Ringtone]
}
Array can contain 100,000 elements, so its data requires dynamic storage.
Conceptually:
RingtoneStore
┌─────────────────────┐
│ Array representation│
└──────────┬──────────┘
│
▼
Backing Storage
[Ringtone ...]
The Array value is still a Value Type, even though its backing storage is dynamically allocated and commonly lives on the heap.
So the better question is not:
"Is this struct on the Stack or Heap?"
but:
"How is this value represented and where is its backing storage?"
4. Copy-on-Write
This becomes especially interesting with large collections.
Suppose we have 100,000 ringtones:
let ringtones = [Ringtone](...)
Now:
let a = ringtones
let b = a
Does Swift immediately copy all 100,000 elements?
No.
Array uses Copy-on-Write (CoW).
Conceptually:
a ─────┐
├──> Shared Storage
b ─────┘
There are two Array values, but they can temporarily share the same backing storage.
If we only read:
print(b[0])
there is no reason to copy the storage.
This makes copying a large Array relatively cheap.
5. What Happens When We Mutate?
Now:
var a = [1, 2, 3]
var b = a
b.append(4)
Before mutation:
a ─────┐
├──> Storage A
b ─────┘
[1, 2, 3]
If b modified Storage A directly, a would also change. That would violate Value Semantics.
So Swift separates the storage:
a ─────────> Storage A
[1, 2, 3]
b ─────────> Storage B
[1, 2, 3, 4]
This is Copy-on-Write.
The important optimization is:
Don't copy until mutation actually requires it.
6. What Does "Uniquely Referenced" Mean?
CoW doesn't mean every mutation requires an O(n) copy.
Consider:
var a = [1, 2, 3]
a.append(4)
If a is the only value using its backing storage:
a ─────> Storage A
the storage is uniquely referenced.
Swift can mutate it directly.
But:
var a = [1, 2, 3]
var b = a
gives:
a ─────┐
├──> Storage A
b ─────┘
The storage is shared.
If b mutates, CoW may need to create a new storage.
So the simplified model is:
Mutation
↓
Is storage shared?
│
├── No → mutate in place
│
└── Yes → copy → mutate
This is the key to understanding the performance of Swift collections.
7. A Struct Can Still Contain a Class
Here's an important edge case:
final class AudioPlayer {
var volume = 50
}
struct Ringtone {
var name: String
var player: AudioPlayer
}
Although Ringtone is a struct, player is a reference to a class instance.
After:
let ringtone2 = ringtone1
we can have:
ringtone1 ──┐
│
▼
AudioPlayer
volume = 50
▲
│
ringtone2 ──┘
Therefore:
ringtone2.player.volume = 100
can also be observed through:
ringtone1.player.volume
because both structs share the same `AudioPlayer`.
This gives us an important rule:
A struct provides Value Semantics at its own level, but it does not automatically make every property deeply independent.
This is why exposing mutable reference-type state directly from a value type should be done carefully.
8. Value Semantics and Concurrency
Value Semantics are also useful in concurrent code because they reduce shared mutable state.
For example:
struct UserState {
var name: String
var age: Int
}
If two tasks work with independent copies:
Task A → UserState A
Task B → UserState B
then mutation in Task A doesn't change Task B's value.
This significantly reduces opportunities for Data Races.
However, it's important to be precise:
structdoes not automatically mean thread-safe.
A struct can still contain a mutable class reference or access external shared state.
So a better statement is:
Value Semantics reduce shared mutable state, which makes concurrent code easier to reason about and reduces potential data races.
Conclusion
The most useful mental model is not:
struct = Stack
class = Heap
Instead:
Value Type
↓
Value Semantics
↓
Independent state
↓
CoW can optimize storage sharing
and:
Reference Type
↓
Reference Semantics
↓
Shared identity
↓
Shared mutable state
↓
ARC manages lifetime
And when performance is involved:
Array copy
↓
Share backing storage
↓
Read → no copy
↓
Mutation
↓
Is storage unique?
├── Yes → mutate in place
└── No → Copy-on-Write
Once you understand these relationships, concepts like Swift Concurrency, Sendable, ARC, data races, and collection performance become much easier to reason about.
The key takeaway is simple:
Value vs Reference Semantics describe how data behaves. Stack vs Heap describes how data may be stored. Don't confuse the two.
Top comments (0)