Swift Computed Properties
Introduction
In Swift, properties are a fundamental concept that allows you to associate values with a particular class, structure, or enumeration. While stored properties save a value as part of an instance, computed properties calculate a value on the fly. Think of computed properties as methods that look like properties—they run code to determine their value rather than simply retrieving a stored value.
Computed properties are particularly useful when a value depends on other properties or when you want to provide a custom getter and setter for a value without directly exposing the underlying data.
Understanding Computed Properties
The Basics
A computed property doesn't actually store a value. Instead, it provides a getter (and optionally a setter) that retrieves and sets other properties indirectly. Let's start with a simple example:
struct Circle {
var radius: Double
// Computed property
var area: Double {
return Double.pi * radius * radius
}
}
// Create a circle instance
let circle = Circle(radius: 5.0)
// Access the computed property
print("Circle area: \(circle.area)")
// Output: Circle area: 78.53981633974483
In this example, area
is a computed property that calculates the area of the circle based on its radius. The area isn't stored anywhere—it's calculated each time you access the property.
Syntax of Computed Properties
The basic syntax for a computed property is:
var propertyName: Type {
get {
// Code to calculate and return the value
}
set(newValue) {
// Code to set other values based on the new value
}
}
The get
block is mandatory, while the set
block is optional. If you only provide a getter, you can simplify the syntax:
var propertyName: Type {
// Code to calculate and return the value
}
Read-Only Computed Properties
When you only need to retrieve a calculated value without setting it, you can create a read-only computed property:
struct Rectangle {
var width: Double
var height: Double
// Read-only computed property
var area: Double {
return width * height
}
var perimeter: Double {
return 2 * (width + height)
}
}
let rectangle = Rectangle(width: 10, height: 5)
print("Rectangle area: \(rectangle.area)")
// Output: Rectangle area: 50.0
print("Rectangle perimeter: \(rectangle.perimeter)")
// Output: Rectangle perimeter: 30.0
In this example, both area
and perimeter
are read-only computed properties that calculate their values based on the rectangle's width and height.
Read-Write Computed Properties
To create a property that can be both read and written to, you need to implement both a getter and a setter:
struct Temperature {
// Stored property
var celsius: Double
// Read-write computed property
var fahrenheit: Double {
get {
return celsius * 9/5 + 32
}
set {
celsius = (newValue - 32) * 5/9
}
}
}
var temp = Temperature(celsius: 25.0)
print("Celsius: \(temp.celsius)°C")
// Output: Celsius: 25.0°C
print("Fahrenheit: \(temp.fahrenheit)°F")
// Output: Fahrenheit: 77.0°F
// Set the fahrenheit value
temp.fahrenheit = 86.0
print("Updated Celsius: \(temp.celsius)°C")
// Output: Updated Celsius: 30.0°C
In this example, fahrenheit
is a read-write computed property. When we get the value, it converts from Celsius to Fahrenheit. When we set the value, it updates the celsius
property by converting from Fahrenheit.
Notice how the setter receives a default parameter called newValue
. You can also specify a custom parameter name:
var fahrenheit: Double {
get {
return celsius * 9/5 + 32
}
set(newFahrenheit) {
celsius = (newFahrenheit - 32) * 5/9
}
}
Property Observers with Computed Properties
You can't directly add property observers to computed properties because you can already include any required logic in the getter and setter. However, you can observe changes to the stored properties that your computed property depends on:
struct Progress {
var task: String
var amount: Double {
didSet {
amount = min(max(amount, 0), 1)
print("Progress for \(task) updated to \(Int(amount * 100))%")
}
}
var formattedPercentage: String {
return "\(Int(amount * 100))%"
}
}
var progress = Progress(task: "Loading data", amount: 0.0)
progress.amount = 0.5
// Output: Progress for Loading data updated to 50%
print(progress.formattedPercentage)
// Output: 50%
Here, formattedPercentage
is a read-only computed property that formats the progress percentage, while amount
is a stored property with an observer.
Practical Applications
Example 1: Personal Finance
Let's create a simple budget tracker using computed properties:
struct Budget {
var income: Double
var expenses: [String: Double]
var totalExpenses: Double {
return expenses.values.reduce(0, +)
}
var balance: Double {
return income - totalExpenses
}
var summary: String {
return """
Income: $\(String(format: "%.2f", income))
Expenses: $\(String(format: "%.2f", totalExpenses))
Balance: $\(String(format: "%.2f", balance))
"""
}
}
var myBudget = Budget(income: 5000, expenses: [
"Rent": 1500,
"Food": 600,
"Utilities": 200,
"Entertainment": 300
])
print(myBudget.summary)
// Output:
// Income: $5000.00
// Expenses: $2600.00
// Balance: $2400.00
// Adding a new expense
myBudget.expenses["Transportation"] = 150
print("Updated balance: $\(String(format: "%.2f", myBudget.balance))")
// Output: Updated balance: $2250.00
This example demonstrates how computed properties can provide useful derived values based on the underlying data.
Example 2: User Profile
Computed properties can help manage user information:
struct User {
var firstName: String
var lastName: String
var fullName: String {
return "\(firstName) \(lastName)"
}
var initials: String {
let firstInitial = firstName.first?.uppercased() ?? ""
let lastInitial = lastName.first?.uppercased() ?? ""
return "\(firstInitial)\(lastInitial)"
}
// A computed property with setter that updates the first and last name
var displayName: String {
get {
return fullName
}
set {
let components = newValue.components(separatedBy: " ")
if components.count > 0 {
firstName = components[0]
}
if components.count > 1 {
lastName = components.dropFirst().joined(separator: " ")
}
}
}
}
var user = User(firstName: "John", lastName: "Doe")
print("Full name: \(user.fullName)")
// Output: Full name: John Doe
print("Initials: \(user.initials)")
// Output: Initials: JD
// Change the display name
user.displayName = "Jane Smith"
print("Updated first name: \(user.firstName)")
// Output: Updated first name: Jane
print("Updated last name: \(user.lastName)")
// Output: Updated last name: Smith
This example demonstrates how computed properties can transform data and even update multiple stored properties when set.
Best Practices
-
Use computed properties when the value depends on other properties. This ensures the value is always up-to-date.
-
Keep computation simple. Since computed properties are evaluated every time they're accessed, complex calculations can affect performance.
-
Consider caching expensive computations if they're accessed frequently but change infrequently.
-
Use read-only computed properties when you only need to derive a value from other properties.
-
Remember that computed properties cannot be constants (
let
). They must be declared withvar
.
Common Pitfalls
-
Infinite recursion: Be careful not to create cycles where a computed property accesses itself.
swift// Problematic code - causes infinite recursion
var badProperty: String {
return badProperty + "!" // Calls itself endlessly!
} -
Side effects: Avoid side effects in getters, as they may be called more often than you expect.
-
Performance concerns: Remember that computed properties recalculate each time they're accessed. For complex calculations, consider using stored properties with property observers instead.
Summary
Computed properties are a powerful feature in Swift that allow you to:
- Calculate values dynamically based on other properties
- Provide read-only or read-write access to derived values
- Create clean and intuitive interfaces for your structures and classes
- Encapsulate transformation logic within the property itself
They are particularly useful when a value is derived from other properties and needs to always reflect the current state of those properties.
Exercises
-
Create a
Person
structure withbirthYear
as a stored property andage
as a computed property that calculates the person's current age. -
Extend the
Rectangle
structure to include computed properties fordiagonal
(the length of the diagonal) andaspectRatio
(width divided by height). -
Create a
BankAccount
structure with stored properties forbalance
andinterestRate
, and a computed property that calculates the interest earned over a specified number of months. -
Create a
ShoppingCart
structure that contains an array ofItem
objects, with computed properties fortotalPrice
,itemCount
, andaverageItemPrice
. -
Build a
Color
structure with RGB values as stored properties and computed properties for HSL (Hue, Saturation, Lightness) values.
Additional Resources
- Swift Documentation: Properties
- Apple Developer: Swift Properties
- Swift by Sundell: Computed Properties
- Hacking with Swift: Property Observers
Happy coding with Swift computed properties!
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)