Macros, property wrappers and result builders
Write a property wrapper, build a result builder, understand the macro system and its limits, and know how to read generated code when something breaks.
Property wrappers
@propertyWrapper
struct Clamped<Value: Comparable> {
private var value: Value
private let range: ClosedRange<Value>
init(wrappedValue: Value, _ range: ClosedRange<Value>) {
self.range = range
self.value = min(max(wrappedValue, range.lowerBound), range.upperBound)
}
var wrappedValue: Value {
get { value }
set { value = min(max(newValue, range.lowerBound), range.upperBound) }
}
var projectedValue: Clamped<Value> { self }
}
struct Settings {
@Clamped(0...100) var volume = 120 // stores 100
@Clamped(1...12) var columns = 0 // stores 1
}
var settings = Settings()
settings.volume = 250 // still 100
print(settings.$volume.wrappedValue) // access the wrapper itselfwrappedValueis what the property reads and writes;projectedValueis exposed with a leading$.- The wrapper is a value type, so mutation through
wrappedValuerequires the wrapper to bemutatingor a reference type. init(wrappedValue:)lets the property be initialised inline; other initialisers take configuration arguments.- Property wrappers add a layer at every access. In a hot loop that matters, which is why the standard library uses them sparingly.
Result builders
@resultBuilder
struct HTMLBuilder {
static func buildBlock(_ parts: String...) -> String {
parts.joined()
}
static func buildOptional(_ part: String?) -> String {
part ?? ""
}
static func buildEither(first part: String) -> String { part }
static func buildEither(second part: String) -> String { part }
static func buildArray(_ parts: [String]) -> String {
parts.joined()
}
static func buildExpression(_ text: String) -> String { text }
}
func page(@HTMLBuilder _ content: () -> String) -> String {
"<!doctype html><html><body>" + content() + "</body></html>"
}
let html = page {
"<h1>Reports</h1>"
if includeCharts {
"<figure id=\"chart\"></figure>"
}
for row in rows {
"<li>" + row + "</li>"
}
}| Builder method | Enables | Required |
|---|---|---|
buildBlock | Multiple statements | Yes |
buildOptional | if without an else | No |
buildEither | if and else, switch | No |
buildArray | for loops | No |
buildExpression | Converting an expression to the result type | No |
Macros and generated code
// A freestanding macro expands at the call site
let name = #function
let file = #filePath
let warning = #warning("remove before release")
// An attached macro augments a declaration: the compiler requires
// a peer declaration generated by a compiler plugin.
@freestanding(expression)
macro stringify<T>(_ value: T) -> (T, String) = #externalMacro(module: "MacrosImpl", type: "StringifyMacro")
@attached(member, names: named(init(from:)))
public macro Codable() = #externalMacro(module: "MacrosImpl", type: "CodableMacro")
// expand a macro to see exactly what it generated, without building:
// swiftc -dump-macro-expansions Sources/App/Model.swift
extension String {
// a macro can replace boilerplate but not control flow in the caller
var isNotEmpty: Bool { !isEmpty }
}⚠️
A macro is code that writes code, so a mistake produces generated declarations with confusing diagnostics pointing at the expansion rather than your source. Use
-dump-macro-expansions before debugging, and keep macros small and single-purpose.FAQ
Can a macro change control flow in the caller?
No. Macros are hygienic and their expansion is limited to the declaration they attach to, plus names you explicitly declare. Anything that needs to rewrite surrounding code has to be a function or a result builder.
Where do property wrappers make the most sense?
At a boundary you repeat everywhere: validation, unit conversion, persistence keys, and dependency injection. One wrapper removes the same boilerplate from dozens of properties and makes the rule enforceable in one place.
Related
Generics, opaque types and protocol design Memory management, ARC and performance
Last refreshed 2026-09-18.