A Swift struct can share mutable objects after assignment. To keep copies independent, use value-type properties or hide class storage behind copy-on-write. This guide builds a synthetic tile-row calculator for a Mac desktop preview and checks that editing one copy leaves the others unchanged.
Why does copying the struct still change the original?
Suppose a desktop preview calculates the width of a row of tiles. You copy its settings to try another tile width, expecting the original preview to remain unchanged. That expectation fails when both settings values contain a reference to the same mutable object.
Swift copies a struct’s stored values when assigning it to another variable. For a class property, that stored value is a reference: the referenced object remains shared. The wrapper is still a struct, but its observable behavior can include shared mutation. These rules follow from Swift’s documented distinction between value and reference types.
This complete example demonstrates the problem. It uses only the Swift standard library, so no imports are required.
final class TileDimensions {
var width: Int
init(width: Int) {
self.width = width
}
}
struct SharedLayout {
let dimensions: TileDimensions
}
let original = SharedLayout(
dimensions: TileDimensions(width: 40)
)
let edited = original
edited.dimensions.width = 60
assert(original.dimensions === edited.dimensions)
assert(original.dimensions.width == 60)
print(original.dimensions.width) // 60
Declaring the property or wrapper with let does not freeze the object’s mutable properties. It prevents replacing that reference or struct value. Swift’s Structures and Classes chapter explicitly distinguishes constant references from immutable object state.
Putting these objects in an array also preserves their shared identity. Array copies independently manage their elements, but a class-valued element is itself a reference. Replacing an element affects one array; modifying an object referenced by both arrays is observable through both. Apple demonstrates this distinction in Array’s documentation.
Which design gives the calculation independent values?
Define the contract before choosing an implementation: after var edited = original, every supported edit to either layout must leave the other layout’s calculated width unchanged. Readers of the type should not need to know whether its storage is shared internally.
The following decision aid applies that contract to a local preview model. It is a design recommendation derived from the documented copying rules.
| Situation | Suitable design | Condition to preserve independence |
|---|---|---|
| Settings consist of numbers, strings, or other value-semantic components | Store those values directly in the struct | Check that nested components also provide independent observable values |
| Existing class-backed storage must remain | Use private copy-on-write storage | Every mutation must detach shared mutable state first |
| A separate snapshot is needed only at a named operation | Provide an explicit copying operation | Document that ordinary assignment still shares objects unless the wrapper also enforces independence |
| Several parts of the program intentionally edit one object | Keep reference semantics | Describe the shared-object behavior in the API |
For three integer settings, direct struct properties are the simplest choice. The class-backed implementation below is useful when adapting an existing storage design or learning how the boundary works; it does not establish a performance advantage for this small calculation.
Copy-on-write allows two values to share storage until one changes. Apple describes wrapping a class in a struct and applying copy-on-write as a way to combine separately allocated storage with value semantics in Explore Swift performance. Merely wrapping the class does not implement that behavior.
An explicit copy() method alone also leaves a gap: callers can still write ordinary assignment without invoking it. If assignment is part of your value contract, the wrapper’s mutation paths must enforce independence automatically.
How do you implement and check copy-on-write?
Use one private storage object, construct it inside the wrapper, and expose only calculated values and controlled mutation methods. Immediately before changing storage, call isKnownUniquelyReferenced. If uniqueness is not established, replace the storage with an independent copy. Apple documents this sequence specifically for implementing copy-on-write.
The worked calculation uses illustrative inputs: three tiles, each 40 points wide, with a 10-point gap between adjacent tiles. There are two gaps, so the row width is 3 * 40 + 2 * 10 = 140 points. Increasing tile width to 60 produces 200 points.
To try the complete program on a Mac, first run swift --version in Terminal to confirm that a Swift toolchain is available, as recommended by Swift’s Getting Started guide. Save the following code as Layout.swift, then run swift Layout.swift from its directory. The program calculates in memory and prints its results.
struct TileLayout {
private final class Storage {
let columns: Int
var tileWidth: Int
var gap: Int
init(columns: Int, tileWidth: Int, gap: Int) {
self.columns = columns
self.tileWidth = tileWidth
self.gap = gap
}
func copy() -> Storage {
Storage(
columns: columns,
tileWidth: tileWidth,
gap: gap
)
}
}
private var storage: Storage
init(columns: Int, tileWidth: Int, gap: Int) {
precondition((1...1000).contains(columns))
precondition((0...1000).contains(tileWidth))
precondition((0...1000).contains(gap))
storage = Storage(
columns: columns,
tileWidth: tileWidth,
gap: gap
)
}
var rowWidth: Int {
storage.columns * storage.tileWidth
+ (storage.columns - 1) * storage.gap
}
private mutating func ensureUniqueStorage() {
if !isKnownUniquelyReferenced(&storage) {
storage = storage.copy()
}
}
mutating func setTileWidth(_ width: Int) {
precondition((0...1000).contains(width))
ensureUniqueStorage()
storage.tileWidth = width
}
mutating func setGap(_ gap: Int) {
precondition((0...1000).contains(gap))
ensureUniqueStorage()
storage.gap = gap
}
}
var original = TileLayout(columns: 3, tileWidth: 40, gap: 10)
var edited = original
edited.setTileWidth(60)
assert(original.rowWidth == 140)
assert(edited.rowWidth == 200)
original.setGap(5)
assert(original.rowWidth == 130)
assert(edited.rowWidth == 200)
let snapshot = edited
edited.setGap(20)
assert(snapshot.rowWidth == 200)
assert(edited.rowWidth == 220)
print(original.rowWidth, snapshot.rowWidth, edited.rowWidth)
// Expected output: 130 200 220
The program requires no imports or external packages. Its accepted ranges are deliberate example constraints: one to 1,000 columns, and zero to 1,000 points for width and gap. They keep the integer calculation bounded. An out-of-range argument fails a precondition; it is not silently clamped. If these setters later accept editable text from a desktop interface, validate that input before calling them and give the user a correction path.
The checks cover three observable requirements: editing the copied layout preserves the original, editing the original preserves the copy, and retaining a constant snapshot preserves its result during a later edit. They test behavior without requiring a particular allocation count. Swift was unavailable in the research environment, so this example was inspected but not executed there; the output shown is the expected result.
What must remain true when the wrapper grows?
The example’s independence comes from its entire API boundary. Storage is private, no initializer accepts an externally retained storage object, and no getter returns one. Both mutation methods pass through the same detachment operation. The getter calculates a result without changing storage.
When adding features, trace each new path that can change observable state. A new setter, subscript, or editing callback needs the same treatment as the existing methods. A getter that returns a mutable child object would let callers bypass detachment, even if the getter itself is read-only.
The storage copy must also cover all mutable state relevant to the value. Here, Storage.copy() copies three integers, which is sufficient. If storage later contains a mutable class-valued child, assigning that child to the new storage would preserve another shared reference. Copy that child appropriately or replace it with a component that already provides value semantics. The class-element behavior documented for Array illustrates the same boundary.
Use this sequence when extending the calculator:
- Copy a layout before exercising each new editing operation.
- Verify that the untouched layout retains its previous result.
- Repeat with the roles reversed.
- Retain a snapshot, perform several later edits, and check it again.
- Inspect every returned property and initializer argument for mutable references that can escape the wrapper.
A final storage class prevents subclasses from adding behavior, but it does not make its properties immutable. Likewise, marking an operation mutating allows it to update the struct; the detachment logic is what preserves this example’s independence.
How should Foundation objects be copied?
If an existing model stores Foundation objects such as NSMutableString, use the concrete type’s documented copying behavior when implementing storage detachment. Swift assignment does not automatically call Foundation copying methods.
Foundation’s NSCopying protocol defines functional copying, while the exact meaning depends on the class. Its documentation also permits returning the original object when both the class and its contents are immutable. Consequently, object identity alone is not a universal test of whether a copy satisfies your value contract.
When you need a mutable result from a type with distinct mutable and immutable forms, consult NSMutableCopying. Apple documents mutableCopy(with:) and the convenience method mutableCopy() for that purpose. Classes without that distinction should adopt NSCopying instead.
For the tile calculator, the practical boundary is straightforward: a Foundation object accepted as input must not remain an externally mutable alias to your supposedly independent settings. Take a suitable independent snapshot on entry, detach appropriately before edits, and return value snapshots rather than the internal mutable object. Verify the concrete class’s treatment of any nested mutable contents before relying on its copy operation.
Does copy-on-write make the type Sendable?
Value independence and concurrency conformance require separate reasoning. Apple’s Sendable documentation requires a checked sendable struct’s stored members to be sendable. A private mutable storage class does not satisfy that requirement merely because its enclosing type is a struct.
The uniqueness check is also not synchronization. Apple warns that isKnownUniquelyReferenced can return true during simultaneous access and requires appropriate synchronization around such use. It checks strong references; weak or unowned aliases may still exist. Keeping storage inaccessible outside the wrapper therefore matters as well as checking uniqueness. See the standard-library function’s documented limits.
Keep this example within one synchronous calculation context. If a later version needs to pass settings between concurrency domains, the straightforward alternative is a struct containing sendable value properties, with checked Sendable conformance. Adding @unchecked Sendable transfers correctness responsibility to the implementer; it does not supply missing coordination. Apple explains these requirements in the Sendable reference.
For a small desktop calculation, prefer direct value properties whenever they meet the task. When mutable class storage must remain, preserve the contract through private ownership, complete storage copies, controlled edits, and observable independence checks. The useful result is predictable: changing a preview’s working settings leaves every retained layout value with its own calculated result.
Sources
- Value And Reference Types In Swift — Swift.org.
- Structures and Classes — The Swift Programming Language, Swift project.
- Array — Apple Developer Documentation.
- Explore Swift performance — Apple Developer.
- isKnownUniquelyReferenced(_:) — Apple Developer Documentation.
- Getting Started — Swift.org.
- NSCopying — Apple Developer Documentation.
- NSMutableCopying — Apple Developer Documentation.
- Sendable — Apple Developer Documentation.