Skip to content

Swift Properties Explained — Stored, Computed, Observers, and Wrappers

DodaTech Updated 2026-06-28 8 min read

In this tutorial, you will learn about Swift Properties Explained. We cover key concepts, practical examples, and best practices to help you master this topic.

Swift properties are values associated with a type, instance, or type instance, enabling you to store data, compute values on the fly, observe changes, and wrap behavior around property access for cleaner and safer code.

What You'll Learn

  • Stored properties and their variations
  • Computed properties and how they derive values
  • Property observers (willSet and didSet)
  • Lazy properties for deferred initialization
  • Property wrappers for reusable property behavior
  • Real-world patterns used in iOS development

Why It Matters

Properties are the backbone of data management in Swift. Every app you build — whether a simple calculator or a complex social media platform — relies on properties to hold state. Understanding the full spectrum of Swift's property system lets you write safer, more expressive code that catches bugs at compile time rather than runtime.

Real-World Use

Consider a banking app that tracks account balances. You need a stored property for the current balance, a computed property for the formatted display string, a property observer to trigger fraud detection when the balance changes, and a lazy property to load the Transaction history only when the user taps "View History." Swift properties handle all of this natively.

Learning Path

flowchart LR
  A[Initializers
Lesson 10] --> B[Properties
You are here] B --> C[Inheritance
Lesson 12] B --> D[Protocols
Lesson 13] style B fill:#f90,color:#fff

Stored Properties

A stored property is a constant or variable that stores a value as part of an instance or type. Every stored property must be assigned an initial value either at declaration or in an initializer.

struct BankAccount {
    let accountNumber: String
    var balance: Double
    let currency: String = "USD"

    init(accountNumber: String, initialDeposit: Double) {
        self.accountNumber = accountNumber
        self.balance = initialDeposit
    }
}

var myAccount = BankAccount(accountNumber: "123456789", initialDeposit: 1000.0)
print("Account \(myAccount.accountNumber) opened with $\(myAccount.balance)")

Output: Account 123456789 opened with $1000.0

The accountNumber is a constant stored property — once set it cannot change. The balance is a variable stored property that can change over time. The currency has a default value so it doesn't need to be set in the initializer.

Type Stored Properties

You can also define stored properties on the type itself using the static keyword. These belong to the type, not any instance.

struct BankConfiguration {
    static let apiBaseURL = "https://api.bank.example.com"
    static var transactionFee: Double = 0.50
    static var isMaintenanceMode = false
}

print("API: \(BankConfiguration.apiBaseURL)")
BankConfiguration.transactionFee = 0.75
print("Fee updated to $\(BankConfiguration.transactionFee)")

Output:

API: https://api.bank.example.com
Fee updated to $0.75

Type stored properties are lazily initialized on first access and are shared across all instances.

Computed Properties

Computed properties do not store a value directly. Instead, they provide a getter and an optional setter to retrieve and set other properties indirectly.

struct Rectangle {
    var width: Double
    var height: Double

    var area: Double {
        return width * height
    }

    var perimeter: Double {
        get {
            return 2 * (width + height)
        }
        set(newPerimeter) {
            let side = newPerimeter / 4
            width = side
            height = side
        }
    }
}

var rect = Rectangle(width: 5.0, height: 3.0)
print("Area: \(rect.area)")
print("Perimeter: \(rect.perimeter)")
rect.perimeter = 16.0
print("New width: \(rect.width), New height: \(rect.height)")

Output:

Area: 15.0
Perimeter: 16.0
New width: 4.0, New height: 4.0

The area property is read-only — it only has a getter. The perimeter property has both a getter and a setter. When you set the perimeter, the setter adjusts both dimensions to form a square with that perimeter.

Read-Only Computed Properties

You can omit the get keyword for read-only computed properties:

struct Temperature {
    var celsius: Double

    var fahrenheit: Double {
        return celsius * 9 / 5 + 32
    }

    var kelvin: Double {
        return celsius + 273.15
    }
}

let temp = Temperature(celsius: 25.0)
print("\(temp.celsius)C = \(temp.fahrenheit)F = \(temp.kelvin)K")

Output: 25.0C = 77.0F = 298.15K

Property Observers

Property observers watch for changes to a stored property. You can respond to changes using willSet (called just before the value is set) and didSet (called immediately after).

struct Account {
    var balance: Double {
        willSet {
            print("Balance changing from \(balance) to \(newValue)")
        }
        didSet {
            if balance < 0 {
                print("Warning: Account overdrawn!")
                balance = 0
            } else if balance > 10000 {
                print("Large deposit detected — flagging for review")
            }
        }
    }

    init(balance: Double) {
        self.balance = balance
    }
}

var account = Account(balance: 500.0)
account.balance = 200.0
account.balance = 15000.0
account.balance = -50.0
print("Final balance: \(account.balance)")

Output:

