Swift Properties Explained — Stored, Computed, Observers, and Wrappers
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
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.
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.
Infinite Recursion in property observers: Calling the property itself inside
willSetordidSetcauses infinite recursion. Always modify stored properties through their underlying storage, not through self.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
@Atomicor a dispatch queue for Thread Safety.Misunderstanding let with computed properties: You cannot declare a computed property with
letbecause computed properties must be recalculatable. Usevareven for read-only computed properties.
Practice Questions
- What is the difference between a stored property and a computed property?
- When would you use
lazy varinstead of a normal stored property? - What are the names of the old and new values in
willSetanddidSet? - How do property wrappers reduce code duplication?
- Challenge: Create a
@Loggedproperty 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
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