Properties in Swift: Stored, Computed, Lazy, and Property Observers
Every character in a Pixar movie has a stats card — Woody’s loyalty rating, Buzz’s fuel level, WALL-E’s compacted-cube count. Some stats are written on the card at birth and never change. Others are calculated on the fly: “total missions” might just be the sum of solo missions plus team missions. Swift properties work the same way — they attach data to your types, and they come in several flavors depending on how that data is stored and accessed.
This guide covers stored, computed, lazy, and observed properties, plus type-level (static) properties. We won’t dive into property wrappers — those get their own dedicated post.
What You’ll Learn
- What Are Properties?
- Stored Properties
- Computed Properties
- Lazy Properties
- Property Observers
- Type (Static) Properties
- Common Mistakes
- What’s Next?
What Are Properties?
A property is a value associated with a particular struct, class, or enumeration. Properties connect data to your types, giving each instance its own set of values.
There are two broad categories:
- Stored properties hold a value directly — like a field on a character’s stats card that was filled in when the card was created.
- Computed properties calculate a value on demand from other properties — like a “full name” field that combines first and last name every time you read it.
Think of it like a Pixar character profile. The character’s name is stored — it doesn’t change. But their fullTitle
(say, “Sheriff Woody”) is computed from their rank and name every time you ask for it.
Apple Docs:
Properties— The Swift Programming Language
Stored Properties
A stored property is a constant (let) or variable (var) that is stored as part of an instance. Every instance of
the type gets its own copy.
Let’s create a simple Pixar character:
struct Character {
let name: String // constant stored property
var energyLevel: Int // variable stored property
}
let woody = Character(name: "Woody", energyLevel: 100)
print(woody.name)
print(woody.energyLevel)
Woody
100
Here, name is a let property — it can never change after initialization. energyLevel is a var property — it can
be updated later.
Structs and let instances
There’s an important rule with structs: if you declare a struct instance with let, all of its properties become
immutable — even the var ones.
let buzz = Character(name: "Buzz", energyLevel: 80)
// buzz.energyLevel = 50 // ❌ Error: cannot assign to property of a 'let' constant
// Compiler error: Cannot assign to property: 'buzz' is a 'let' constant
To modify a struct’s var properties, the instance itself must be declared with var:
var buzz = Character(name: "Buzz", energyLevel: 80)
buzz.energyLevel = 50 // ✅ Works because 'buzz' is a var
print(buzz.energyLevel)
50
Note: This behavior is unique to structs (value types). Classes (reference types) allow mutation of
varproperties even when the instance is declared withlet, becauseletonly locks the reference, not the object it points to.
Computed Properties
A computed property doesn’t store a value. Instead, it provides a getter that calculates a value on demand, and an optional setter to update related properties.
Getter only
If a computed property only has a getter, you can omit the get keyword entirely:
struct ToyProfile {
let firstName: String
let rank: String
var fullTitle: String {
"\(rank) \(firstName)"
}
}
let woody = ToyProfile(firstName: "Woody", rank: "Sheriff")
print(woody.fullTitle)
Sheriff Woody
Every time you access fullTitle, Swift runs that code block and returns the result. Nothing is stored — it’s freshly
computed each time.
Getter and setter
You can add a set block to let callers write to a computed property. The setter receives a special newValue
parameter by default:
struct Rectangle {
var width: Double
var height: Double
var area: Double {
get {
width * height
}
set {
// Keep the aspect ratio by scaling width
width = newValue / height
}
}
}
var screen = Rectangle(width: 16, height: 9)
print(screen.area)
screen.area = 288
print(screen.width)
144.0
32.0
The getter returns width * height. The setter takes the new area value and recalculates width to match, keeping
height constant.
Tip: Computed properties must always be declared with
var, neverlet. Even if they’re read-only, the value isn’t fixed — it’s recalculated each time.
Lazy Properties
A lazy stored property is a property whose initial value isn’t calculated until the first time it’s used. You mark
it with the lazy keyword.
This is useful when the initial value is expensive to create, or depends on something that isn’t available until after initialization.
Imagine loading a heavy 3D scene for a Pixar movie — you don’t want to load it until the character actually enters that scene:
struct Movie {
let title: String
lazy var sceneData: String = {
print("Loading heavy scene data...")
return "Scene data for \(title)"
}()
}
var movie = Movie(title: "Toy Story")
print("Movie created")
print(movie.sceneData)
print(movie.sceneData)
Movie created
Loading heavy scene data...
Scene data for Toy Story
Scene data for Toy Story
Notice that “Loading heavy scene data…” only prints once — on the first access. The second access reuses the stored result. The loading work is deferred until you actually need the data.
Warning: Lazy properties must always be
var, neverlet. That’s because their value isn’t set during initialization — it’s filled in later, which requires mutability.
Property Observers
Property observers let you respond to changes in a property’s value. Swift provides two observers:
willSet— called just before the value changes. You get access tonewValue.didSet— called just after the value changes. You get access tooldValue.
This is perfect for logging, validation, or triggering side effects when a value changes:
struct ScoreTracker {
var score: Int = 0 {
willSet {
print("Score is about to change to \(newValue)")
}
didSet {
print("Score changed from \(oldValue) to \(score)")
}
}
}
var tracker = ScoreTracker()
tracker.score = 10
tracker.score = 25
Score is about to change to 10
Score changed from 0 to 10
Score is about to change to 25
Score changed from 10 to 25
Each time score is set, both observers fire in order: willSet first, then didSet. This gives you a chance to react
to every change — like a movie director yelling “Cut!” after every take.
Note: Observers are not called during initialization — only when the property is set after the instance is fully initialized.
Type (Static) Properties
All the properties above belong to individual instances. A type property belongs to the type itself — shared across
all instances. You declare one with the static keyword.
Think of it as the movie studio name — every Pixar character belongs to the same studio:
struct PixarCharacter {
static let studio = "Pixar Animation Studios"
static var totalCharacters = 0
let name: String
init(name: String) {
self.name = name
PixarCharacter.totalCharacters += 1
}
}
let woody = PixarCharacter(name: "Woody")
let buzz = PixarCharacter(name: "Buzz")
print(PixarCharacter.studio)
print(PixarCharacter.totalCharacters)
Pixar Animation Studios
2
studio and totalCharacters are accessed on the type PixarCharacter, not on any instance. The studio constant is
the same everywhere. The totalCharacters variable increments each time a new character is created.
Tip: Type properties are lazily initialized by default — they’re created the first time they’re accessed. And unlike instance
lazyproperties, they’re guaranteed to be initialized only once, even in multithreaded code.
Common Mistakes
Trying to use lazy with let
A common beginner mistake is declaring a lazy property with let:
struct Movie {
// ❌ Error: 'lazy' cannot be used on a let property
lazy let sceneData = "Heavy scene"
}
Lazy properties need to be mutable because they have no value at initialization time — the value is assigned later, on first access.
struct Movie {
// ✅ Lazy properties must be var
lazy var sceneData = "Heavy scene"
}
Infinite recursion in a computed property setter
If a computed property’s setter tries to assign to itself, you’ll create an infinite loop:
struct Score {
var points: Int {
get { points } // ❌ This calls the getter again — infinite recursion
set { points = newValue } // ❌ This calls the setter again — infinite recursion
}
}
A computed property doesn’t have its own storage. You need a separate stored property to back it:
struct Score {
private var _points: Int = 0
var points: Int {
get { _points }
set { _points = max(0, newValue) } // ✅ Validation with a backing store
}
}
Here, _points is the actual stored property, and points is a computed wrapper that adds validation (no negative
scores).
What’s Next?
- Stored properties hold values directly as part of an instance.
- Computed properties calculate values on demand using getters and optional setters.
- Lazy properties defer expensive initialization until first access — always use
var. - Property observers (
willSet/didSet) let you react to changes in a value. - Type (static) properties are shared across all instances of a type.
Now that you know how to attach data to your types, it’s time to give them behavior. Head over to Methods and Subscripts in Swift to learn how to add functions to your structs, classes, and enums.