Tutorials Ultimate Swift Series Chapter 13

Structures

SwiftChapter 13 of the Ultimate Swift Series30 minMay 15, 2026Beginner

So far you've worked with the types Swift gives you: Int, Double, String, Bool, Array, Dictionary. Those cover the primitives -- but real apps are full of concepts that aren't a single number or string. A song. A user. A version number. A build artifact.

This chapter introduces the first way to define your own types: the structure, or struct. Structures bundle related values together under one name and let you give that name behavior of its own.

Why You Need More Than Primitives

Imagine you're shipping an iOS app and you want to track its version. Semantic versioning gives you three numbers: major, minor, patch. So 1.4.7 is major 1, minor 4, patch 7.

You could store them as three separate Ints:

let appMajor = 1
let appMinor = 4
let appPatch = 7

That works for one version. Now you want to compare against the user's installed version:

let installedMajor = 1
let installedMinor = 2
let installedPatch = 0
 
let isNewer = appMajor > installedMajor
    || (appMajor == installedMajor && appMinor > installedMinor)
    || (appMajor == installedMajor && appMinor == installedMinor && appPatch > installedPatch)

Three numbers are now six variables, and the comparison logic is repeated every time you want to compare. Scale this to three or four versions in flight (current, latest, minimum-supported, just-rejected) and the code gets messy fast.

What you really want is a single thing called a version with three numbers inside it. That's a structure.

Defining a Structure

The syntax begins with the struct keyword, a name, and a pair of braces:

struct Version {
    let major: Int
    let minor: Int
    let patch: Int
}

Inside the braces, you list properties -- the values that every instance of this type carries. Here, every Version has a major, a minor, and a patch, each typed as Int.

Create instances the same way you'd call a function:

let appVersion = Version(major: 1, minor: 4, patch: 7)
let installed = Version(major: 1, minor: 2, patch: 0)

Notice you didn't write an init. Swift synthesizes a memberwise initializer for every struct, with one parameter per property in declaration order. Free of charge.

Initializers enforce completeness

Swift won't let you create a partially built struct. If Version declares three properties, every initializer call must supply values for all three. That guarantee is one of Swift's quiet superpowers -- a whole class of "I forgot to set X" bugs from other languages just can't happen here.

Accessing Properties

Use dot syntax to read property values, exactly like you've done with String.count or Array.first:

appVersion.major     // 1
appVersion.minor     // 4
appVersion.patch     // 7

Properties can themselves be structures. Here's a Release that wraps a Version with extra metadata:

struct Release {
    let version: Version
    var notes: String
    var isLive: Bool
}
 
var v140 = Release(
    version: Version(major: 1, minor: 4, patch: 0),
    notes: "Adds dark mode and AirDrop sharing.",
    isLive: false
)

Reach through with chained dots:

v140.version.major   // 1

Mini-exercise

Design a Build struct that captures everything Xcode needs to identify a build artifact. Include at least: a Version, a build number, and the target platform (iOS, macOS, etc.). Don't worry about the platform type -- a String is fine for now.

Mutability: let vs var, Twice

A struct's mutability is controlled in two places: the property declaration and the variable that holds the instance.

Look at Release again:

struct Release {
    let version: Version    // can't be changed after init
    var notes: String       // can be changed after init
    var isLive: Bool        // can be changed after init
}

version is let, so once you set it, no one can swap a different Version in. notes and isLive are var, so they can change. But changing them also requires that the instance be stored in a var:

var draft = Release(
    version: Version(major: 1, minor: 4, patch: 0),
    notes: "Dark mode.",
    isLive: false
)
draft.notes = "Dark mode and AirDrop sharing."  // ✅
draft.isLive = true                              // ✅
 
let locked = Release(
    version: Version(major: 1, minor: 4, patch: 0),
    notes: "Final.",
    isLive: true
)
locked.isLive = false  // ⛔ Cannot assign: 'locked' is a 'let' constant

Even though isLive is var, locked is a let, so the whole instance is frozen. Both checks have to pass for an assignment to compile.

Mini-exercise

Predict what each line below will do -- compile or fail? Then check.

let v1 = Version(major: 1, minor: 0, patch: 0)
v1.major = 2
 
var v2 = Version(major: 1, minor: 0, patch: 0)
v2.major = 2

(The first fails because the property major is let. The second fails for the same reason -- making v2 a var doesn't help, because major itself is constant. To make major mutable, you'd need to declare it as var inside the struct.)

Methods

A struct isn't just a bag of data -- it can have behavior too. Functions defined inside a struct are called methods.

The version comparison from the intro fits naturally as a method on Version:

struct Version {
    let major: Int
    let minor: Int
    let patch: Int
 
    func isNewer(than other: Version) -> Bool {
        if major != other.major { return major > other.major }
        if minor != other.minor { return minor > other.minor }
        return patch > other.patch
    }
}

Two things to notice:

  1. Inside the method, you can refer to major, minor, and patch directly -- they refer to this instance's properties. No self. needed (though you can write self.major if it makes things clearer).
  2. The method takes another Version as a parameter and compares against it.

Call it with dot syntax, like any other member:

let app = Version(major: 1, minor: 4, patch: 7)
let installed = Version(major: 1, minor: 2, patch: 0)
 
