Skip to content

Zero to iOS Hero 11: Structures and value semantics

This is part 11 of the Zero to iOS Hero series.

A tuple can carry a title, tags, rating, and favorite flag. A structure turns those related values into one named type with one initialization boundary and behavior that belongs to the model.

Swift structures use value semantics. Assignment, argument passing, and returns produce independent logical values. Editing a copy does not silently edit the original.

Define a named model

Use struct to declare a new type:

struct FieldNote {
let id: Int
var title: String
var tags: [String]
var isFavorite: Bool
}

The declaration describes every FieldNote. Create an instance by supplying its stored properties:

let note = FieldNote(
id: 1,
title: "Fog over the north ridge",
tags: ["weather", "ridge"],
isFavorite: false
)

FieldNote is the type. note is one value of that type.

Stored properties define the value

Each stored property contributes data to the instance:

PropertyDeclarationModel rule
Identitylet id: IntThis note keeps its identifier
Titlevar title: StringAn edit may replace the title
Tagsvar tags: [String]An edit may change the tag collection
Favoritevar isFavorite: BoolThe user may toggle the flag

Property mutability and binding mutability work together. A var property says the type permits a change. The instance still needs to be stored in a variable before code can make that change.

let fixed = FieldNote(id: 1, title: "Fog", tags: [], isFavorite: false)
// fixed.title = "Lake" // Error
var editable = fixed
editable.title = "Lake" // Allowed

A constant structure freezes all of its stored properties, including properties declared with var.

Memberwise initialization is generated

When a structure declares no custom initializer, Swift provides a memberwise initializer:

FieldNote(id:title:tags:isFavorite:)

The compiler uses the property names, types, order, and defaults to form that initializer. This is convenient inside a module and for small models.

A public library type does not expose an automatically public memberwise initializer. Write the initializer explicitly when construction is part of a public module contract.

Defining a custom initializer inside the original structure also changes which synthesized initializers remain available. Put a convenience initializer in an extension when retaining the memberwise form is useful and the custom path does not own a required invariant.

Assignment creates an independent value

Copy the note, then edit only the copy:

let original = FieldNote(
id: 1,
title: "Fog over the north ridge",
tags: ["weather", "ridge"],
isFavorite: false
)
var edited = original
edited.title = "Fog lifting over the north ridge"
edited.tags.append("morning")

Afterward:

original.title -> Fog over the north ridge
edited.title -> Fog lifting over the north ridge
original.tags -> weather, ridge
edited.tags -> weather, ridge, morning

The two variables do not point at one shared FieldNote object. Each represents its own logical value.

Run the copy proof

The checkpoint edits a copied title, copied tags, and copied favorite flag. It also returns a renamed copy from a nonmutating method:

idle

Execution sends this source to the project runner. It uses Swift 6.3.3 on Linux for standard-library code, not the Apple SDK, an iOS simulator, or a device.

Edit the source, then choose Run Swift. If no runner is configured, the source stays in this editor.

Compiler diagnostics

(none)

Standard output

(no stdout)

Standard error

(no stderr)

Expected output:

Original: Fog over the north ridge | weather, ridge | favorite false
Edited: Fog lifting over the north ridge | weather, ridge, morning | favorite true
Renamed copy: North ridge fog

The preconditions prove the original retained its title, two tags, and false favorite flag. The copied array also behaves as an independent value.

This is standard-library code for the Swift 6.3.3 Linux editor. It proves Swift structure and collection value semantics. It does not prove persistence identity, SwiftUI observation, an app target, Simulator, or device behavior.

Collections use copy-on-write storage

The tags array is also a value type. Its visible rule is simple: changing edited.tags does not change original.tags.

Swift collections can avoid copying their complete storage at assignment time. Two logical values may share a buffer until one value mutates. At that point, the mutating value obtains storage it can change without affecting the other value.

assignment
original.tags ----+
+-> shared buffer
edited.tags ------+
mutation of edited.tags
original.tags ------> original buffer
edited.tags --------> copied, changed buffer

This copy-on-write optimization changes allocation timing, not value semantics. Code depends on independent logical values. Performance work measures when large values trigger copies.

Methods put behavior beside the model

A method is a function associated with the type:

struct FieldNote {
var title: String
func renamed(to newTitle: String) -> FieldNote {
var copy = self
copy.title = newTitle
return copy
}
}

The call reads as an operation on a note:

let renamed = original.renamed(to: "North ridge fog")

The method does not mutate original. It creates a local copy of self, changes that copy, and returns it.

This shape works well when the caller needs both the before and after values, or when a transformation pipeline benefits from explicit returned values.

Mutating methods change the variable

Structure methods cannot change stored properties unless marked mutating:

mutating func toggleFavorite() {
isFavorite.toggle()
}

