Swift Structs

Model data with structs: stored and computed properties, methods, mutating methods, memberwise and custom initializers, property observers and why structs are value types.

Lesson 12 of 43Intermediate18 min read

So far you've stored data in separate variables, arrays and dictionaries. Real apps deal with things: a product with a name, price and stock level; a message with a sender, text and time; a point with x and y. A struct groups related values into one new type, together with the functions that work on them.

Structs are the most common way to model data in Swift. Apple's own frameworks use them heavily: String, Array, Int and most SwiftUI views are structs. They're value types: when you assign a struct to another variable or pass it to a function, the receiver gets its own independent copy, just like arrays.

In this lesson you'll define structs with stored and computed properties, add methods, change properties with mutating methods, write initializers that validate data, react to changes with didSet, and see how value semantics keeps data safe.

Defining a struct

Swift
struct Product {
    let name: String
    var price: Double
    var stock: Int = 0
}

var mug = Product(name: "Mug", price: 8.5, stock: 12)
let pen = Product(name: "Pen", price: 1.2)       // stock uses its default
mug.price = 9.0
print(mug.name, mug.price)
  • Type names use UpperCamelCase; properties use lowerCamelCase.
  • Swift writes a memberwise initializer for you, with a labelled argument for each property. Properties with default values can be left out.
  • let properties can't change after creation; var properties can, but only if the instance itself is a var.

Computed properties and methods

A computed property calculates its value each time it's read:

Swift
struct Rectangle {
    var width: Double
    var height: Double
    var area: Double { width * height }
    func describe() -> String { "\(width)x\(height), area \(area)" }
}

Methods are functions inside the type and can read its properties directly. A method that changes properties must be marked mutating, because changing a struct changes the whole value:

Swift
mutating func scale(by factor: Double) {
    width *= factor
    height *= factor
}

Initializers and property observers

Write your own init to validate or calculate values. A failable initializer, init?, returns nil when the input isn't valid:

Swift
struct Temperature {
    let celsius: Double
    init?(celsius: Double) {
        guard celsius >= -273.15 else { return nil }
        self.celsius = celsius
    }
}

Property observers run code when a stored property changes: didSet runs after the change, with the previous value available as oldValue; willSet runs just before. They're handy for keeping values in range or reacting to updates. static properties and methods belong to the type itself, such as Temperature.absoluteZero.

Value semantics

Swift
var a = Rectangle(width: 2, height: 3)
var b = a
b.width = 10
print(a.width)     // 2.0: a is unchanged

Each variable holds its own copy. A function that receives a struct can't change the caller's copy unless it's passed inout. This makes code predictable: data only changes where you can see it change. Apple recommends structs by default, using classes only when you need shared identity or inheritance, as the classes lesson explains.

Writing your own init replaces the memberwise one

The free memberwise initializer only exists while you haven't written an init inside the struct's braces. Once you add one, Swift assumes you want full control and removes it:

Swift
struct User {
    let name: String
    let joinedYear: Int
    init(name: String) {
        self.name = name
        self.joinedYear = 2024
    }
}
User(name: "Ada")                    // fine
User(name: "Ada", joinedYear: 2020)  // error: extra argument 'joinedYear' in call

If you want both, put your custom init in an extension instead, and the memberwise initializer stays available. The extensions lesson shows this pattern.

Copy, change, keep both

Because assigning a struct makes a copy, you can create a variation of a value without touching the original. This is a common way to build a "changed version" of some data:

Swift
let mug = Product(name: "Mug", price: 10.0)
var saleMug = mug
saleMug.price *= 0.8
print(mug.price, saleMug.price)      // 10.0 8.0

The original mug can even stay a let. Only the copy needs to be a var, since that's the one you change. SwiftUI uses exactly this idea: each change produces a new value, and the screen updates to match it.

You can also give a property private(set), as in private(set) var balance = 0. Code outside the struct can read it, but only the struct's own mutating methods can change it.

Swift Structs Syntax

Struct syntax

struct Name {
    let a: Int
    var b: String = "default"
    var computed: Int { a * 2 }
    var watched = 0 { didSet { print(oldValue, watched) } }
    static let shared = 1
    func read() -> Int { a }
    mutating func change() { b = "new" }
    init?(a: Int) { guard a >= 0 else { return nil }; self.a = a }
}

Swift Structs Summary

  • A struct groups properties and methods into a new type; Swift provides a memberwise initializer.
  • Computed properties calculate values; methods that change properties must be mutating.
  • Custom init validates data; init? returns nil for invalid input.
  • didSet and willSet observe changes; static members belong to the type.
  • Structs are value types: copies are independent, and let instances can't change at all.

Swift Structs Examples

Each example shows the code, the output it produces and what each part does.

Example 1

A product struct

Swift
struct Product {
    let name: String
    var price: Double
    var stock: Int = 0

    var isAvailable: Bool { stock > 0 }
    var stockValue: Double { price * Double(stock) }

    mutating func sell(_ quantity: Int) -> Bool {
        guard quantity <= stock else { return false }
        stock -= quantity
        return true
    }
}

var lamp = Product(name: "Desk lamp", price: 24.5, stock: 3)
print(lamp.sell(2))
print(lamp.sell(5))
print(lamp.stock, lamp.isAvailable, lamp.stockValue)
Output
true
false
1 true 24.5
Line-by-line explanation
  1. Lines 6 to 7Computed properties are worked out each time they're read.
  2. Line 9sell changes stock, so it must be mutating; it can only be called on a var instance.
  3. Line 10guard refuses to sell more than is in stock.
  4. Line 19After selling 2 of 3, one lamp is left.
