Polyglot
Languages Swift property wrappers
Swift § property-wrappers

Property wrappers

Property wrappers (Swift 5.1+) admit declarative property behaviour via the @propertyWrapper attribute. A property wrapper is a struct/class/enum that wraps a value and intercepts get/set operations; the conventional @Wrapper var name: T syntax admits substantial code reduction. Property wrappers are the foundation of SwiftUI’s state management (@State, @Published, @Binding, @Environment, @ObservedObject, @StateObject) — admit substantial declarative UI through their value-projection mechanism. Result builders (@resultBuilder, formerly function builders) admit DSL-style block syntax — the SwiftUI view-tree DSL is built on @ViewBuilder. The combination — property wrappers for declarative attribute extension, result builders for DSL syntax — is the substance of Swift’s compile-time metaprogramming surface.

Property wrappers

A property wrapper is a type with a wrappedValue:

@propertyWrapper
struct Capitalized {
    private var value: String = ""

    var wrappedValue: String {
        get { value }
        set { value = newValue.capitalized }
    }
}

struct Person {
    @Capitalized var name: String
}

var p = Person()
p.name = "alice"
print(p.name)                                      // "Alice"

The @Capitalized declaration intercepts assignments to name — the wrapper’s set capitalises the value before storing.

The expansion

The @Capitalized var name is sugar for:

struct Person {
    private var _name = Capitalized()
    var name: String {
        get { _name.wrappedValue }
        set { _name.wrappedValue = newValue }
    }
}

The wrapper provides the wrappedValue getter and setter; the storage is hidden in the wrapper.

Initial value

The wrapper’s init(wrappedValue:) admits initialisation from an inline value:

@propertyWrapper
struct Default<T> {
    let defaultValue: T
    private var value: T?

    init(wrappedValue: T) {
        self.defaultValue = wrappedValue
        self.value = nil
    }

    var wrappedValue: T {
        get { value ?? defaultValue }
        set { value = newValue }
    }
}

struct Config {
    @Default var name: String = "anonymous"        // calls init(wrappedValue:)
    @Default var port: Int = 8080
}

Projected value (projectedValue)

A wrapper may expose an additional value via projectedValue — accessed with $:

@propertyWrapper
struct Tracked<T> {
    private var value: T
    var changeCount: Int = 0

    init(wrappedValue: T) {
        self.value = wrappedValue
    }

    var wrappedValue: T {
        get { value }
        set {
            value = newValue
            changeCount += 1
        }
    }

    var projectedValue: Int {
        changeCount
    }
}

struct Counter {
    @Tracked var count: Int = 0
}

var c = Counter()
c.count = 1
c.count = 2
c.count = 3
print(c.count)                                     // 3
print(c.$count)                                    // 3 (the changeCount)

The $count accesses the projected value; the conventional pattern in SwiftUI for bindings.

Wrapper with custom init

A wrapper may admit substantial custom initialisation:

@propertyWrapper
struct Clamped<T: Comparable> {
    private var value: T
    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 { value }
        set { value = min(max(newValue, range.lowerBound), range.upperBound) }
    }
}

struct Settings {
    @Clamped(0...100) var volume: Int = 50
    @Clamped(0...1) var opacity: Double = 0.5
}

var s = Settings()
s.volume = 200                                     // clamped to 100
s.opacity = -1                                     // clamped to 0

Standard property wrappers

The Swift ecosystem admits substantial standard wrappers:

@Published (Combine)

import Combine

class ViewModel: ObservableObject {
    @Published var count: Int = 0
    @Published var items: [Item] = []
}

let vm = ViewModel()
vm.$count.sink { value in                          // projectedValue is a publisher
    print("count changed to \(value)")
}

@State (SwiftUI)

import SwiftUI

struct ContentView: View {
    @State private var count: Int = 0

    var body: some View {
        VStack {
            Text("Count: \(count)")
            Button("Increment") {
                count += 1
            }
        }
    }
}

The @State admits substantial declarative state management — SwiftUI rebuilds the view when count changes.

@Binding (SwiftUI)

struct CounterView: View {
    @Binding var count: Int

    var body: some View {
        Button("Increment: \(count)") {
            count += 1
        }
    }
}