Call the method on a variable:

var note = original
note.toggleFavorite()

The keyword makes mutation part of the method contract. The call still cannot mutate a let instance.

A mutating method may assign new property values or replace self with a new complete value. Use it when in-place editing is the natural API. Return a new value when preserving both versions or making the transformation explicit helps the caller.

self names the current value

Inside an instance method, self refers to the instance receiving the call:

func hasTitle(_ title: String) -> Bool {
self.title == title
}

The explicit prefix resolves the collision between the property and parameter. Swift lets most property access omit self when no ambiguity exists.

Do not add self. to every expression as decoration. Use it where the language requires it or where it clarifies which value owns a member.

Value semantics apply across function boundaries

Passing a structure to a normal parameter gives the function a value:

func renamed(_ note: FieldNote, to title: String) -> FieldNote {
var result = note
result.title = title
return result
}

The function cannot mutate the caller’s variable through an ordinary parameter. An inout parameter can make caller mutation explicit, as post 7 demonstrated.

Returning a structure also returns a value. The compiler may optimize physical copies while preserving behavior as if values were independent.

Equality can be synthesized

Declare Equatable when equality of every stored property matches the model’s equality rule:

struct FieldNote: Equatable {
let id: Int
var title: String
var tags: [String]
var isFavorite: Bool
}

Because each property is equatable, Swift can synthesize ==:

let unchanged = original == original
let differs = original != edited

Full stored-value equality is not always domain identity. Two versions with the same id but different titles are the same logical record under an identifier policy, yet they are not equal under synthesized structural equality.

Name the rule. Use Equatable for value equality and a dedicated identifier comparison when the product asks whether two versions refer to the same record.

A structure can protect invariants

The automatic memberwise initializer accepts any values of the declared types. A blank title is still a String, and duplicate tags still form an array.

Move construction behind an explicit initializer when every valid instance must satisfy more rules:

import Foundation
struct FieldNote {
let id: Int
var title: String
init?(id: Int, title: String) {
guard id > 0 else { return nil }
let trimmed = title.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return nil }
self.id = id
self.title = trimmed
}
}

This example needs Foundation for whitespace trimming, so it does not belong in the browser checkpoint. Post 14 develops initialization and invariant protection in depth.

The design principle starts now: do not create an invalid value and rely on every caller to repair it later.

Struct is the default, not a law

Structures fit values that should be copied, compared, transformed, serialized, or moved across boundaries without shared identity.

Classes fit shared identity and reference semantics. They also support inheritance, deinitialization, and reference counting. Post 13 uses a shared editing session to show when those capabilities change the model.

Choose from the product rule:

Product ruleLikely starting point
Each assignment should create an independent logical valueStructure
Several owners must observe and mutate one shared identityClass or actor
The domain is a closed set of alternativesEnumeration
The type needs inheritance from a classClass

“Struct by default” is useful because independent values are easier to reason about. It is not permission to simulate shared identity with global indexes and manual pointer-like plumbing.

Common mistakes

  • Choosing a class because the model has methods: Structures can define methods, properties, subscripts, initializers, extensions, and protocol conformances.
  • Assuming assignment always shares an object: Structure assignment creates an independent logical value.
  • Assuming value semantics always copy bytes immediately: Copy-on-write and compiler optimization can delay physical copying.
  • Declaring every property with var: Keep stable facts constant inside the model.
  • Using synthesized equality as record identity without deciding the rule: Structural equality and identity answer different questions.
  • Adding a custom initializer without considering the memberwise initializer: The synthesized construction surface can change.

Practice

Modify the runnable example in small steps:

  1. Change only edited.id and explain why the compiler rejects it.
  2. Remove mutating from toggleFavorite() and read the diagnostic.
  3. Store original in a let, then try to call the mutating method.
  4. Add a rating property and prove changing the copy leaves the original rating intact.
  5. Compare original and edited with synthesized equality.
  6. Write a nonmutating tagged(with:) method that returns a changed copy.

Each exercise checks a type contract, not just syntax.

Checkpoint

You should now be able to explain:

  • How a structure turns related values into one named type.
  • Why a let structure prevents property mutation.
  • How assignment and argument passing preserve value semantics.
  • Why copy-on-write does not turn arrays into reference types.
  • When a method needs mutating.
  • Why structural equality and record identity can differ.
  • When shared identity points toward a class instead.

The next post models a closed set of loading states with an enumeration and associated values.

Series navigation

References

  • Value and reference models: The Swift Programming Language chapter Structures and Classes defines shared capabilities, structure copying, class identity, and copy-on-write collections.
  • Structure construction: Initialization documents default, memberwise, and custom initializers and their synthesis rules.
  • Behavior on values: Methods specifies instance methods, self, and mutating methods for structures and enumerations.