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 = 7That 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.
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 // 7Properties 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 // 1Mini-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' constantEven 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:
- Inside the method, you can refer to
major,minor, andpatchdirectly -- they refer to this instance's properties. Noself.needed (though you can writeself.majorif it makes things clearer). - The method takes another
Versionas 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) // falseThis 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
- Add a method
isSameMajor(as:)that returnstrueif two versions share the same major number. This is useful for "do I need to show a 'major update' banner?" logic. - Add a computed-style method
displayString()that returns"1.4.7"from aVersion(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 affectedYour 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 1This 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:
- After the struct name,
: CustomStringConvertibledeclares thatVersionadopts the protocol. - Inside, a new
descriptionproperty satisfies the protocol's requirement.
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
- A
structis a named, custom type that bundles related values (its properties) and behavior (its methods). - Swift auto-generates a memberwise initializer for every struct -- you don't write one yourself unless you need custom behavior.
- A property is mutable only if both the property is declared
varand the holding variable is declaredvar. Bothlets lock you out. - Methods inside a struct can reference the struct's properties by name (no
self.required, though it's allowed). - Structs are value types: assigning a struct to a new variable copies it. Mutating one doesn't affect the other. This is the same behavior you've already seen with
Int,Double, andString-- because those are structs too. - Conforming to a protocol like
CustomStringConvertiblerequires implementing the protocol's properties or methods. In return, the rest of the standard library treats your type more usefully (here,print()callsdescription).
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.
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