struct ParentView: View {
    @State private var count: Int = 0

    var body: some View {
        CounterView(count: $count)                 // pass binding via $
    }
}

@Environment (SwiftUI)

struct ContentView: View {
    @Environment(\.colorScheme) var colorScheme
    @Environment(\.locale) var locale

    var body: some View {
        Text("Theme: \(colorScheme == .dark ? "dark" : "light")")
    }
}

@StateObject and @ObservedObject (SwiftUI)

class UserStore: ObservableObject {
    @Published var users: [User] = []
}

struct ContentView: View {
    @StateObject private var store = UserStore()   // owns the lifecycle

    var body: some View {
        UserList(store: store)
    }
}

struct UserList: View {
    @ObservedObject var store: UserStore           // does not own

    var body: some View {
        ForEach(store.users) { user in
            Text(user.name)
        }
    }
}

The conventional discipline:

  • @StateObject — for the owning view (initialises and retains).
  • @ObservedObject — for passing to child views (views observing).

Custom property wrappers — common patterns

@UserDefault for persistence

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

class Settings {
    @UserDefault(key: "username", defaultValue: "") var username: String
    @UserDefault(key: "theme", defaultValue: "light") var theme: String
}

@Atomic for thread-safe access

@propertyWrapper
final class Atomic<T> {
    private var value: T
    private let lock = NSLock()

    init(wrappedValue: T) {
        self.value = wrappedValue
    }

    var wrappedValue: T {
        get {
            lock.lock(); defer { lock.unlock() }
            return value
        }
        set {
            lock.lock(); defer { lock.unlock() }
            value = newValue
        }
    }
}

class Counter {
    @Atomic var count: Int = 0                     // thread-safe
}

@Logged for debugging

@propertyWrapper
struct Logged<T> {
    private var value: T
    let label: String

    init(wrappedValue: T, _ label: String) {
        self.value = wrappedValue
        self.label = label
    }

    var wrappedValue: T {
        get { value }
        set {
            print("[\(label)] \(value)\(newValue)")
            value = newValue
        }
    }
}

struct Counter {
    @Logged("count") var count: Int = 0
}

Result builders

The @resultBuilder admits DSL-style block syntax — collecting a series of expressions into a single result:

@resultBuilder
struct ListBuilder {
    static func buildBlock(_ items: String...) -> [String] {
        Array(items)
    }
}

func makeList(@ListBuilder _ builder: () -> [String]) -> [String] {
    builder()
}

let list = makeList {
    "first"
    "second"
    "third"
}
// ["first", "second", "third"]

The mechanism transforms the closure’s expressions into calls to buildBlock.

@ViewBuilder (SwiftUI)

The substantial use case — SwiftUI’s view tree DSL:

struct ContentView: View {
    var body: some View {                          // @ViewBuilder applied implicitly
        VStack {
            Text("Hello")
            Text("World")
            Button("Click") { }
        }
    }
}

The body is a @ViewBuilder closure; the multiple expressions are combined into a TupleView.

Result builder methods

A complete result builder admits:

@resultBuilder
struct ArrayBuilder {
    static func buildBlock(_ items: Int...) -> [Int] { Array(items) }

    // Optional support:
    static func buildOptional(_ items: [Int]?) -> [Int] { items ?? [] }

    // If/else support:
    static func buildEither(first: [Int]) -> [Int] { first }
    static func buildEither(second: [Int]) -> [Int] { second }

    // Loop support:
    static func buildArray(_ components: [[Int]]) -> [Int] { components.flatMap { $0 } }

    // Limited availability:
    static func buildLimitedAvailability(_ component: [Int]) -> [Int] { component }
}

func makeArray(@ArrayBuilder _ builder: () -> [Int]) -> [Int] {
    builder()
}

let result = makeArray {
    1
    2
    if condition {
        3
    } else {
        4
    }
    for n in 5...7 {
        n
    }
}

The mechanism admits substantial DSL flexibility.

Common patterns

@Published model

class TodoList: ObservableObject {
    @Published var items: [TodoItem] = []
    @Published var filter: Filter = .all

