Okay so. We need to talk about classes.
If you've been following along with this series, you're probably feeling pretty comfortable with structs at this point. You've got properties, methods, initializers, computed properties, access control, the whole thing. You feel good. You feel ready.
And then Swift says: "Great. Now meet classes."
And you go: "...are those just structs with a different name?"
And Swift goes: "No."
And you go: "They look exactly like structs."
And Swift goes: "They are not exactly like structs. Sit down."
So. Let's sit down. 🍥
On the Surface, Classes Look Identical
Here's a class:
class AnimeCharacter {
var name: String
var powerLevel: Int
init(name: String, powerLevel: Int) {
self.name = name
self.powerLevel = powerLevel
}
func describe() {
print("\(name) — Power Level: \(powerLevel)")
}
}
And here's a struct doing the exact same thing:
struct AnimeCharacter {
var name: String
var powerLevel: Int
func describe() {
print("\(name) — Power Level: \(powerLevel)")
}
}
Spot the difference? One says class, one says struct. The struct doesn't need a custom init because Swift generates one automatically. Everything else looks basically the same.
So why do both exist? Why not just pick one and call it a day?
Because they behave completely differently in five important ways. And those five differences are not trivia — they're the reason you'll choose one over the other throughout your entire career as an iOS developer.
The Five Ways Classes Are Different From Structs
Let me walk through them one by one, because each one matters.
1. Classes Don't Get a Free Initializer
Remember how structs automatically generate a memberwise initializer? You define the properties and Swift quietly creates AnimeCharacter(name:powerLevel:) for you — no extra work required.
Classes don't get that.
With a class, you write the initializer yourself. Every time. Which at first sounds annoying, but there's a very good reason for it — and it connects to the next point.
2. Classes Can Inherit From Other Classes
This is the big one. The headline feature. The thing that makes classes genuinely different from structs rather than just structurally similar.
Inheritance means one class can be built on top of another class, automatically getting all its properties and methods as a starting point:
class Ninja {
var name: String
var village: String
init(name: String, village: String) {
self.name = name
self.village = village
}
func introduce() {
print("I am \(name) from \(village).")
}
}
class Hokage: Ninja {
var title: String
init(name: String, village: String, title: String) {
self.title = title
super.init(name: name, village: village)
}
func announce() {
print("I am \(title) \(name), leader of \(village)!")
}
}
Hokage inherits everything from Ninja — name, village, and introduce() — and adds its own stuff on top. You can create a Hokage and call introduce() on it even though Hokage never defined that method itself.
This is why classes don't get a free initializer. If they did, and you later added a property to Ninja, every Hokage initializer would silently break. Swift decided it's better to make you write your own, so you're always fully aware of what your initializer is doing and what it affects.
3. Copies of Classes Share the Same Data
This is the one that surprises people most, and it's genuinely important for understanding how SwiftUI works.
With a struct, every copy is independent:
var naruto = AnimeCharacterStruct(name: "Naruto", powerLevel: 9000)
var naruto2 = naruto
naruto2.powerLevel = 9001
print(naruto.powerLevel) // 9000 — unchanged
print(naruto2.powerLevel) // 9001 — only this one changed
With a class, copies point to the same underlying data:
var naruto = AnimeCharacterClass(name: "Naruto", powerLevel: 9000)
var naruto2 = naruto
naruto2.powerLevel = 9001
print(naruto.powerLevel) // 9001 — ALSO changed!
print(naruto2.powerLevel) // 9001
naruto and naruto2 aren't two separate characters — they're two names for the same character. Change one, you change both.
This sounds alarming, but it's actually exactly what you want for shared state in an app. If a user updates their profile name on one screen, you want every other screen showing that name to update automatically — not to be stuck showing the old version from their own private copy. Classes make that kind of shared, synchronized data easy.
4. Classes Have Deinitializers
When the very last reference to a class instance goes away, Swift can run a special function called a deinitializer — written as deinit:
class NinjaSchool {
var name: String
init(name: String) {
self.name = name
print("\(name) opened!")
}
deinit {
print("\(name) closed forever.")
}
}
Structs don't have this because each copy of a struct is independent — there's no "last copy" to track. Classes have it because when multiple things point to the same instance, Swift needs to know when truly nobody is using it anymore.
5. You Can Change Properties on a Constant Class
This one is subtle but important. With structs, a constant instance means nothing can change:
let naruto = AnimeCharacterStruct(name: "Naruto", powerLevel: 9000)
naruto.powerLevel = 9001 // ❌ can't change — it's a let
With classes, a constant instance just means you can't point it at a different instance — but the data inside can still change:
let naruto = AnimeCharacterClass(name: "Naruto", powerLevel: 9000)
naruto.powerLevel = 9001 // ✅ this is fine!
let naruto means "naruto always refers to this specific character." It doesn't mean "this character's properties can never change."
So When Do You Use a Class vs a Struct?
Most Swift developers default to structs. They're simpler, safer (because copies are independent), and Swift's standard library is built almost entirely on structs.
You reach for a class when you specifically want one of those five behaviors — most often the shared data one. In SwiftUI, your UI components are structs, but your data models that need to be shared across multiple views are classes. That's the pattern you'll see again and again as you build real apps.
The short version:
| Use a Struct when... | Use a Class when... |
|---|---|
| Each copy should be independent | Multiple things should share the same data |
| You want the free memberwise initializer | You need inheritance |
| You're building UI in SwiftUI | You're building data models in SwiftUI |
| Simple, contained data | Data that needs to be observed and shared |
The One Thing To Hold Onto
If everything above felt like a lot — and honestly, it is a lot for one sitting — the single most important thing to remember is this:
Structs give every copy its own data. Classes make all copies share the same data.
Everything else about classes (inheritance, deinit, constant mutability) flows from that fundamental difference. Once that clicks, the rest starts to make sense.
We'll go deeper on inheritance, deinitializers, and how all of this connects to SwiftUI in the next few articles. For now, just sit with the idea that structs and classes aren't interchangeable — they're different tools for different jobs, and knowing which one to reach for is one of the most important instincts you'll build as a Swift developer. 🌸
This article was written by me; AI was used to improve grammar and readability.
Top comments (12)
"Structs give every copy its own data. Classes make all copies share the same data" is the right mental model, and there is a nuance worth adding so nobody fears the copies: for the standard collections (Array, Dictionary, String), Swift implements this with copy-on-write. Assigning a struct-backed array to a new variable copies a reference under the hood, and the real copy only happens at the moment one of them mutates. So the semantics are "every copy is independent" while the physics are lazy, which is why passing big value types around is cheaper than the mental model suggests.
The let surprise with classes also generalizes into a rule that transfers to every language: let freezes the binding, not the object. A let class instance means "this variable will always point at that object," not "that object cannot change." It is the same distinction as const in JavaScript or final in Java. Value types are the special case where freezing the binding effectively freezes the data too, and once you see it that way, the behavior stops being surprising and becomes a definition.
The copy-on-write point is such a useful addition — because the mental model of "every copy is independent" is the right one to hold onto for reasoning about correctness, but "the physics are lazy" is what makes the performance story not actually scary. Those two things are true simultaneously and it's worth knowing both.
The "let freezes the binding, not the object" generalization is the cleaner version of the signpost analogy honestly — and you're right that it transfers directly. Once you've seen it that way, the Swift behavior stops being a quirk to memorize and becomes a specific instance of a broader rule that shows up in almost every language. The value types being the special case where binding and data happen to be the same thing is the inversion that makes the whole thing click. Really appreciate you adding this layer — it's exactly the kind of context that turns "I know how this works" into "I understand why this works." 🌸
Appreciate that. One last spot where the binding rule pays off: it explains the mutating keyword. A mutating method reassigns self, the whole value, which is exactly why the compiler rejects it on a let struct. Same freeze-the-binding rule, just surfacing in the method system instead of the variable. Once that clicks, "cannot use mutating member on immutable value" stops being a cryptic error and becomes the rule restating itself.
That's the connection I hadn't made explicit — mutating reassigning self under the hood is exactly why the freeze-the-binding rule shows up there too. It's not a separate rule about methods, it's the same rule expressing itself in a different context. Once you see it that way, "cannot use mutating member on immutable value" stops being a thing to memorize and becomes something you could have predicted from first principles. That's the kind of unification that makes a mental model actually useful rather than just a collection of facts. 🌸
"Predicted from first principles rather than memorized" is the whole reason it's worth chasing the single rule instead of collecting the special cases. That's the difference between knowing a language and being able to reason in it. Genuinely good thread, thanks for taking it this far with me.
"Knowing a language vs being able to reason in it" is exactly the distinction worth chasing, and it's what makes these threads more valuable than the articles alone honestly. The article gives you the map, the conversation finds out whether the map holds up under pressure. Really glad you pushed it this far, genuinely one of the better exchanges I've had on here.
Thanks for taking the time to write this article! ❤️ Swift's transition from structs to classes is one of those topics that seems simple at first, but you've highlighted why it becomes much more nuanced once reference semantics, identity, and shared mutable state enter the picture.
What stood out to me is that this isn't just about learning a new keyword—it's about changing the way a developer thinks. Structs encourage predictable, value-oriented design, while classes introduce identity, lifecycle, and the responsibility of managing shared state. That's a mental shift many beginners underestimate, and your article helps bridge that gap.
One thought that could make this even stronger is adding a section on decision-making rather than just differences. For example: "If you're building a model that represents immutable data, choose a struct. If multiple objects need to observe and mutate the same instance, a class may be the better fit." Real-world scenarios like networking models, SwiftUI state, or caching objects would help readers develop intuition instead of memorizing rules.
It could also be interesting to briefly connect this discussion with Swift's emphasis on protocol-oriented programming. Many experienced Swift developers start with structs by default and only reach for classes when identity or inheritance is genuinely required. Understanding why that philosophy exists is often more valuable than simply knowing the syntax.
Overall, this was a thoughtful read. Thanks again for sharing it and encouraging developers to think beyond the surface-level differences. Articles like this help people build stronger design instincts, not just better Swift syntax. 👏🏻🙂
Thank you for this—and those are genuinely good suggestions! 😊 The decision-making framework point especially—you're right that knowing the differences is step one, but knowing when to reach for each one is where the real instinct gets built. I deliberately kept this one focused on the "what's different" part because I didn't want to overload a single article, but the "how to choose" angle is a great candidate for a follow-up, maybe once protocols are covered since that's where the "structs by default" philosophy really starts to make sense.
The protocol-oriented programming point is one I'm looking forward to getting to in the series—it reframes a lot of the structs vs. classes conversation in a way that makes the Swift philosophy click rather than just feeling like a set of rules to memorize. Really appreciate the thoughtful read! 🌸
Nice article! I like how you focused on the practical differences instead of only explaining the theory. The examples were clear and easy to follow. 👏
Thank you! The theory only really lands when you can see it doing something concrete—glad the examples helped! 🌸
Thank you for sharing such a great article😄 . It was really helpful for understanding Swift classes more clearly.
Thank you for reading! Really glad it helped clarify things 😊🌸