Structs vs Classes in Swift: Understanding Value and Reference Types


Imagine Remy from Ratatouille writes down his secret soup recipe on a card. If you photocopy that card, you get your own independent copy — scribble on it all you want, and Remy’s original stays untouched. But if there’s only one master recipe book in the kitchen and everyone shares it, then any chef who changes a step changes it for everyone. That’s the core difference between structs (the photocopy) and classes (the shared book) in Swift.

In this guide, you’ll learn how to define structs and classes, understand value types vs reference types, see how class inheritance works, and know when to choose each one. We won’t cover protocols or memory management — those have their own dedicated posts.

What You’ll Learn

What Are Structs and Classes?

Both structs and classes let you group related data and behavior into a single custom type. You can give them properties (data) and methods (actions), and use them to model anything in your app — a movie, a character, a recipe.

The key difference is how copies work:

  • A struct is a value type. When you copy it, you get an independent duplicate. Changing the copy doesn’t affect the original.
  • A class is a reference type. When you copy it, both variables point to the same object in memory. Changing one affects the other.

This single distinction drives most of the decisions around when to use each.

Defining a Struct and a Class

Structs

Define a struct with the struct keyword. Swift automatically generates a memberwise initializer — a free init that takes each property as a parameter:

struct Movie {
    var title: String
    var year: Int
    var rating: Double
}

let coco = Movie(title: "Coco", year: 2017, rating: 8.4)
print("\(coco.title) (\(coco.year)) — \(coco.rating)/10")
Coco (2017) — 8.4/10

You didn’t write an init — Swift created one automatically because Movie is a struct. The memberwise initializer includes one parameter per stored property, in the order they’re declared.

Classes

Define a class with the class keyword. Classes do not get a free memberwise initializer, so you must write your own:

class Director {
    var name: String
    var movieCount: Int

    init(name: String, movieCount: Int) {
        self.name = name
        self.movieCount = movieCount
    }
}

let pete = Director(name: "Pete Docter", movieCount: 4)
print("\(pete.name) directed \(pete.movieCount) films")
Pete Docter directed 4 films

The init method runs when you create a new instance. Inside init, self.name refers to the property, while name refers to the parameter.

Tip: If you want a struct-like memberwise initializer for a class, you have to write it yourself. This is by design — classes often need more complex initialization logic.

Value Types vs Reference Types

This is the most important section in this post. Understanding this difference is essential to writing correct Swift code.

Structs Copy Independently

When you assign a struct to a new variable or pass it to a function, Swift creates a full, independent copy:

var original = Movie(title: "Up", year: 2009, rating: 8.3)
var copy = original

copy.title = "Inside Out"
copy.year = 2015

print("Original: \(original.title) (\(original.year))")
print("Copy: \(copy.title) (\(copy.year))")
Original: Up (2009)
Copy: Inside Out (2015)

Changing copy didn’t affect original. They’re completely separate — like two independent photocopies of Remy’s recipe card.

Classes Share the Same Instance

When you assign a class instance to a new variable, both variables point to the same object in memory:

let director1 = Director(name: "Brad Bird", movieCount: 2)
let director2 = director1

director2.movieCount = 3

print("Director 1: \(director1.movieCount) films")
print("Director 2: \(director2.movieCount) films")
Director 1: 3 films
Director 2: 3 films

Both director1 and director2 refer to the same Director object. When we changed movieCount through director2, the change was visible through director1 too — because there’s only one object. It’s like two chefs looking at the same master recipe book.

Warning: Shared mutable state through class references is a common source of bugs. When multiple parts of your code can change the same object, unexpected mutations can happen. This is one reason Swift encourages structs by default.

Identity vs Equality

Because class instances are shared, Swift gives you the identity operator (===) to check if two variables point to the exact same object:

let a = Director(name: "Andrew Stanton", movieCount: 3)
let b = a
let c = Director(name: "Andrew Stanton", movieCount: 3)

print(a === b)  // Same object
print(a === c)  // Different objects with same data
true
false

Structs don’t have === because each copy is always a distinct value — there’s no concept of shared identity.

