Okay. This one is going to feel weird at first. I'm going to warn you in advance so you don't think something is broken when you try it.
Here's what's going to happen: you're going to create a class, make a copy of it, change something in the copy, and then discover that the original changed too.
And you're going to say: "That's not how copying works."
And I'm going to say: "You're right. But for classes, that's exactly how it works. Sit down. Let's talk." ๐ฅ
The Moment That Breaks Your Brain
Let's create a simple class for a ninja character:
class Ninja {
var name: String
var powerLevel: Int
init(name: String, powerLevel: Int) {
self.name = name
self.powerLevel = powerLevel
}
}
Now let's create an instance, copy it, and change something in the copy:
var naruto = Ninja(name: "Naruto", powerLevel: 9000)
var naruto2 = naruto
naruto2.name = "Sasuke"
print(naruto.name) // "Sasuke" ๐ฑ
print(naruto2.name) // "Sasuke"
Wait. We changed naruto2, but naruto changed too?
Yes. That's not a bug. That's how classes work.
The Signpost Analogy
Here's the mental model that makes this click.
When you create a struct, Swift stores the actual data directly in the variable. Copying a struct is like photocopying a document โ you get two completely separate pieces of paper. Change one, the other is untouched.
When you create a class, Swift stores the data somewhere in memory and gives your variable a signpost pointing to that location. Copying a class doesn't copy the data โ it copies the signpost. Now you have two signposts pointing at the same piece of data.
var naruto = Ninja(name: "Naruto", powerLevel: 9000)
// naruto is a signpost pointing to data: {name: "Naruto", powerLevel: 9000}
var naruto2 = naruto
// naruto2 is a SECOND signpost pointing to the SAME data
naruto2.name = "Sasuke"
// You followed naruto2's signpost and changed the data it points to
// naruto's signpost points to the SAME data, so naruto.name is also "Sasuke"
There's only one piece of data. Both variables are just different ways of pointing at it.
Why Would Swift Do This On Purpose?
This isn't a mistake or an oversight. It's a deliberate design decision, and it's actually useful.
Think about a real iOS app. Say you have a user profile that shows up on a settings screen, a home screen, and a notification. If all three screens have their own independent copy of the user data, and the user changes their username โ now you have to update three separate copies. Miss one and you have inconsistent data showing in different parts of your app.
With a class, all three screens are looking at the same piece of data through their own signpost. Change the username once, and every screen that holds a reference to that user sees the updated value automatically. That's exactly what you want for shared state.
This is why Swift calls classes reference types โ you're passing around references (signposts) to data, not copies of the data itself. Structs are value types โ the value is the data, and copying creates a fresh independent copy.
So How Do You Make a True Copy?
Sometimes you genuinely do want a separate copy โ two independent ninjas that can change without affecting each other. Swift doesn't give you that automatically for classes, but you can create it yourself by writing a copy() method that creates a fresh instance with the same values:
class Ninja {
var name: String
var powerLevel: Int
init(name: String, powerLevel: Int) {
self.name = name
self.powerLevel = powerLevel
}
func copy() -> Ninja {
Ninja(name: name, powerLevel: powerLevel)
}
}
Now when you want a true independent copy, you call copy() explicitly:
var naruto = Ninja(name: "Naruto", powerLevel: 9000)
var naruto2 = naruto.copy()
naruto2.name = "Sasuke"
print(naruto.name) // "Naruto" โ unchanged โ
print(naruto2.name) // "Sasuke" โ only this one changed โ
Two signposts, two separate pieces of data. The copy() method creates a brand new Ninja instance in memory with the same starting values โ so now each variable has its own data to work with.
Choosing a Class Sends a Message
Here's something worth holding onto that goes beyond the technical mechanics:
When you choose to use a class instead of a struct, you're making a statement about how you expect the data to be used. You're saying: "I want multiple parts of my code to share and observe the same piece of data."
When you choose a struct, you're saying: "Each owner should get their own independent copy."
Most Swift developers lean toward structs by default precisely because the copy-is-a-copy behavior is simpler and easier to reason about. You reach for a class when shared, synchronized data is exactly what you need โ like a data model that needs to be observed and updated across multiple views in a SwiftUI app.
The signpost is a feature, not a bug. You just have to know it's there.
The One Thing To Hold Onto
Copying a class copies the signpost, not the data. Both copies end up pointing at the same underlying object, so changing one changes what the other sees too.
If you want a true independent copy, write a copy() method that creates a new instance. Swift won't do it for you automatically โ which is intentional, because it forces you to be explicit about something that matters. ๐ธ
This article was written by me; AI was used to improve grammar and readability.
Top comments (2)
Great explanation. The signpost analogy makes reference semantics much easier to visualize.
One thing Iโd add is that implementing copy() becomes much more complex once a class contains nested reference types. A shallow copy may still share internal objects, while a deep copy creates independent copies of the entire object graph. Thatโs where copying semantics become an architectural decision rather than just a convenience method. Nice article!
Good analogy. The tool that makes it concrete is the identity operator ===: naruto === naruto2 is true because they point at the same signpost, and after a real copy() it flips to false (== compares values, === compares identity). In practice the bug is rarely that obvious line, it is a shared instance passed into a function or held in an array that something mutates far from where you assigned it.