Skip to main content

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:

  1. Stored Properties
  2. Computed Properties
  3. Property Observers
  4. Type Properties (static properties)
  5. 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

swift
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:

swift
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:

swift
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

swift
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

swift
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

swift
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:

swift
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:

swift
// 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:

swift
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

  1. Create a Rectangle structure with stored properties for width and height, and computed properties for area and perimeter.

  2. Implement a Temperature structure that converts between Celsius, Fahrenheit, and Kelvin using computed properties.

  3. Create a BudgetTracker structure that uses property observers to log changes to an expense total.

  4. Design a UserPreference structure using property wrappers to persist user settings to UserDefaults.

  5. Build a GameCharacter structure using different property types to track health, experience, level, and abilities.

Additional Resources

Happy coding!



If you spot any mistakes on this website, please let me know at feedback@compilenrun.com. I’d greatly appreciate your feedback! :)