Swift Structure Properties
Properties are an essential part of Swift structures, allowing you to store and compute values associated with your data types. In this tutorial, we'll explore the different types of properties available in Swift structures and how to use them effectively.
Introduction to Structure Properties
Properties in Swift structures are values associated with the structure that help represent its state and behavior. They're a fundamental part of encapsulating data and functionality together.
Swift offers several types of properties for structures:
- Stored Properties
- Computed Properties
- Property Observers
- Type Properties (static properties)
- Property Wrappers
Each type serves a different purpose and provides unique capabilities for your structures.
Stored Properties
Stored properties are the most basic kind of property - they store a value as part of an instance of a structure.
Basic Stored Properties
struct Person {
var name: String
var age: Int
}
// Creating an instance with stored properties
var person = Person(name: "John", age: 28)
print(person.name) // Output: John
print(person.age) // Output: 28
// Modifying stored properties
person.age = 29
print(person.age) // Output: 29
Default Values
You can provide default values for stored properties:
struct Temperature {
var celsius: Double = 0.0
var fahrenheit: Double {
return celsius * 9 / 5 + 32
}
}
// Creating an instance with the default value
var temp = Temperature()
print(temp.celsius) // Output: 0.0
print(temp.fahrenheit) // Output: 32.0
// Creating an instance with a custom value
var hotTemp = Temperature(celsius: 100)
print(hotTemp.celsius) // Output: 100.0
print(hotTemp.fahrenheit) // Output: 212.0
Lazy Stored Properties
Lazy properties are initialized only when they're first accessed, which can help with performance:
struct ExpensiveResource {
init() {
print("Expensive resource initialized")
}
}
struct ResourceManager {
lazy var resource = ExpensiveResource()
var name: String
init(name: String) {
self.name = name
print("\(name) manager initialized")
}
}
var manager = ResourceManager(name: "Data")
// Output: Data manager initialized
print("Manager created, accessing resource...")
// Accessing the lazy property for the first time
_ = manager.resource
// Output: Expensive resource initialized
Computed Properties
Computed properties don't actually store values - they provide a getter and an optional setter to retrieve and set other properties indirectly.
Read-Only Computed Properties
struct Circle {
var radius: Double
var area: Double {
return .pi * radius * radius
}
var perimeter: Double {
return 2 * .pi * radius
}
}
let circle = Circle(radius: 5)
print(circle.area) // Output: 78.53981633974483
print(circle.perimeter) // Output: 31.41592653589793
Getter and Setter
struct Temperature {
var celsius: Double
var fahrenheit: Double {
get {
return celsius * 9 / 5 + 32
}
set {
celsius = (newValue - 32) * 5 / 9
}
}
}
var temp = Temperature(celsius: 25)
print(temp.fahrenheit) // Output: 77.0
temp.fahrenheit = 86
print(temp.celsius) // Output: 30.0
Property Observers
Property observers let you monitor changes to a property's value and respond to them.
willSet and didSet Observers
struct StepCounter {
var totalSteps: Int = 0 {
willSet {
print("About to set totalSteps to \(newValue)")
}
didSet {
if totalSteps > oldValue {
print("Added \(totalSteps - oldValue) steps")
}
}
}
}
var counter = StepCounter()
counter.totalSteps = 100
// Output: About to set totalSteps to 100
// Output: Added 100 steps
counter.totalSteps = 120
// Output: About to set totalSteps to 120
// Output: Added 20 steps
Type Properties
Type properties belong to the type itself rather than to instances of the type. In structures, we use the static
keyword:
struct SoundSettings {
static var volume: Int = 5
var deviceName: String
func playSound() {
print("Playing sound on \(deviceName) at volume \(SoundSettings.volume)")
}
static func resetVolume() {
volume = 5
print("Volume reset to \(volume)")
}
}
// Accessing static property
print(SoundSettings.volume) // Output: 5
// Modifying static property
SoundSettings.volume = 7
print(SoundSettings.volume) // Output: 7
// Static properties are shared across all instances
let phone = SoundSettings(deviceName: "iPhone")
let laptop = SoundSettings(deviceName: "MacBook")
phone.playSound() // Output: Playing sound on iPhone at volume 7
laptop.playSound() // Output: Playing sound on MacBook at volume 7
// Using static method
SoundSettings.resetVolume() // Output: Volume reset to 5
Property Wrappers
Property wrappers add a layer of behavior management to properties. They're especially useful when the same behavior is needed for multiple properties:
// A simple property wrapper that constrains values to a range
@propertyWrapper
struct Clamped<T: Comparable> {
private var value: T
private let range: ClosedRange<T>
init(wrappedValue: T, range: ClosedRange<T>) {
self.range = range
self.value = min(max(wrappedValue, range.lowerBound), range.upperBound)
}
var wrappedValue: T {
get { return value }
set { value = min(max(newValue, range.lowerBound), range.upperBound) }
}
}
struct Player {
@Clamped(range: 0...100)
var health: Int = 100
@Clamped(range: 0...20)
var strength: Int = 5
}
var player = Player()
print(player.health) // Output: 100
player.health = 150 // Tries to set health to 150
print(player.health) // Output: 100 (clamped to maximum)
player.health = -10 // Tries to set health to -10
print(player.health) // Output: 0 (clamped to minimum)
Real-World Application Example
Let's create a BankAccount
structure that demonstrates the practical use of different property types:
struct BankAccount {
// Stored Properties
var accountNumber: String
var accountHolder: String
private var _balance: Double = 0
// Type Property
static let interestRate: Double = 0.02
// Computed Property with getter and setter
var balance: Double {
get {
return _balance
}
set {
let transaction = Transaction(oldValue: _balance, newValue: newValue)
_balance = newValue
transactionHistory.append(transaction)
}
}
// Readonly computed property
var formattedBalance: String {
return "$\(String(format: "%.2f", _balance))"
}
// Lazy property
lazy var transactionHistory: [Transaction] = []
// Computed property that uses type property
var yearlyInterest: Double {
return _balance * BankAccount.interestRate
}
// Methods using these properties
mutating func deposit(_ amount: Double) {
if amount > 0 {
balance += amount
print("Deposited \(amount). New balance: \(formattedBalance)")
}
}
mutating func withdraw(_ amount: Double) {
if amount > 0 && amount <= _balance {
balance -= amount
print("Withdrew \(amount). New balance: \(formattedBalance)")
} else {
print("Insufficient funds")
}
}
func printStatement() {
print("---- Account Statement ----")
print("Account: \(accountNumber)")
print("Holder: \(accountHolder)")
print("Current Balance: \(formattedBalance)")
print("Annual Interest: \(String(format: "$%.2f", yearlyInterest))")
if !transactionHistory.isEmpty {
print("\nRecent Transactions:")
for (index, transaction) in transactionHistory.enumerated() {
print("\(index + 1). \(transaction.description)")
}
}
}
}
struct Transaction {
let timestamp = Date()
let oldValue: Double
let newValue: Double
var change: Double {
return newValue - oldValue
}
var description: String {
let formatter = DateFormatter()
formatter.dateStyle = .short
formatter.timeStyle = .short
let changeStr = change >= 0 ? "+\(change)" : "\(change)"
return "\(formatter.string(from: timestamp)): \(changeStr)"
}
}
// Using the BankAccount structure
var account = BankAccount(accountNumber: "123456789", accountHolder: "Jane Doe")
account.deposit(1000)
account.withdraw(250)
account.deposit(500)
account.printStatement()
// Accessing the computed property
print("Your balance is \(account.formattedBalance)")
Output:
Deposited 1000.0. New balance: $1000.00
Withdrew 250.0. New balance: $750.00
Deposited 500.0. New balance: $1250.00
---- Account Statement ----
Account: 123456789
Holder: Jane Doe
Current Balance: $1250.00
Annual Interest: $25.00
Recent Transactions:
1. 4/28/23, 10:15 AM: +1000.0
2. 4/28/23, 10:15 AM: -250.0
3. 4/28/23, 10:15 AM: +500.0
Your balance is $1250.00
This example demonstrates:
- Stored properties (
accountNumber
,accountHolder
,_balance
) - Computed properties (
balance
,formattedBalance
) - Property observers (implemented via the
balance
setter) - Lazy properties (
transactionHistory
) - Type properties (
interestRate
)
Summary
In this tutorial, we've explored the different types of properties available in Swift structures:
- Stored properties hold values as part of a structure instance
- Computed properties calculate values based on other properties
- Property observers react to changes in property values
- Type properties belong to the type itself rather than to instances
- Property wrappers encapsulate property behavior that can be reused
Understanding how to use these different property types effectively will help you create more powerful, flexible, and maintainable Swift structures.
Practice Exercises
-
Create a
Rectangle
structure with stored properties for width and height, and computed properties for area and perimeter. -
Implement a
Temperature
structure that converts between Celsius, Fahrenheit, and Kelvin using computed properties. -
Create a
BudgetTracker
structure that uses property observers to log changes to an expense total. -
Design a
UserPreference
structure using property wrappers to persist user settings to UserDefaults. -
Build a
GameCharacter
structure using different property types to track health, experience, level, and abilities.
Additional Resources
- Swift Documentation: Properties
- Apple's Swift Programming Language Guide
- Swift by Sundell: Property Wrappers
- Swift Evolution Proposals on Properties
Happy coding!
If you spot any mistakes on this website, please let me know at feedback@compilenrun.com. I’d greatly appreciate your feedback! :)