Swift Structs, Classes, Enums and Protocols Explained

iOS App Development with Swift and SwiftUI: Beginner to Job-Ready Projects · Swift Basics · 6 min read · Updated

Copy or share?

  1. A struct is a value type. Assigning it makes a new copy.
  2. Changing the copy leaves the original alone, so structs are easy to reason about.
  3. A class is a reference type. Assigning it points to the same object.
  4. A change made through any name shows up everywhere, so use classes on purpose.

Real apps hold real things: an expense, a user, a recipe. Swift gives you several ways to model them. Picking the right one is a skill, and it is a very common interview topic. This lesson builds the data model that the Expense Tracker uses later.

Structs

A struct groups related values and the functions that work on them. You make a new one by calling its name with the values. Swift gives every struct a free memberwise initializer, so you do not have to write the set-up code yourself.

struct Expense {
    var title: String
    var amount: Double
    var category: String = "General"

    func summary() -> String {
        "\(title): \(amount)"
    }
}

let lunch = Expense(title: "Lunch", amount: 8.5)
print(lunch.summary())   // Lunch: 8.5

Notice the function summary() has no return. When a function body is a single expression, Swift returns it for you. Also, lunch is a let, so none of its properties can change. If you need to change one, declare it with var.

Value types and reference types

This is the idea to understand well. Structs are value types. When you assign a struct to a new name, or pass it to a function, you get your own *copy*. Changing the copy does not change the original. Classes are reference types. A copy of a class variable points to the *same* object, so a change through one name shows up through the other.

struct PointS { var x = 0 }
class PointC { var x = 0 }

var a = PointS(); var b = a
b.x = 5
print(a.x, b.x)     // 0 5, two separate copies

let c = PointC(); let d = c
d.x = 5
print(c.x, d.x)     // 5 5, one shared object
  • Use a struct by default. Copies are easy to reason about, and SwiftUI is built around structs.
  • Use a class when you need one shared object that many parts of the code change, or when a framework requires it. Classes can also inherit from other classes, which structs cannot do.
  • A class needs an init that sets every property, unless all of them have default values.
  • A struct that must change its own properties inside a method marks that method mutating.

Enums

An enum is a type with a fixed set of cases. It is perfect for things that can be one of a few options, such as an expense category. Cases can even carry extra values.

enum Category: String, CaseIterable {
    case food = "Food"
    case travel = "Travel"
    case fun = "Fun"
}

for c in Category.allCases {
    print(c.rawValue)
}

enum LoadState {
    case loading
    case loaded(count: Int)
    case failed(message: String)
}

let state = LoadState.loaded(count: 3)
switch state {
case .loading: print("Loading")
case .loaded(let count): print("Loaded \(count)")
case .failed(let message): print(message)
}

CaseIterable gives you allCases, a list of every case, which is ideal for building a picker. The LoadState enum is a very common pattern. One value tells a screen whether to show a spinner, a list or an error message, and the switch makes you handle every case.

Protocols

A protocol is a list of requirements, the properties and methods a type must have. Any type that meets the list can say it *conforms* to the protocol. Code that asks for the protocol does not care which type it receives, as long as the type can do the job.

protocol Summarizable {
    var title: String { get }
    func summary() -> String
}

extension Expense: Summarizable {}   // Expense already has both

func printAll(_ items: [any Summarizable]) {
    for item in items { print(item.summary()) }
}

printAll([lunch, Expense(title: "Bus", amount: 2)])

Two protocols appear in SwiftUI work again and again. Identifiable says a type has a stable id, so a list can tell items apart. Codable lets a type turn into and out of JSON, which is lesson 11. Protocols also make testing easier, because you can swap a real part for a simple fake one in lesson 13.

Extensions

An extension adds new methods or computed properties to a type that already exists, even one you did not write. It is a tidy way to keep code grouped.

extension Double {
    var asMoney: String { String(format: "%.2f", self) }
}
print(12.5.asMoney)   // 12.50

Choosing between them

  • Data that describes a thing: a struct.
  • One of a few options: an enum.
  • A shared object with its own life, such as a manager that many screens use: a class.
  • A promise of behaviour, so different types can be swapped: a protocol.

Your task

  1. Make an Expense struct with id (a UUID()), title, amount and a Category enum. Make it conform to Identifiable.
  2. Write an extension on Array<Expense> with a total property.
  3. Prove the difference between a struct and a class copy with your own tiny example, and write one sentence about what you saw.
  4. Make an enum LoadState and a function that returns a message for each case.

How to check it worked

  • Your total property adds up amounts correctly.
  • Changing a copy of the struct leaves the original unchanged, and changing a copy of the class changes both.
  • You can say when you would choose a struct and when a class.

Frequently asked questions

Should I use a struct or a class in Swift?

Use a struct by default. Choose a class when you need one shared object with its own life, inheritance, or when a framework needs it.

What is a protocol in Swift?

A list of properties and methods that a type promises to have. Code that asks for the protocol works with any type that meets it, which helps with flexibility and testing.

What is an enum used for?

For a value that can be only one of a fixed set of cases, such as a category or a loading state. Cases can carry extra data.