'Swift Macros in the Wild: Building Reusable SwiftUI Views with @expression'

'Swift Macros in the Wild: Building Reusable SwiftUI Views with @expression'

author: Wesley Matlock read_time: 5 —

While building the PushTo100 app, I kept running into the same boilerplate patterns — especially around buttons and consistent styling. I started wondering: What if I could turn these into reusable macros with real SwiftUI support?

I’d skimmed the WWDC docs and played around a bit, but PushTo100 was where macros finally clicked. It was the right project for experimenting with boilerplate reduction, keeping UI styles consistent, and speeding up dev cycles without cutting corners.

Fast-forward to now, and I’ve got a working SwiftUI app with custom macros — cutting boilerplate, speeding up development, and giving PushTo100 a consistent, styled UI without the usual repetition. They’re theme-aware, performance-measured, and built using the new @freestanding(expression) syntax—which lets you inject expressions directly into Swift code during compilation, perfect for declarative SwiftUI views. If you’re building SwiftUI views, this macro will bend your brain in the best way.

🫶 Quick thing before we keep going: If this post saves you a few lines of SwiftUI boilerplate or gets you thinking differently about macros, smashing that 👏 button (up to 50 times!) helps it reach more iOS devs. Seriously — thanks for reading.

Let’s build it.

🧱 Project Setup: Macros with SwiftPM

Starting with the new swift package macro template:

swift swift package init --type macro swift

This gives you the scaffolding for:

  • MacroTools: where your macros live
  • MacroToolsMacros: your macro implementation
  • MacroToolsTests: snapshot + diagnostics testing

Update your Package.swift like this:

swift .products: [ .macro(name: "MacroTools", targets: ["MacroToolsMacros"]), ], .targets: [ .macroTarget( name: "MacroToolsMacros", dependencies: [ .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), .product(name: "SwiftCompilerPlugin", package: "swift-syntax") ] ), .target(name: "MacroTools", dependencies: ["MacroToolsMacros"]), ] swift

Then in your SwiftUI app (PushTo100MacrosApp), add:

swift .package(path: "../MacroTools") swift

Everything’s wired up and ready — this is where the magic starts to feel real.

🧙‍♂️ The Macro: @ThemedButton

Here’s a Swift macro that generates a Button with your app’s theme baked in.

🔤 Macro Declaration

swift @freestanding(expression) public macro ThemedButton(_ title: String) -> some View = #externalMacro( module: "MacroToolsMacros", type: "ThemedButtonMacro" ) swift

🛠 Implementation

```swift import SwiftSyntax
import SwiftSyntaxMacros

public struct ThemedButtonMacro: ExpressionMacro {
public static func expansion(
of node: MacroExpansionExprSyntax,
in context: some MacroExpansionContext
) throws -> ExprSyntax {
guard let firstArg = node.argumentList.first?.expression.as(StringLiteralExprSyntax.self),
let label = firstArg.segments.first?.description else {
throw CustomMacroError(“Expected a string literal for the button title.”)
}
return “””
Button(action: {
print(“Tapped (raw: label)”)
}) {
Text((raw: label))
.padding()
.background(Color.accentColor)
.foregroundColor(.white)
.clipShape(Capsule())
}
“””
}
} ```swift

Simple, right? From this:

swift @ThemedButton("Tap Me") swift

You get a fully styled SwiftUI view — compact, reusable, and dialed in to match the rest of your app. Let’s keep building.

🎨 App Side: SwiftUI Theme Support

We’ll keep it light (or dark) with SwiftUI’s built-in theming tools.

```swift @main
struct PushTo100MacrosApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.preferredColorScheme(.dark)
}
}
}

// ContentView.swift
struct ContentView: View {
var body: some View {
VStack(spacing: 20) {
Text(“Welcome to PushTo100Macros!”)
.font(.title)

        @ThemedButton("Tap Me")  
    }  
    .padding()  
}   } ```swift

Clean. Custom. Theme-aware.

🔬 Advanced Macro: Expression Tree Parsing

Let’s go one level deeper: PushTo100 needed a smarter way to conditionally display views based on simple flags, so I used MacroExpansionContext and Syntax tree parsing to generate those views on the fly.

A @GreetingText(“name”) macro that wraps logic in view code.

swift @freestanding(expression) public macro GreetingText(_ name: String) -> some View = #externalMacro( module: "MacroToolsMacros", type: "GreetingTextMacro" ) swift

This one will parse the string and build a custom message:

swift Text("Hello, \(name)!") .font(.title2) .foregroundColor(.secondary) swift

In practice:

swift @GreetingText("Pushup Warrior") swift

Expands to that entire view block. You can even add token analysis here if you want to get fancy with formatting or conditions.

⚖️ Performance: Macros vs Computed Properties

Here’s what I measured using Instruments and Xcode build logs:

Computed var View

  • Compile Time: 0.38s
  • Render Time: ⚪️ Fast
  • Memory Usage: 🔵 Low

@ThemedButton Macro

  • Compile Time: 0.36s
  • Render Time: ⚪️ Fast
  • Memory Usage: 🟢 Slightly lower

While the difference might seem small in this tiny demo, the real gains show up when you scale to dozens — or hundreds — of components. Macro expansion happens at compile time, so you’re reducing runtime logic and cutting down on SwiftUI’s view-builder overload in large trees. It also means fewer symbols for the runtime to resolve, especially if you’re using heavy on closures or modifiers.

That said, the wins aren’t magical. You’ll notice the gains more in terms of maintainability and code cleanliness unless you’re hammering performance edge cases.

Takeaway:

  • Compile time was slightly faster using macros — thanks to expansion being done before type-checking.
  • Memory usage on render was marginally better (likely due to less runtime closure wrapping).
  • But debugging? That’s a different beast.

❌ When NOT to Use Macros

I’ve been burned. Here’s what to watch out for:

  • Stack traces become unreadable when crashes happen inside a macro-generated view.
  • Error messages point to expansion, not your actual Swift file.
  • Silent staleness: change your theme colors and forget the macro re-expands from cached code.
  • Breaks refactoring tools — Xcode can’t “see” macro-generated symbols cleanly.

🚫 Tip:

Use macros for static things, not logic that changes frequently or needs debug visibility.

✅ Macro Best Practices

  • Keep macro output small and explicit
  • Avoid nesting macros inside macros
  • Prefer macro + #Preview combo to see expansion. For example, you can add this right next to your macro declaration:

swift #Preview { @ThemedButton("Preview Me") .padding() .previewDisplayName("Macro Button Preview") } swift

  • Use .emit(diagnostic:at:) to show dev errors at call sites
  • Write snapshot tests for your macro output using MacroTesting

🧠 Final Thoughts

Swift macros are creeping into day-to-day dev workflows, code reviews, and architectural debates — and fast. They’re powerful. Fast. Declarative. But also dangerous if you don’t test and scope them properly.

So if you ever find yourself reaching for the same UI patterns over and over — like I did with PushTo100 — consider how macros could make that code faster to write and easier to read.
You’ll have a better answer than I did.

🎯 Bonus: More Real-World iOS Survival Stories

If you’re hungry for more tips, tricks, and a few battle-tested stories from the trenches of native mobile development, swing by my collection of articles:
👉 https://medium.com/@wesleymatlock

These posts are packed with real-world solutions, some laughs, and the kind of knowledge that’s saved me from a few late-night debugging sessions. Let’s keep building apps that rock — and if you’ve got questions or stories of your own, drop me a line. I’d love to hear from you.


Originally published on Medium