    var filteredItems: [TodoItem] {
        switch filter {
        case .all: return items
        case .active: return items.filter { !$0.isDone }
        case .completed: return items.filter { $0.isDone }
        }
    }
}

@State for view-local state

struct CounterView: View {
    @State private var count: Int = 0
    @State private var isShowingSheet: Bool = false

    var body: some View {
        VStack {
            Text("Count: \(count)")
            Button("Increment") { count += 1 }
            Button("Show Details") { isShowingSheet = true }
        }
        .sheet(isPresented: $isShowingSheet) {
            DetailView(count: $count)
        }
    }
}

Custom wrapper composition

struct UserSettings {
    @UserDefault(key: "name", defaultValue: "") var name: String
    @Clamped(0...100) var volume: Int = 50
    @Atomic var counter: Int = 0
}

Result builder for query DSL

@resultBuilder
struct QueryBuilder {
    static func buildBlock(_ conditions: Condition...) -> Query {
        Query(conditions: Array(conditions))
    }
}

struct Query {
    let conditions: [Condition]
}

struct Condition {
    let field: String
    let op: String
    let value: Any
}

func find(@QueryBuilder _ builder: () -> Query) -> Query {
    builder()
}

let q = find {
    Condition(field: "active", op: "=", value: true)
    Condition(field: "role", op: "=", value: "admin")
}

Combine @Published for substantial reactivity

class SearchViewModel: ObservableObject {
    @Published var query: String = ""
    @Published var results: [Result] = []

    private var cancellables: Set<AnyCancellable> = []

    init() {
        $query
            .debounce(for: .milliseconds(300), scheduler: DispatchQueue.main)
            .removeDuplicates()
            .flatMap { query in
                self.search(query: query)
            }
            .receive(on: DispatchQueue.main)
            .assign(to: &$results)
    }

    func search(query: String) -> AnyPublisher<[Result], Never> { /* ... */ }
}

The pattern is substantial in SwiftUI/Combine architectures.

@Environment for substantial dependency injection

struct EnvironmentKey: SwiftUI.EnvironmentKey {
    static let defaultValue: APIClient = APIClient.shared
}

extension EnvironmentValues {
    var apiClient: APIClient {
        get { self[EnvironmentKey.self] }
        set { self[EnvironmentKey.self] = newValue }
    }
}

struct ContentView: View {
    @Environment(\.apiClient) var client

    var body: some View {
        // use client
    }
}

Wrapper with multiple stored values

@propertyWrapper
struct Validated<T, V: Validator> {
    private var value: T
    let validator: V

    init(wrappedValue: T, validator: V) {
        self.validator = validator
        self.value = wrappedValue
    }

    var wrappedValue: T {
        get { value }
        set {
            guard validator.isValid(newValue) else {
                fatalError("Invalid value")
            }
            value = newValue
        }
    }

    var projectedValue: V {
        validator
    }
}

@MainActor (concurrency)

The @MainActor is not a property wrapper but admits a similar declarative form for actor isolation:

@MainActor
class ViewModel: ObservableObject {
    @Published var data: [Item] = []

    func update() async {                          // implicitly main-actor isolated
        let fetched = try? await fetch()
        self.data = fetched ?? []
    }
}

Treated in Concurrency.

A note on the conventional discipline

The contemporary Swift property-wrappers/result-builders advice:

  • Use property wrappers for declarative attribute extension.
  • Use SwiftUI’s standard wrappers (@State, @Binding, @Published, @Environment).
  • Use @StateObject for owning observable objects in views.
  • Use @ObservedObject for passed-in observable objects.
  • Implement custom wrappers sparingly — only when the syntax-reduction is substantial.
  • Use projectedValue ($) for binding-style patterns.
  • Use @resultBuilder for DSL syntax (rare in application code; conventional in libraries).
  • Use @MainActor for UI-related classes.

The combination — property wrappers for declarative attribute extension, result builders for DSL syntax, the SwiftUI integration via standard wrappers, the Combine integration via @Published — is the substance of Swift’s declarative metaprogramming surface. The discipline admits substantial code reduction in framework-driven contexts (SwiftUI, Combine); pure-Swift application code typically uses fewer custom wrappers.