Lesson 96Advanced Topics
Property Wrappers
Reusable Property Logic
Property wrappers let you extract common property behavior into reusable types. Define once, use everywhere!
Syntax
@propertyWrapper
struct MyWrapper {
var wrappedValue: T { get set }
}
struct MyWrapper {
var wrappedValue: T { get set }
}
Common Use Cases
@Clamped
Keep values in range
@Trimmed
Clean whitespace
@UserDefault
Persist to storage
@Capitalized
Format strings
Projected Value ($)
Access extra info via $propertyName. Used for history, binding, or metadata.
var projectedValue: SomeType { ... }
SwiftUI Wrappers
@State- Local view state@Binding- Two-way data flow@Published- Observable property@StateObject- View-owned observable@Environment- System values
main.swift
// PROPERTY WRAPPERS
// Add behavior to properties with reusable logic
// ===== BASIC PROPERTY WRAPPER =====
@propertyWrapper
struct Clamped<T: Comparable> {
var value: T
let range: ClosedRange<T>
var wrappedValue: T {
get { value }
set { value = min(max(newValue, range.lowerBound), range.upperBound) }
}
init(wrappedValue: T, _ range: ClosedRange<T>) {
self.range = range
self.value = min(max(wrappedValue, range.lowerBound), range.upperBound)
}
}
struct Player {
@Clamped(0...100) var health: Int = 100
@Clamped(0...10) var level: Int = 1
}
var player = Player()
player.health = 150 // Clamped to 100
player.health = -10 // Clamped to 0
print(player.health) // 0
// ===== PROJECTED VALUE ($) =====
@propertyWrapper
struct Logged<T> {
private var value: T
private(set) var history: [T] = []
var wrappedValue: T {
get { value }
set {
history.append(value)
value = newValue
}
}
var projectedValue: [T] { history }
init(wrappedValue: T) {
self.value = wrappedValue
}
}
struct Settings {
@Logged var volume: Int = 50
}
var settings = Settings()
settings.volume = 75
settings.volume = 100
print(settings.$volume) // [50, 75] - access history via $
// ===== USER DEFAULTS WRAPPER =====
@propertyWrapper
struct UserDefault<T> {
let key: String
let defaultValue: T
var wrappedValue: T {
get {
UserDefaults.standard.object(forKey: key) as? T ?? defaultValue
}
set {
UserDefaults.standard.set(newValue, forKey: key)
}
}
}
struct AppSettings {
@UserDefault(key: "isDarkMode", defaultValue: false)
static var isDarkMode: Bool
@UserDefault(key: "username", defaultValue: "Guest")
static var username: String
}
AppSettings.isDarkMode = true
print(AppSettings.isDarkMode) // true (persisted)
// ===== TRIMMED STRING =====
@propertyWrapper
struct Trimmed {
private var value: String = ""
var wrappedValue: String {
get { value }
set { value = newValue.trimmingCharacters(in: .whitespacesAndNewlines) }
}
init(wrappedValue: String) {
self.wrappedValue = wrappedValue
}
}
struct User {
@Trimmed var name: String
@Trimmed var email: String
}
var user = User(name: " John ", email: " [email protected] ")
print(user.name) // "John"
print(user.email) // "[email protected]"
// ===== LAZY INITIALIZATION =====
@propertyWrapper
struct LateInit<T> {
private var value: T?
var wrappedValue: T {
get {
guard let value = value else {
fatalError("Property accessed before initialization")
}
return value
}
set { value = newValue }
}
init() { }
}
class ViewController {
@LateInit var label: String // Must be set before use
}
// ===== CAPITALIZED =====
@propertyWrapper
struct Capitalized {
private var value: String = ""
var wrappedValue: String {
get { value }
set { value = newValue.capitalized }
}
init(wrappedValue: String) {
self.wrappedValue = wrappedValue
}
}
struct Person {
@Capitalized var firstName: String
@Capitalized var lastName: String
}
var person = Person(firstName: "john", lastName: "DOE")
print(person.firstName) // "John"
print(person.lastName) // "Doe"
// ===== COMMON SWIFTUI PROPERTY WRAPPERS =====
// @State - Local state for views
// @Binding - Two-way connection to state
// @Published - Observable property for classes
// @ObservedObject - Observe external ObservableObject
// @StateObject - Create and own ObservableObject
// @Environment - Access environment values
// @AppStorage - UserDefaults in SwiftUI
// ===== COMBINING WRAPPERS =====
// Note: Multiple wrappers apply from inside out
struct Profile {
@Capitalized @Trimmed var displayName: String = ""
}Try It Yourself!
Create an @Encrypted property wrapper that stores strings in an encrypted format!