Swift Copy-on-Write Optimization Explained

Copy-on-write is one of the most important performance features in Swift. It lets value types behave like independent values while delaying expensive copying until a mutation actually happens.

Quick answer: Copy-on-write means Swift can share storage between copied values until one of them changes. At that point, Swift makes a real copy so each value keeps its own data.

Difficulty: Intermediate

Helpful to know first: You'll understand this better if you know basic Swift value types, how copying assignment works, and the difference between classes and structs.

1. What Is Copy-on-Write?

Copy-on-write, often shortened to CoW, is an optimization strategy used by Swift value types such as Array, String, Dictionary, and Set. When you copy one of these values, Swift does not immediately duplicate all of the underlying data.

At the language level, this is why assigning an array to a new variable is usually cheap, even when the array contains many elements.

2. Why Copy-on-Write Matters

Without this optimization, every copy of a large value would require duplicating all of its contents immediately. That would make common Swift code much slower and use more memory than necessary.

Copy-on-write matters because it gives you the best of both worlds:

This is especially valuable in code that passes arrays, strings, or dictionaries through many layers of a program.

3. Basic Syntax or Core Idea

There is no special syntax for using copy-on-write. You benefit from it automatically when working with Swift's standard value types. The core idea is easiest to see by assigning one value to another and then mutating one copy.

Simple assignment and mutation

The following example shows how a copied array can diverge after mutation:

var numbers = [1, 2, 3]
var moreNumbers = numbers

moreNumbers.append(4)

// numbers is still [1, 2, 3]
// moreNumbers is [1, 2, 3, 4]

Before the append, both variables can share storage. The append requires mutation, so Swift makes sure moreNumbers gets its own copy first.

Why this is still value semantics

Even though storage may be shared behind the scenes, the observable behavior is that changing one variable does not change the other. That is the key promise of Swift value types.

4. Step-by-Step Examples

Example 1: Arrays share storage until mutation

This example shows the most familiar copy-on-write behavior in Swift. Assignment is cheap, but the first write after a shared copy triggers duplication.

var a = [10, 20, 30]
var b = a

b[0] = 99

// a is [10, 20, 30]
// b is [99, 20, 30]

The mutation affects only b. Swift preserves independent results even though the values started out sharing data.

Example 2: String concatenation does not copy eagerly

String also uses copy-on-write. You can assign a string and change one copy later without affecting the other.

var message = "Hello"
var copy = message

copy += ", world"

// message is "Hello"
// copy is "Hello, world"

This is important because strings can contain a lot of data. Swift avoids making them expensive to copy when there is no reason to do so.

Example 3: Dictionary updates follow the same rule

Dictionary values behave the same way. A copied dictionary stays cheap until one copy is modified.

var original = ["name": "Ava", "role": "Admin"]
var updated = original

updated["role"] = "Editor"

// original is ["name": "Ava", "role": "Admin"]
// updated is ["name": "Ava", "role": "Editor"]

The important detail is that the mutation changes only the updated dictionary.

Example 4: A custom copy-on-write wrapper

You can build your own copy-on-write type when you need value semantics with internal sharing. Swift often uses a reference type as storage and a value type as the public wrapper.

final class BufferStorage {
    var bytes: [UInt8]

    init(bytes: [UInt8]) {
        self.bytes = bytes
    }

    func copy() -> BufferStorage {
        return BufferStorage(bytes: bytes)
    }
}

struct Buffer {
    private var storage: BufferStorage

    init(bytes: [UInt8]) {
        self.storage = BufferStorage(bytes: bytes)
    }

    mutating func append(_ byte: UInt8) {
        if !isKnownUniquelyReferenced(&storage) {
            storage = storage.copy()
        }
        storage.bytes.append(byte)
    }

    var bytes: [UInt8] {
        storage.bytes
    }
}

var first = Buffer(bytes: [1, 2])
var second = first

second.append(3)

This pattern gives Buffer value semantics while postponing duplication until it is truly needed.

5. Practical Use Cases

Copy-on-write is useful any time you want values to be easy to pass around without paying a copy cost up front.

A good rule is: if the data is often copied but only occasionally mutated, copy-on-write can be a strong fit.

6. Common Mistakes

Mistake 1: Assuming every assignment makes a deep copy immediately

New Swift developers often think let b = a duplicates all data right away. In reality, Swift often delays the copy until mutation.

Problem: Expecting an eager deep copy can lead to wrong performance assumptions and confusing debugging when you inspect memory usage.

var numbers = [1, 2, 3]
let copy = numbers

// No full copy is required yet.

Fix: Assume value semantics, not eager duplication. Swift will copy only if a mutation requires it.

var numbers = [1, 2, 3]
let copy = numbers

// The data stays shared until one side mutates.

This mental model matches how Swift actually behaves and helps you reason about performance more accurately.

Mistake 2: Mutating through a copy and expecting the original to change

Because storage may start out shared, beginners sometimes expect both variables to reflect the change. That is not how value types work.

Problem: The mutation only affects the variable being modified, so the original remains unchanged.

var first = ["a", "b"]
var second = first

second.append("c")
// first is still ["a", "b"]

Fix: If you want shared mutable state, use a reference type on purpose. If you want independent values, keep using Swift value types.

final class Box {
    var items: [String] = []
}

let box1 = Box()
let box2 = box1
box2.items.append("c")

The corrected version works because classes share identity, while arrays and strings preserve value behavior.

Mistake 3: Writing a custom type without protecting shared storage

If you build your own value wrapper around reference storage, you must check whether the storage is uniquely referenced before mutating it.

Problem: Mutating shared reference storage breaks value semantics because two copies can unexpectedly change together.

final class Storage {
    var value: Int
    init(value: Int) { self.value = value }
}

struct BrokenCounter {
    var storage: Storage

    mutating func increment() {
        storage.value += 1
    }
}

Fix: Check uniqueness and copy before writing when storage is shared.

struct Counter {
    private var storage: Storage

    mutating func increment() {
        if !isKnownUniquelyReferenced(&storage) {
            storage = Storage(value: storage.value)
        }
        storage.value += 1
    }
}

This works because the copy happens before mutation, so each logical value remains independent.

7. Best Practices

Practice 1: Prefer value types for independent data

When different parts of your program should not affect each other, use struct, Array, String, or similar value types. Copy-on-write gives you safety without forcing expensive duplication up front.

struct Document {
    var title: String
    var tags: [String]
}

This is a good fit because each document can be passed around independently.

Practice 2: Mutate only when necessary

Because the first mutation on shared storage may trigger a copy, it is better to group changes together instead of mutating the same copied value repeatedly in many places.

var items = [1, 2, 3]
var copy = items

copy.append(4)
copy.append(5)

The first append may trigger the copy, but later appends reuse the new unique storage.

Practice 3: Use custom CoW only when profiling justifies it

It is easy to over-engineer a type with reference storage and uniqueness checks. Standard value types already use copy-on-write in many cases, so build your own only when you have a real need.

// Good reason: a custom buffer type with large shared storage.
// Weak reason: wrapping a tiny Int that never becomes expensive to copy.

This keeps your code simpler and avoids hidden complexity that may not pay off.

8. Limitations and Edge Cases

One common surprise is that benchmarking a single mutation may show a spike in work because that mutation finally pays for the deferred copy.

9. Practical Mini Project

Let us build a tiny clipboard history model that stores text entries and copies efficiently when duplicated. The goal is to keep copying cheap until a history instance is modified.

final class HistoryStorage {
    var entries: [String]

    init(entries: [String] = []) {
        self.entries = entries
    }

    func copy() -> HistoryStorage {
        return HistoryStorage(entries: entries)
    }
}

struct ClipboardHistory {
    private var storage: HistoryStorage

    init() {
        self.storage = HistoryStorage()
    }

    mutating func add(_ text: String) {
        if !isKnownUniquelyReferenced(&storage) {
            storage = storage.copy()
        }
        storage.entries.append(text)
    }

    var entries: [String] {
        storage.entries
    }
}

var history1 = ClipboardHistory()
history1.add("Hello")
history1.add("World")

var history2 = history1
history2.add("Swift")

This mini project demonstrates the classic copy-on-write pattern: copy values cheaply, then duplicate the backing storage only when a mutation requires it.

10. Key Points

11. Practice Exercise

Practice by predicting when a copy happens in the following scenario, then compare your answer with the solution.

Expected output: first stays unchanged, while second contains the appended values.

Hint: The copy is deferred until the first mutation of second.

Solution:

var first = [1, 2, 3, 4, 5]
var second = first

second.append(6)
second.append(7)

print(first)
print(second)

12. Final Summary

Swift copy-on-write is a performance optimization that keeps value types fast and easy to use. It allows storage to be shared across copies until mutation makes a real duplicate necessary.

That design is a major reason Swift collections feel simple at the language level but still scale well in real applications. You get predictable value semantics, lower memory overhead, and good performance for the common case of copying more often than mutating.

When you design your own types, follow the same idea only when it solves a real problem. For most code, Swift’s standard library already gives you copy-on-write behavior where it matters most. If you want to go further, study value semantics and then profile your code to see where custom storage management is actually worth it.