Swift Value Types vs Classes: Why Structs Are the Default Choice

Swift encourages you to use struct for most data models because value types are easier to reason about, safer to share, and usually simpler to test. This article explains what that rule means in practice, why it exists, and how to decide when a class is still the better choice.

Quick answer: Prefer struct when the type represents a piece of data and you want copies to be independent. Use class only when you need shared mutable identity, inheritance, or reference-based behavior.

Difficulty: Beginner

Helpful to know first: You'll understand this better if you know basic Swift syntax, variables and constants, and how functions pass values around.

1. What Is Prefer Value Types (structs) over Classes?

This rule of thumb means that in Swift, the default type for most custom data should be a struct rather than a class. A struct is a value type, so assigning it, passing it into a function, or returning it usually creates an independent value.

Swift’s design strongly favors value types because they reduce accidental mutation and make code easier to predict.

2. Why Prefer Value Types Matters

This choice affects correctness, readability, and how much mental overhead your code creates. If a type is a struct, you can usually reason about it by looking at the code in front of you. If it is a class, changes anywhere that hold a reference can affect the same instance.

That matters in real projects because many bugs come from unexpected shared state. With value types, you get fewer surprises when:

Swift also applies performance optimizations such as copy-on-write to common value types, so using structs is often both safer and efficient.

3. Basic Syntax or Core Idea

Defining a struct

A struct is declared with the struct keyword. Here is a simple model representing a user profile.

struct UserProfile {
    var name: String
    var age: Int
}

This type stores data and can be copied like any other value. When you assign one profile to another variable, the new variable gets its own copy.

Comparing with a class

A class uses the class keyword and stores a reference to a shared instance.

class UserAccount {
    var name: String
    var age: Int

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

The syntax looks similar, but the behavior is different when values are copied or shared.

4. Step-by-Step Examples

Example 1: Copying a struct creates an independent value

In this example, changing one variable does not affect the other because each variable has its own copy of the value.

struct Point {
    var x: Double
    var y: Double
}

var a = Point(x: 10, y: 20)
var b = a
b.x = 99

print(a.x)
print(b.x)

The output shows that a and b are independent after the assignment.

Example 2: Copying a class shares the same instance

Here, both variables point to the same object, so a change through one variable is visible through the other.

class PointRef {
    var x: Double
    var y: Double

    init(x: Double, y: Double) {
        self.x = x
        self.y = y
    }
}

var first = PointRef(x: 10, y: 20)
var second = first
second.x = 99

print(first.x)
print(second.x)

This behavior is useful for shared state, but it is also where accidental coupling begins.

Example 3: Passing a struct into a function

Functions that take value types usually work with a copy, so the original value stays unchanged unless you explicitly return a new one or use inout.

struct Settings {
    var volume: Int
}

func mute(_ settings: Settings) {
    var copy = settings
    copy.volume = 0
    print(copy.volume)
}

let original = Settings(volume: 50)
mute(original)
print(original.volume)

The function changes only its local copy, which makes side effects easier to control.

Example 4: A struct with computed behavior

Value types are not just passive containers. They can also include methods and computed properties.

struct Rectangle {
    var width: Double
    var height: Double

    var area: Double {
        width * height
    }

    func scaled(by factor: Double) -> Rectangle {
        Rectangle(width: width * factor, height: height * factor)
    }
}

This is a common pattern in Swift: keep the data as a struct and let methods produce new values when needed.

5. Practical Use Cases

Using struct is usually the right choice for types like these:

Classes are still useful for things that represent a single shared entity or controller-like object, such as a cache, session manager, or mutable service with identity.

6. Common Mistakes

Mistake 1: Using a class when independent copies are expected

Beginners often choose a class because it feels familiar, then get surprised when changing one variable changes another. This is the most common reason Swift recommends value types first.

Problem: Both variables reference the same object, so a change in one place leaks into the other place.

class Theme {
    var name: String

    init(name: String) {
        self.name = name
    }
}

let light = Theme(name: "Light")
let copy = light
copy.name = "Dark"

Fix: Use a struct when each variable should represent its own value.

struct Theme {
    var name: String
}

var light = Theme(name: "Light")
var copy = light
copy.name = "Dark"

The struct version works better because each variable gets its own independent value.

Mistake 2: Expecting class identity behavior from a struct

Some code needs to check whether two variables refer to the exact same instance. That works naturally with classes, but not with structs, because structs do not have identity.

Problem: Using === with a struct produces a compile-time error because identity comparison only applies to class instances.

struct Book {
    var title: String
}

let a = Book(title: "Swift Basics")
let b = a

if a === b {
    print("Same instance")
}

Fix: Compare values with == by making the struct conform to Equatable when needed.

struct Book: Equatable {
    var title: String
}

let a = Book(title: "Swift Basics")
let b = a

if a == b {
    print("Equal values")
}

The corrected version compares content instead of identity, which matches value semantics.

Mistake 3: Mutating a struct through a constant

A common confusion is that a let-bound struct cannot have its stored properties changed. This often appears as a compiler message about using a mutating member on an immutable value.

Problem: The constant prevents mutation of the struct value, so Swift rejects property changes.

struct Counter {
    var value: Int
}

let counter = Counter(value: 1)
counter.value = 2

Fix: Use var when the value itself must change, or create a new instance if you want to keep the original constant.

var counter = Counter(value: 1)
counter.value = 2

This works because the variable can now hold an updated value.

7. Best Practices

Practice 1: Default to structs for data that has no identity

If your type just describes something, use a struct first. You can always move to a class later if the design truly needs shared identity.

struct InvoiceLine {
    var item: String
    var price: Double
}

This keeps the model simple and avoids lifecycle concerns that do not add value.

Practice 2: Use methods that return new values instead of mutating shared state

Returning a new struct often makes behavior easier to test and reuse.

struct SearchFilter {
    var query: String
    var includeArchived: Bool

    func withArchived() -> SearchFilter {
        SearchFilter(query: query, includeArchived: true)
    }
}

This approach preserves the original value and makes the transformation explicit.

Practice 3: Use classes only when identity is important

Choose a class when multiple parts of the app must observe the same object or when the object lifecycle itself matters.

final class SessionStore {
    var token: String?
}

Marking the class final is often a good idea when you do not need inheritance, because it keeps the design focused and avoids subclassing complexity.

8. Limitations and Edge Cases

Note: Value types do not magically prevent all shared state. If a struct stores a reference to a class, the class instance can still be shared.

9. Practical Mini Project

Here is a small, complete example that models a shopping cart as a value type. This is a good fit because each cart snapshot should be independent.

struct CartItem {
    var name: String
    var price: Double
}

struct ShoppingCart {
    var items: [CartItem] = []

    var total: Double {
        items.reduce(0) { $0 + $1.price }
    }

    mutating func add(_ item: CartItem) {
        items.append(item)
    }
}

var cartA = ShoppingCart()
cartA.add(CartItem(name: "Notebook", price: 4.99))
cartA.add(CartItem(name: "Pen", price: 1.49))

var cartB = cartA
cartB.add(CartItem(name: "Sticker", price: 0.99))

print(cartA.total)
print(cartB.total)

This example shows the benefit of structs clearly: copying a cart gives you an independent snapshot that can diverge safely.

10. Key Points

11. Practice Exercise

Try this exercise to check your understanding of when to prefer a value type.

Expected output: The original profile keeps its old display name, and the updated copy has the new one.

Hint: The method should return a new Profile instead of changing shared state.

Solution:

struct Profile {
    var username: String
    var displayName: String

    func renamed(to newDisplayName: String) -> Profile {
        Profile(username: username, displayName: newDisplayName)
    }
}

let original = Profile(username: "alex", displayName: "Alex")
let updated = original.renamed(to: "Alex Johnson")

print(original.displayName)
print(updated.displayName)

12. Final Summary

In Swift, preferring value types means choosing struct first for most custom data. This default gives you clearer ownership, fewer accidental side effects, and code that is easier to test and reason about.

Use classes when you genuinely need shared identity, inheritance, or object lifecycle behavior. If you are unsure, start with a struct and only switch to a class when the design clearly requires reference semantics.

That habit aligns well with Swift’s language design and usually leads to simpler, safer code. As you continue learning, compare this rule with Equatable, mutating methods, and the differences between value and reference semantics in more depth.