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
@StateObjectfor owning observable objects in views. - Use
@ObservedObjectfor passed-in observable objects. - Implement custom wrappers sparingly — only when the syntax-reduction is substantial.
- Use
projectedValue($) for binding-style patterns. - Use
@resultBuilderfor DSL syntax (rare in application code; conventional in libraries). - Use
@MainActorfor 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.