Balance changing from 500.0 to 200.0
Balance changing from 200.0 to 15000.0
Large deposit detected — flagging for review
Balance changing from 15000.0 to -50.0
Warning: Account overdrawn!
Final balance: 0.0

Property observers are invaluable for validation, logging, and triggering side effects when data changes.

Lazy Properties

A lazy property's initial value is not calculated until the first time it is accessed. This is useful when the initial value is computationally expensive or depends on external factors.

struct ReportGenerator {
    let dataPoints: [Int]

    lazy var average: Double = {
        let sum = dataPoints.reduce(0, +)
        return Double(sum) / Double(dataPoints.count)
    }()

    lazy var sortedData: [Int] = {
        print("Sorting data...")
        return dataPoints.sorted()
    }()

    lazy var reportText: String = {
        print("Generating report...")
        return "Report: \(sortedData.count) items, average \(average)"
    }()

    init(dataPoints: [Int]) {
        self.dataPoints = dataPoints
        print("ReportGenerator initialized")
    }
}

var report = ReportGenerator(dataPoints: [7, 2, 9, 1, 5])
print("Created report object")
print("Average requested: \(report.average)")
print("Sorted data requested: \(report.sortedData)")
print("Report text: \(report.reportText)")

Output:

ReportGenerator initialized
Created report object
Average requested: 4.8
Sorting data...
Sorted data requested: [1, 2, 5, 7, 9]
Generating report...
Report: 5 items, average 4.8

The expensive operations only run when needed. If the user never accesses sortedData or reportText, the work is never done.

Property Wrappers

Property wrappers let you define reusable behavior that can be attached to any property. They encapsulate the getter/setter logic in a separate type.

@propertyWrapper
struct Clamped<T: Comparable> {
    private var value: T
    let minValue: T
    let maxValue: T

    init(wrappedValue: T, min: T, max: T) {
        self.minValue = min
        self.maxValue = max
        self.value = Swift.min(Swift.max(wrappedValue, min), max)
    }

    var wrappedValue: T {
        get { value }
        set { value = Swift.min(Swift.max(newValue, minValue), maxValue) }
    }
}

struct GameCharacter {
    @Clamped(min: 0, max: 100) var health: Double = 100.0
    @Clamped(min: 1, max: 99) var level: Int = 1
    var name: String
}

var hero = GameCharacter(name: "Archer")
hero.health = 150.0
hero.level = 105
print("\(hero.name) — Health: \(hero.health), Level: \(hero.level)")
hero.health = -20.0
print("After negative damage: Health: \(hero.health)")

Output:

Archer — Health: 100.0, Level: 99
After negative damage: Health: 0.0

Property wrappers keep your validation logic DRY and reusable across multiple properties and types.

Common Mistakes

  1. Forgetting to initialize stored properties: All stored properties must have an initial value by the time initialization completes. If you declare a stored property without a default, you must set it in an initializer.

  2. Using computed properties for expensive operations: Computed properties recalculate every time they are accessed. If the calculation is expensive, use a lazy stored property instead.

  3. Infinite Recursion in property observers: Calling the property itself inside willSet or didSet causes infinite recursion. Always modify stored properties through their underlying storage, not through self.

  4. Setting a lazy property from multiple threads: Lazy properties are not thread-safe by default. If accessed from multiple threads simultaneously, they may be initialized multiple times. Use @Atomic or a dispatch queue for Thread Safety.

  5. Misunderstanding let with computed properties: You cannot declare a computed property with let because computed properties must be recalculatable. Use var even for read-only computed properties.

Practice Questions

  1. What is the difference between a stored property and a computed property?
  2. When would you use lazy var instead of a normal stored property?
  3. What are the names of the old and new values in willSet and didSet?
  4. How do property wrappers reduce code duplication?
  5. Challenge: Create a @Logged property wrapper that prints every read and write access to a property, including the old and new values.

Mini Project

Build a ShoppingCart struct that uses:

  • A stored property for items (array of strings)
  • A computed property for the total item count
  • A property observer that logs every addition or removal
  • A lazy property for generating a receipt string
  • A property wrapper that clamps the item count between 0 and 50

FAQ

Can I add property observers to a computed property?

No. Property observers only apply to stored properties. Computed properties already control their getter and setter directly.

Are lazy properties thread-safe?

No. Lazy properties are not thread-safe by default. If accessed from multiple threads, the initializer may run multiple times. Use a synchronization mechanism for thread-safe lazy initialization.

What is the difference between lazy and computed?

A lazy property stores its value once and never recalculates. A computed property recalculates on every access. Use lazy for expensive one-time operations, computed for values that depend on changing state.

Can I use property wrappers with computed properties?

No. Property wrappers are only valid on stored properties. They intercept the get and set of stored storage, which computed properties do not have.

How do I create a read-only computed property?

Omit the setter and the get keyword: var area: Double { width * height }.

What's Next

Continue your Swift journey with Inheritance to learn how classes share and override properties, or explore Protocols to define property requirements that types must conform to.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro