---
title: "Swift Value Types: Structs, Enums, and Copy-on-Write"
slug: "swift-value-types"
category: "coding"
tags: ["swift", "value-types", "structs", "copy-on-write", "performance"]
status: "stable"
last_updated: 2026-05-14
summary: "Why structs are the default in Swift, when classes are justified, and how copy-on-write keeps collections fast."
related: ["[[coding/swift]]", "[[coding/swift-actors]]", "[[coding/swift-protocols]]", "[[ios/core-data]]", "[[ios/swiftui]]"]
---

## Overview

Swift's type system separates value semantics (structs, enums, tuples) from reference semantics (classes, actors). Value types copy on assignment, giving each variable its own independent version of the data. This page explains when to use each, how the compiler implements copy-on-write for collections, and the narrow set of cases where a class is the right choice. See [[coding/swift]] for the broader rules, and [[coding/swift-actors]] for concurrency implications.

## Reach for `struct` first

Default to `struct` for every new type unless you have a specific reason to use `class`. Structs eliminate aliasing bugs: when you pass a struct to a function, the function receives a copy and cannot affect the caller's instance.

```swift
struct Money {
    let amount: Decimal
    let currency: String

    func adding(_ other: Money) -> Money {
        precondition(currency == other.currency)
        return Money(amount: amount + other.amount, currency: currency)
    }
}

var price = Money(amount: 9.99, currency: "USD")
var discounted = price          // independent copy
discounted = discounted.adding(Money(amount: -1.00, currency: "USD"))
// price is still 9.99; discounted is 8.99
```

A struct with all `let` properties is automatically `Sendable`, which means it can cross actor boundaries with no extra annotation. See [[coding/swift-actors]].

## Use `class` only when identity or lifecycle is part of the contract

Three situations justify a class:

1. The type must be observed as a reference. SwiftUI's `@Observable` and `ObservableObject` require reference types.
2. The type manages an external resource with a clear open/close lifecycle (a database connection, a file handle, a network session).
3. The type must interoperate with an Objective-C framework that requires a class hierarchy.

Outside these cases, modeling with a struct composes better and is safer across concurrency boundaries. For [[ios/swiftui]] view models using the Observation macro, `@Observable final class` is the correct pattern; it is the framework's design requirement, not a reason to prefer classes in general.

## Understand copy-on-write for standard collections

Swift's built-in `Array`, `Dictionary`, and `Set` are value types backed by a heap buffer. The copy is deferred: two variables sharing the same buffer incur no cost until one of them mutates.

```swift
var original = [1, 2, 3]
var copy = original     // no heap copy yet; both share the buffer

copy.append(4)          // copy-on-write triggers here; original is unaffected
```

Collections with a single owner never allocate a second buffer; the mutation is done in place. Multiple owners pay for a copy only on the first mutation. This makes passing arrays to functions cheap for read-only access.

Custom types can implement copy-on-write manually using `isKnownUniquelyReferenced`:

```swift
final class Buffer {
    var storage: [Int]
    init(_ storage: [Int]) { self.storage = storage }
    func copy() -> Buffer { Buffer(storage) }
}

struct LargeDataStore {
    private var _buffer = Buffer([])

    private mutating func ensureUniqueBuffer() {
        if !isKnownUniquelyReferenced(&_buffer) {
            _buffer = _buffer.copy()
        }
    }

    mutating func append(_ value: Int) {
        ensureUniqueBuffer()
        _buffer.storage.append(value)
    }
}
```

Implement copy-on-write for custom types only when profiling shows the copy cost is measurable.

## Prefer `enum` over Boolean flags for exclusive states

A Boolean flag that is meaningful only in context of another variable is a sign the state machine is implicit. Encode it as an `enum`.

```swift
// Bad: three Booleans can be in impossible combinations
var isLoading: Bool
var didFail: Bool
var hasData: Bool

// Better: all valid states are explicit
enum FeedState {
    case idle
    case loading
    case loaded([Post])
    case failed(Error)
}
```

An enum makes impossible states unrepresentable. Pattern-matching on it in a `switch` is exhaustive; the compiler warns about unhandled cases.

## Avoid large value types on hot paths

Value semantics is not free of runtime cost when the stack copy is large. Structs with dozens of stored properties and no copy-on-write buffer are copied in full on assignment. Measure before assuming a large struct is faster than a class. Instruments' Allocations and CPU profiler show copy costs.

In practice the threshold is high: a struct with 8 to 12 word-size fields fits in CPU registers or a small stack frame on ARM64, so copies are fast. Profile first.

## `mutating` methods signal intent; immutable values are safer by default

Mark methods that change stored properties `mutating`. A `let` binding refuses to call mutating methods, catching accidental mutation at compile time.

```swift
struct Counter {
    private(set) var value: Int = 0
    mutating func increment() { value += 1 }
}

let fixed = Counter()
// fixed.increment()  // error: cannot use mutating member on immutable value
var active = Counter()
active.increment()
```

Use `private(set)` to expose a read-only view of a property while keeping mutation internal. This is cleaner than a separate read-only computed property.

## Related

- [[coding/swift]]
- [[coding/swift-actors]]
- [[coding/swift-protocols]]
- [[coding/swift-optionals]]
- [[ios/swiftui]]
- [[ios/core-data]]