app.isNewer(than: installed)   // true
installed.isNewer(than: app)   // false

This is a huge improvement over the six-variable mess from earlier: the comparison rules now live in one place, with the data they operate on.

Mini-exercises

  1. Add a method isSameMajor(as:) that returns true if two versions share the same major number. This is useful for "do I need to show a 'major update' banner?" logic.
  2. Add a computed-style method displayString() that returns "1.4.7" from a Version(major: 1, minor: 4, patch: 7). Hint: use string interpolation.

Value Semantics: Copy on Assignment

Here's where structs differ from the reference types you'll meet in the next chapter. Structs are value types: when you assign one to a new variable, you get a copy, not a shared reference.

You've already seen this with Int, even if you didn't have a name for it:

var a = 5
var b = a   // b gets a copy of a's value
 
a = 10
 
a   // 10
b   // 5  ← not affected

Your structs work exactly the same way:

var v1 = Version(major: 1, minor: 0, patch: 0)
var v2 = v1   // copy
 
// Imagine major is var for this example...
// v1.major = 2
// v2.major would still be 1

This guarantee is why structs feel "safe": when you pass one into a function or assign it to another variable, you don't have to worry that someone else has a handle on the same underlying data and might change it behind your back.

Read = as "assign", not "is equal to." var b = a doesn't mean "b is a." It means "copy the value of a into b." After that moment, b is its own thing.

Everything Is a Structure

You might be surprised to learn that the "built-in" types you've been using all along are themselves structures. If you peek at the standard library, you'll see:

public struct Int : ... { ... }
public struct Double : ... { ... }
public struct String : ... { ... }
public struct Bool : ... { ... }
public struct Array<Element> : ... { ... }
public struct Dictionary<Key, Value> : ... { ... }

That's why Int and String have value semantics -- they're structs. The skills you're learning here are the same skills the standard library authors use. There's no special "magic" tier above your own code.

Conforming to a Protocol

The ... after the colon in those declarations is a list of protocols the type promises to implement. Protocols are the topic of a later chapter, but a tiny example here shows how structs and protocols team up.

Swift's standard library has a protocol called CustomStringConvertible. It has one requirement: a description: String property. Any type that conforms to it gets nicer printing for free -- because print() checks for CustomStringConvertible and uses your description instead of the default "noisy" output.

Make Version conform:

struct Version: CustomStringConvertible {
    let major: Int
    let minor: Int
    let patch: Int
 
    var description: String {
        "\(major).\(minor).\(patch)"
    }
 
    func isNewer(than other: Version) -> Bool {
        if major != other.major { return major > other.major }
        if minor != other.minor { return minor > other.minor }
        return patch > other.patch
    }
}

Two changes:

description here is a computed property -- it has no stored value, it just runs the expression on the right of the braces every time something reads it. You'll meet computed properties properly soon, but for now know that they're how you derive a value from other properties.

Now interpolation and print() use your formatting:

let v = Version(major: 1, minor: 4, patch: 7)
print(v)         // 1.4.7
"\(v)"           // "1.4.7"

Without the conformance, print(v) would have spat out something like Version(major: 1, minor: 4, patch: 7). Useful for debugging, ugly for users.

Challenges

Challenge 1: subscription pricing

Many apps charge monthly with a yearly discount. Model it:

struct SubscriptionTier {
    let name: String           // "Pro", "Team", etc.
    let monthlyPrice: Double   // in USD
    let yearlyDiscount: Double // 0.0 (no discount) to 1.0 (free)
}

Add a method yearlyPrice() that returns the cost of paying for a year up front: monthlyPrice * 12 * (1 - yearlyDiscount). Test it on a tier with monthlyPrice: 7.99 and yearlyDiscount: 0.2 -- the answer should be about 76.70.

Challenge 2: RGB and palettes

Define an RGB struct with red, green, blue -- each an Int from 0 to 255. Then define an IconPalette struct that holds three RGBs: primary, secondary, accent.

Add a method isDark() on IconPalette that returns true when the primary color's brightness is low. Use this rough rule: brightness = (red + green + blue) / 3. The palette is dark if brightness is less than 80.

Challenge 3: build numbers and printing

Add a build: Int property to Version. Update description so a Version(major: 1, minor: 4, patch: 7, build: 42) prints as "1.4.7 (42)". Verify with print(_:) and string interpolation.

Key Points

In Chapter 14: Properties, you'll go deeper on the property side of structs: computed properties that calculate values on the fly, type-level properties shared across all instances, property observers that react to changes, and lazy initialization for expensive values.

Ch 12: RegexCh 14: Properties
SwiftUIUltimate SwiftUI SeriesSwiftUI tutorials for building native app screens, layouts, navigation, and state-driven interfaces.Ship iOSShip iOS Apps SeriesShipping workflows for iOS apps: signing, TestFlight, App Store Connect, CI, and release hygiene.DeliveryModern Delivery PipelineCI/CD, review, runner, and deploy workflows for teams shipping apps and websites safely.

Ship your apps faster

When you're ready to publish your Swift app to the App Store, Simple App Shipper handles metadata, screenshots, TestFlight, and submissions — all in one place.

Try Simple App Shipper
5 free articles remainingSubscribe for unlimited access