In the previous chapter, every property of a struct was a plain stored value -- one slot in memory, set once at init, optionally mutable afterwards. That's the simplest kind of property, and the kind you'll use most often.
But Swift's property system goes further. Some properties don't store anything at all and instead compute their value on the fly. Some live on the type itself rather than on each instance. Some let you react when their value changes. And some delay their initialization until the moment they're first read. This chapter is a tour of all of those.
Stored Properties: the Baseline
A stored property holds a value. You've seen them already:
struct AppMetadata {
var displayName: String
let bundleID: String
var locale: String
}bundleID is let, so it's fixed at init. displayName and locale are var, so they can change after init -- if the surrounding instance is also a var. (Recall from Chapter 13 that mutability is gated in two places.)
var app = AppMetadata(
displayName: "Simple App Shipper",
bundleID: "com.example.shipper",
locale: "en-US"
)
app.displayName = "Shipper" // ✅
app.bundleID = "com.other" // ⛔ Cannot assign to 'let' constantDefault values
When you have a sensible fallback, give a property a default value right in the declaration:
struct AppMetadata {
var displayName: String
let bundleID: String
var locale: String = "en-US"
var primaryCategory: String = "Productivity"
}Swift updates the synthesized initializer accordingly. Parameters with defaults can be omitted at the call site:
let a = AppMetadata(displayName: "Notes", bundleID: "com.x")
// locale = "en-US", primaryCategory = "Productivity"
let b = AppMetadata(
displayName: "Tally",
bundleID: "com.y",
primaryCategory: "Finance"
)
// locale still defaults; primaryCategory overriddenYou can override any subset; the rest default. Use this to keep your initializer call sites short for the common case.
Computed Properties
Not every property has to store its value. Some can compute it from other properties every time they're read. These are called computed properties, and they're declared like methods returning a single value -- with braces instead of =:
struct ImageAsset {
var widthPx: Int
var heightPx: Int
var aspectRatio: Double {
Double(widthPx) / Double(heightPx)
}
}
let icon = ImageAsset(widthPx: 1024, heightPx: 1024)
icon.aspectRatio // 1.0
var hero = ImageAsset(widthPx: 1920, heightPx: 1080)
hero.aspectRatio // 1.777...
hero.widthPx = 2560
hero.aspectRatio // 2.370... -- recalculated automaticallyA few rules to internalize:
- A computed property must declare its type. The compiler can't infer it from the body in this context.
- A computed property must be declared
var.letdoesn't make sense here, since the value depends on inputs that may change. - Computed properties don't allocate memory. They're called like properties from the outside, but under the hood they execute the closure-like body every read.
From the caller's perspective hero.aspectRatio and hero.widthPx look identical -- both are dot-syntax reads. That's the point: the user doesn't need to know which slots are stored and which are derived.
Mini-exercise
Add a computed property megapixels: Double to ImageAsset that returns the total pixel count divided by one million. Test it on a 1920 × 1080 asset (you should get ~2.07).
Getters and Setters
The computed aspectRatio above is a read-only computed property -- you can read it, but assigning to it doesn't make sense. Sometimes, though, "writing" to a computed property does make sense: you write a value, and the property updates other stored properties to reflect that intent.
When you need both directions, split the body into an explicit get and set:
struct ImageAsset {
var widthPx: Int
var heightPx: Int
var aspectRatio: Double {
get {
Double(widthPx) / Double(heightPx)
}
set {
// newValue is the value being assigned in.
// Keep height the same; resize width to match.
widthPx = Int(Double(heightPx) * newValue)
}
}
}
var asset = ImageAsset(widthPx: 1080, heightPx: 1080)
asset.aspectRatio // 1.0 (square)
asset.aspectRatio = 16.0 / 9.0
asset.widthPx // 1920 -- updated automatically
asset.heightPx // 1080 -- unchangedThree things to know about setters:
- Inside
set, the implicit constantnewValueis the value being assigned. (You can rename it --set(newRatio) { ... }-- but the default name is fine.) - There's no
returnfrom a setter. It just mutates other stored properties. - Once you write an explicit
set, you must also write an explicitget.
This pattern is great for surfacing a friendlier API on top of multiple underlying properties: the caller assigns to one thing, and the struct quietly keeps the related fields in sync.
Type Properties
Every property you've defined so far has been an instance property -- each value of the struct has its own copy. But sometimes the property belongs to the type itself, not to any one instance.
Use static for that:
struct SubmissionQuota {
static let dailyLimit = 50
static var totalSubmissionsToday = 0
let appID: String
}dailyLimit and totalSubmissionsToday aren't on instances of SubmissionQuota -- they're on the type. Access them via the type name:
SubmissionQuota.dailyLimit // 50
SubmissionQuota.totalSubmissionsToday // 0
SubmissionQuota.totalSubmissionsToday += 1Type properties are perfect for:
- Constants that apply to every instance: max sizes, default values, configuration keys.
- Counters and registries: shared state every instance can read or update.
Inside an instance method, you can refer to a type property using either the type's name (SubmissionQuota.dailyLimit) or the implicit Self (Self.dailyLimit). Self (capital S, the type) is different from self (lowercase, the instance). Prefer Self -- if you rename the struct later, your code keeps working.
Property Observers: willSet and didSet
Sometimes you want to react when a stored property changes -- to update a related flag, log the change, or revert an out-of-range value. Swift gives you two hooks for that:
willSetruns just before the new value is committed.didSetruns just after.
Inside willSet, the constant newValue holds the about-to-be-assigned value. Inside didSet, the constant oldValue holds the previous value -- and the new value is already in the property.
A common use case: clamp a slider value into a legal range.
struct VolumeSlider {
var level: Int = 50 {
didSet {
if level < 0 { level = 0 }
if level > 100 { level = 100 }
}
}
}
var volume = VolumeSlider()
volume.level = 150
volume.level // 100 -- clamped
volume.level = -20
volume.level // 0 -- clampedAssigning to level inside didSet does not re-trigger didSet (Swift breaks the recursion for you), so this clamping is safe.
A didSet is also great for keeping derived state in sync. Here's a counter that flips an isPopular flag once it crosses a threshold:
struct DownloadCounter {
static let popularThreshold = 10_000
var count: Int = 0 {
didSet {
isPopular = count >= Self.popularThreshold
}
}
private(set) var isPopular = false
}(Don't worry about private(set) for now -- it just means outside code can read isPopular but only the struct itself can write to it.)
A few subtleties:
- Property observers only run when assigning to a fully initialized instance. They don't fire during init itself. (If you want to validate at init, do it in a custom initializer.)
- They only work on stored properties. For computed properties, fold the reaction logic into the
setblock. - The
oldValueparameter insidedidSetis implicit; you can also name it:didSet(previous) { ... }.
Mini-exercise
In the VolumeSlider above, an assignment of 150 is silently clamped to 100. Rewrite the struct so that before the assignment is committed, the code prints "Clamping volume from 150 to 100", then commits the clamped value. Hint: use willSet for the diagnostic, but you'll still need didSet to do the actual clamping. (willSet can't change newValue.)
Lazy Properties
Some properties are expensive to compute. If you don't always need them, computing them at init wastes work.
A lazy stored property defers its initialization until the first time someone reads it. The body runs at most once -- the result is then stored like any other property.
struct Project {
let filePath: String
lazy var wordCount: Int = {
// Pretend this opens the file and counts words.
// Could take hundreds of milliseconds.
print("Scanning \(filePath)...")
return computeWordCount(at: filePath)
}()
}
var draft = Project(filePath: "chapter.md")
// "Scanning chapter.md..." has NOT printed yet.
print(draft.wordCount)
// → prints "Scanning chapter.md..."
// → then the number
print(draft.wordCount)
// → prints just the number; no rescan.A few requirements:
- A lazy property must be
var. The compiler needs a writable slot to store the computed result on first access. (Even though, conceptually, it never changes after that first read.) - The instance holding a lazy property must itself be a
var. Reading the property mutates the storage (it writes the cached result), so aletinstance won't compile. - The right-hand side is usually written as a self-executing closure:
{ ... }(). The trailing()runs the closure immediately -- except, because oflazy, "immediately" really means "on first read."
Lazy properties are most useful when:
- The initial value is expensive (file I/O, network call, heavy computation).
- The initial value is only sometimes needed (e.g., a thumbnail that loads only when the detail view appears).
- The initial value depends on
self, which a default initializer can't reference.
Challenges
Challenge 1: a credit-pack price book
Define a CreditPack struct with two stored properties: credits: Int and priceUSD: Double. Add a read-only computed property costPerCredit: Double that returns priceUSD / Double(credits).
Test it on:
CreditPack(credits: 10_000, priceUSD: 4.99)-- should be ~0.000499CreditPack(credits: 200_000, priceUSD: 49.99)-- should be ~0.00025
Which pack is the better deal?
Challenge 2: temperature with get/set
Define a Thermometer struct with one stored property: celsius: Double. Add a computed property fahrenheit: Double with both a getter and a setter, so callers can read or write either unit and the struct keeps them in sync. Conversion formulas:
F = C * 9/5 + 32C = (F - 32) * 5/9
Verify: starting with celsius = 100, fahrenheit should be 212.0. Then assign fahrenheit = 32, and celsius should become 0.0.
Challenge 3: TestFlight slot enforcement
Apple caps external TestFlight builds at 10,000 testers. Model that with type properties and observers:
BetaProgramhas astatic let maxTesters = 10_000.- Each instance has a
testerCount: Intstored property with a default of0. - When
testerCountis set abovemaxTesters, clamp it back tomaxTestersand print"Capped at TestFlight limit". - Add a
static var totalEnrollments = 0that increments each time any instance'stesterCountgrows (usedidSetwitholdValueto detect a growth).
Key Points
- Stored properties allocate memory and hold a value. They can be
let(fixed at init) orvar(mutable). - Computed properties don't store anything -- they recompute on every read using other properties. They must be
varand must declare a type. - Getter + setter lets callers assign to a computed property, with the setter usually updating other stored properties. Inside the setter,
newValueis the assigned value. - Type properties (declared with
static) belong to the type itself, not to instances. They're shared state every instance can read or write. Inside a type, refer to them asSelf.propertyName. - Property observers (
willSet,didSet) let you react to changes on stored properties.newValueis available inwillSet,oldValueindidSet. They don't fire during initialization. - Lazy properties defer their initialization until first read, and cache the result. Useful for expensive or sometimes-needed values. They must be
var, and so must the instance holding them.
These tools are how a plain data bag becomes an actual API. In Chapter 15: Methods, you'll round out the picture -- the implicit self, custom initializers (and the gotcha where they swallow the memberwise init), mutating methods for value types, type-level methods that act as namespaces, and the extension keyword that lets you add behavior to types you don't own.
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