Inheritance (Classes Only)

Classes support inheritance — a class can build on another class, inheriting all its properties and methods. The new class is called a subclass, and the class it builds on is the superclass:

class Film {
    var title: String
    var year: Int

    init(title: String, year: Int) {
        self.title = title
        self.year = year
    }

    func summary() -> String {
        "\(title) (\(year))"
    }
}

class PixarFilm: Film {
    var director: String

    init(title: String, year: Int, director: String) {
        self.director = director
        super.init(title: title, year: year)
    }

    override func summary() -> String {
        "\(title) (\(year)) — directed by \(director)"
    }
}

let soul = PixarFilm(title: "Soul", year: 2020, director: "Pete Docter")
print(soul.summary())
Soul (2020) — directed by Pete Docter

Key points about inheritance:

  • PixarFilm: Film means PixarFilm inherits from Film.
  • super.init(...) calls the superclass’s initializer.
  • override marks a method that replaces the superclass version.

Structs cannot inherit from other structs. If you need inheritance, you need a class. However, both structs and classes can conform to protocols, which is often a better choice.

Apple Docs: Structures and Classes — The Swift Programming Language

When to Choose Each

Apple’s official guidance is clear: default to structs. Use classes only when you specifically need reference semantics or class-only features.

Use a struct when:

  • You’re modeling simple data (a point, a movie, a recipe).
  • You want copies to be independent.
  • You don’t need inheritance.
  • The data is used on a single thread or passed between contexts.

Use a class when:

  • You need shared mutable state — multiple parts of your code should see the same object.
  • You need inheritance to build a hierarchy of types.
  • You need identity — checking if two variables point to the same instance.
  • You need interop with Objective-C APIs (which are class-based).

Note: In practice, the vast majority of custom types in a modern Swift app are structs. SwiftUI views, for example, are all structs.

Common Mistakes

Mutating a Struct Through let

Structs assigned to a let constant are completely immutable — you can’t change any of their properties:

// ❌ Won't compile — can't mutate a let struct
let movie = Movie(title: "Cars", year: 2006, rating: 7.1)
// movie.rating = 7.5  // Error: Cannot assign to property
// ✅ Use var if you need to modify the struct
var movie = Movie(title: "Cars", year: 2006, rating: 7.1)
movie.rating = 7.5
print("\(movie.title): \(movie.rating)/10")
Cars: 7.5/10

With classes, let only prevents you from reassigning the variable to a different object — you can still modify the object’s properties. With structs, let locks everything down.

Unexpected Shared State with Classes

This is the flip side of reference semantics. If you expect independent copies but use a class, mutations leak across references:

// ❌ Unexpected shared state
class Recipe {
    var name: String
    var servings: Int

    init(name: String, servings: Int) {
        self.name = name
        self.servings = servings
    }
}

let remyRecipe = Recipe(name: "Ratatouille", servings: 4)
let linguiniRecipe = remyRecipe  // Shared reference!
linguiniRecipe.servings = 2

print("Remy's servings: \(remyRecipe.servings)")
// Prints 2 — Remy's recipe was changed!
Remy's servings: 2
// ✅ Use a struct for independent copies
struct RecipeCard {
    var name: String
    var servings: Int
}

var remyCard = RecipeCard(name: "Ratatouille", servings: 4)
var linguiniCard = remyCard  // Independent copy
linguiniCard.servings = 2

print("Remy's servings: \(remyCard.servings)")
Remy's servings: 4

When you use a struct, Linguini gets his own copy of the recipe. Remy’s original stays safe — exactly like photocopying a recipe card.

What’s Next?

  • Structs and classes both group data and behavior into custom types.
  • Structs are value types — copies are independent.
  • Classes are reference types — copies share the same underlying object.
  • Only classes support inheritance; both structs and classes can use protocols.
  • Swift’s official guidance: default to structs unless you need reference semantics.

Now that you understand how to model data with structs and classes, it’s time to explore the different kinds of properties they can hold. Head to Properties in Swift to learn about stored properties, computed properties, lazy properties, and property observers.