Skip to content

Property wrappers, result builders, and macros

Swift can synthesize code from declarations and expressions. Property wrappers transform stored-property access. Result builders transform a closure’s statements into a value. Macros expand source during compilation through a compiler plugin.

These features remove repeated syntax, but they do not remove behavior. Read the expansion model before adopting the shorthand.

Property wrappers split storage from access

A wrapper type exposes wrappedValue. Applying @Clamped to a property creates wrapper-backed storage and routes reads and writes through that value:

struct Draft {
@Clamped(0...280) var summaryLength = 0
}

Conceptually, Swift synthesizes storage resembling _summaryLength: Clamped<Int> and a computed summaryLength that forwards to wrappedValue. A wrapper can also expose projectedValue through the dollar-prefixed form.

Wrappers work well for a repeated access policy such as clamping, dependency lookup, or observation integration. They are a poor hiding place for network calls, surprising persistence, or rules that need several properties at once. A domain initializer or method is clearer for cross-field invariants.

Result builders transform closure statements

The validation builder accepts rules as separate expressions:

let validation = rules {
Rule.titlePresent
Rule.bodyPresent
if includeLocation {
Rule.locationAllowed
}
}

The compiler feeds each expression through buildExpression, combines parts through buildBlock, and uses buildOptional for the conditional branch. The result is an ordinary [Rule].

Builders fit APIs where ordered, nested declarations are the domain, including SwiftUI view construction. They fit poorly when ordinary control flow or an array literal says the same thing more directly.

Macros expand syntax

Swift macros can be freestanding expressions or declarations, or attached roles that add peers, members, accessors, conformances, and other syntax around a declaration. A macro implementation runs in a compiler plugin and returns syntax. It does not receive unrestricted access to the running application’s state.

@Observable
final class EditorModel {
var title = ""
}

The useful debugging move is to inspect the expansion. In Xcode, expand the macro from the editor. In command-line package work, compiler flags and macro tests can expose generated source. Confirm what members and conformances exist rather than guessing from the attribute name.

Macros add package, compiler-plugin, diagnostic, and testing cost. Prefer a function, generic, protocol, or wrapper when it expresses the contract without source generation.

Compare the three tools

ToolInputOutput centerGood fit
property wrapperproperty declarationstorage and access behaviorrepeated single-property policy
result builderclosure statementsone constructed valuedeclarative nested content
macroSwift syntaxnew Swift syntaxrepeated patterns requiring compile-time generation

Run the checkpoint

idle

Execution sends this source to the project runner. It uses Swift 6.3.3 on Linux for standard-library code, not the Apple SDK, an iOS simulator, or a device.

Edit the source, then choose Run Swift. If no runner is configured, the source stays in this editor.

Compiler diagnostics

(none)

Standard output

(no stdout)

Standard error

(no stderr)

Expected output:

Clamped summary: 280
Rules: titlePresent, bodyPresent, locationAllowed

The standard-library source proves wrapper initialization and mutation plus result-builder expression, block, and optional transforms. It does not prove a macro plugin, SwiftSyntax compatibility, Xcode expansion UI, Observation, SwiftUI, Simulator, or device behavior.

Read generated behavior as an API

Generated syntax still affects access control, overload resolution, diagnostics, initialization, and compile time. Keep the public contract visible in names and documentation. Add focused tests around behavior users depend on, not snapshots of every generated token.

Avoid stacking several attributes until no reader can tell which feature supplied a member. Generated code is most useful when the expansion is predictable and the handwritten call site becomes easier to review.

Check your understanding

You should now be able to explain:

  • What backing storage a property wrapper introduces.
  • How builder methods correspond to closure syntax.
  • Why a builder is not a runtime parser.
  • What boundary executes a macro implementation.
  • Why inspecting expansion is part of debugging generated code.

The next post places Swift code behind module and package boundaries, then makes public API, implementation detail, and C or Objective-C interoperability explicit.

Series navigation

References

  • Generated declaration features: Attributes documents property wrappers, result builders, and macro attributes.
  • Macro model: Macros explains expansion roles and compiler-plugin behavior.
  • Builder transformation: Swift Evolution proposal SE-0289 specifies result-builder transforms.