Property wrappers and result builders

Swift · Advanced · 14 min

@State, @Binding and SwiftUI's declarative body are built on these two features.

A property wrapper packages the logic for storing a property. It is a type marked @propertyWrapper with a wrappedValue:

@propertyWrapper struct Clamped {
    var wrappedValue: Int { didSet { wrappedValue = min(max(wrappedValue, 0), 100) } }
}

Using @Clamped var volume = 50 makes the compiler store a hidden _volume: Clamped and route volume reads and writes to _volume.wrappedValue. A wrapper can expose a projected value through projectedValue, accessed with $volume; SwiftUI's @State uses this to hand out a Binding. Wrappers can take arguments: @Clamped(range: 0...10) var x = 5 (with a matching init(wrappedValue:range:)).

A result builder (@resultBuilder) transforms a closure's sequence of statements into a single value by calling static methods such as buildBlock, buildOptional (for if without else), buildEither (for if/else and switch) and buildArray (for for loops). SwiftUI's @ViewBuilder is a result builder: each view expression in body becomes an argument to buildBlock, which is why you list views without return or commas.

Writing your own builder is useful for DSLs: HTML generation, attributed strings, test fixtures. Keep in mind that inside a builder closure only expressions and supported control flow are allowed — arbitrary statements like guard aren't transformed.

Example

@propertyWrapper
struct Clamped {
    private var value: Int
    let range: ClosedRange<Int>
    init(wrappedValue: Int, _ range: ClosedRange<Int>) {
        self.range = range
        value = min(max(wrappedValue, range.lowerBound), range.upperBound)
    }
    var wrappedValue: Int {
        get { value }
        set { value = min(max(newValue, range.lowerBound), range.upperBound) }
    }
    var projectedValue: ClosedRange<Int> { range }
}
struct Speaker { @Clamped(0...10) var volume = 5 }
var s = Speaker()
s.volume = 42
print(s.volume, s.$volume)           // 10 0...10
@resultBuilder enum LinesBuilder {
    static func buildBlock(_ parts: String...) -> String { parts.joined(separator: "\n") }
    static func buildOptional(_ part: String?) -> String { part ?? "" }
}
func text(@LinesBuilder _ make: () -> String) -> String { make() }
let showFooter = false
print(text { "Title"; "Body"; if showFooter { "Footer" } })   // Title / Body / (empty)

Remember

  • A property wrapper's wrappedValue handles get/set; $name is its projected value
  • Result builders turn statement lists into one value via buildBlock & co.
  • @State and @ViewBuilder are these features in SwiftUI

Watch out for

  • Putting guard or other unsupported statements inside a result-builder closure.

Check yourself

  1. With `@State var count = 0` in SwiftUI, what is `$count`?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.swift.org/swift-book/documentation/the-swift-programming-language/attributes/