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?

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 var properties even when the instance is declared with let, because let only 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, never let. 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, never let. 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 to newValue.
  • didSet — called just after the value changes. You get access to oldValue.

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 lazy properties, 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.