Example 2

A failable initializer and didSet

Swift
struct Volume {
    var level: Int {
        didSet {
            if level > 100 { level = 100 }
            if level < 0 { level = 0 }
            print("Volume \(oldValue) -> \(level)")
        }
    }

    init?(level: Int) {
        guard (0...100).contains(level) else { return nil }
        self.level = level
    }
}

var v = Volume(level: 40)!
v.level += 20
v.level = 150
print(Volume(level: 120) == nil)
Output
Volume 40 -> 60
Volume 60 -> 100
true
Line-by-line explanation
  1. Lines 3 to 7didSet runs after every change, clamps the value into range and reports it.
  2. Lines 10 to 13The failable initializer rejects levels outside 0 to 100.
  3. Line 16The ! is safe here because 40 is valid; in real code, use if let.
  4. Line 19Volume(level: 120) returns nil.
Example 3

Value semantics in action

Swift
struct Point {
    var x: Int
    var y: Int
}

var start = Point(x: 0, y: 0)
var current = start
current.x += 5
current.y += 3

print(start)
print(current)

func moved(_ p: Point) -> Point {
    var copy = p
    copy.x += 1
    return copy
}
print(moved(current), current)
Output
Point(x: 0, y: 0)
Point(x: 5, y: 3)
Point(x: 6, y: 3) Point(x: 5, y: 3)
Line-by-line explanation
  1. Line 7current is a copy, so changing it leaves start alone.
  2. Lines 11 to 12Structs print their properties automatically.
  3. Lines 14 to 17The function works on its own copy and returns a new value; the caller's point is unchanged.

Common Swift Structs Mistakes

Changing a property of a let instance

Don't
let mug = Product(name: "Mug", price: 8.5)
mug.price = 9                       // error: cannot assign to property: 'mug' is a 'let' constant
Do
var mug = Product(name: "Mug", price: 8.5)
mug.price = 9

Forgetting mutating

Don't
struct Counter {
    var count = 0
    func increment() { count += 1 }  // error: left side of mutating operator isn't mutable
}
Do
struct Counter {
    var count = 0
    mutating func increment() { count += 1 }
}

Expecting a shared change

Don't
var a = Point(x: 1, y: 1)
var b = a
b.x = 9
print(a.x)                          // 1, not 9
Do
// change the variable you actually want, or use a class when shared identity is needed
a.x = 9

Swift Structs in Real Projects

Structs in Swift apps

Almost all data in modern Swift apps is modelled with structs: users, messages and settings decoded from JSON, rows in a list, and every SwiftUI view (struct ContentView: View). Because structs are value types, SwiftUI can compare old and new values to decide exactly what to redraw, and data can be passed around without worrying that another part of the app will change it unexpectedly.

Swift Structs FAQ

What is the difference between a struct and a class in Swift?

A struct is a value type: assigning or passing it makes an independent copy. A class is a reference type: assigning it shares the same object. Structs also get a free memberwise initializer and can't inherit, while classes support inheritance and deinit. Apple recommends starting with a struct.

Why do I need the mutating keyword in a Swift struct?

A struct method that changes a property is really replacing the whole value, so Swift asks you to mark it mutating. That tells the compiler it can only be called on a var instance, never on a let constant.

Can a struct have methods in Swift?

Yes. Structs can have methods, computed properties, initializers, static members and protocol conformances, just like classes. The main things they can't do are inherit from another struct and run a deinit.

Why does my struct lose its memberwise initializer?

Writing any init inside the struct's main declaration removes the automatically generated memberwise initializer. Move your custom init into an extension of the struct, and both initializers will be available.

Are Swift structs copied every time they are assigned?

Conceptually yes: each variable gets its own independent value. In practice, standard types such as arrays and strings use copy-on-write and only duplicate their storage when one copy changes, and the compiler removes many other copies, so structs stay fast.

Swift Structs Exercises

Answer each one and check it instantly. If you get stuck, open the hint before the answer.

Fill in the blankseasy

Complete the method so it can change the struct's property.

struct Counter {
    var count = 0
     func reset() { count = 0 }
}
Predict the outputeasy

What does this print?

struct Box { var w: Int; var h: Int; var area: Int { w * h } }
print(Box(w: 3, h: 4).area)
Predict the outputmedium

What does this print?

struct P { var x: Int }
var a = P(x: 1)
var b = a
b.x = 5
print(a.x + b.x)
Multiple choicemedium

What does init? mean?

Multiple choicehard

When does Apple recommend using a struct instead of a class?

Swift Structs Quiz

5 questions to check what you learned.

  1. What does Swift generate automatically for a struct?

    • A class
    • A memberwise initializer with a parameter for each property
    • A copy method
    • A protocol
  2. What is a computed property?

    • A property whose value is calculated each time it's read
    • A property that stores a calculation once
    • A static property
    • A let property
  3. When does didSet run?

    • Before the property changes
    • Only in the initializer
    • After the property changes, with oldValue available
    • Never automatically
  4. Can you call a mutating method on a let instance?

    • Yes
    • No, the instance must be a var
    • Only in playgrounds
    • Only if it returns a value
  5. Why are structs called value types?

    • They only hold numbers
    • They have no methods
    • They're stored in a database
    • Assigning or passing one creates an independent copy