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 }
}

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!