Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Why Can't We Have Nice Macros for #Preview?

Sponsored · Your Podcast. Everywhere. Effortlessly. Share. Educate. Inspire. Entertain. You do you. We'll handle the rest.

Why Can't We Have Nice Macros for #Preview?

Avatar for Yu Takahashi

Yu Takahashi

July 26, 2026

More Decks by Yu Takahashi

Other Decks in Programming

Transcript

  1. Why Can't We Have Nice Macros for #Preview? 🇯🇵 Yu

    Takahashi 2026/07/26 iPlayground 2026
  2. Nicer Previews for SwiftData struct ContentView: View { @Environment(\.modelContext) var

    modelContext @Query var items: [Item] ... } Why Can't We Have Nice Macros for #Preview? 2 / 21
  3. Nicer Previews for SwiftData struct ContentView: View { @Environment(\.modelContext) var

    modelContext @Query var items: [Item] ... } #Preview { let config = ModelConfiguration(isStoredInMemoryOnly: true) let container = try! ModelContainer( for: Item.self, configurations: config ) let item = Item(name: "Test Item") container.mainContext.insert(item) } return ContentView() .modelContainer(container) Why Can't We Have Nice Macros for #Preview? 3 / 21
  4. Nicer Previews for SwiftData @PreviewContent struct ContentView: View { @Environment(\.modelContext)

    var modelContext @Query var items: [Item] ... } Why Can't We Have Nice Macros for #Preview? 4 / 21
  5. Nicer Previews for SwiftData @PreviewContent struct ContentView: View { @Environment(\.modelContext)

    var modelContext @Query var items: [Item] ... } // Generated struct ContentView_Content: View { @Environment(\.modelContext) var modelContext let items: [Item] ... } Why Can't We Have Nice Macros for #Preview? 5 / 21
  6. Nicer Previews for SwiftData @PreviewContent struct ContentView: View { @Environment(\.modelContext)

    var modelContext @Query var items: [Item] ... } // Generated struct ContentView_Content: View { @Environment(\.modelContext) var modelContext let items: [Item] ... } #Preview { ContentView_Content(items: [.init(name: "Test Item")]) } Why Can't We Have Nice Macros for #Preview? 6 / 21
  7. It doesn't compile @PreviewContent struct ContentView: View { @Environment(\.modelContext) var

    modelContext @Query var items: [Item] ... } // Generated struct ContentView_Content: View { @Environment(\.modelContext) var modelContext let items: [Item] ... } #Preview { ContentView_Content(items: [.init(name: "Test Item")]) } Cannot nd 'ContentView_Content' in scope fi Why Can't We Have Nice Macros for #Preview? 7 / 21
  8. It doesn't compile struct ContentView: View { @Environment(\.modelContext) var modelContext

    @Query var items: [Item] ... } // Hand-written struct ContentView_Content: View { @Environment(\.modelContext) var modelContext let items: [Item] ... } #Preview { ContentView_Content(items: [.init(name: "Test Item")]) } Why Can't We Have Nice Macros for #Preview? 8 / 21
  9. Not just me extension Observable { func foo() {} }

    @Observable class Model {} #Preview { Model().foo() // ✅ Model().access(keyPath: \.self) // 🛑 } https://forums.swift.org/t/macro-evaluation-order/79862 Why Can't We Have Nice Macros for #Preview? 9 / 21
  10. Basics of Swift Macros freestanding macros (#Preview) expand in place,

    produce an expression or declaration attached macros (@Observable) attach to existing declarations and augments Why Can't We Have Nice Macros for #Preview? 10 / 21
  11. Basics of Swift Macros freestanding macros (#Preview) expand in place,

    produce an expression or declaration attached macros (@Observable) attach to existing declarations and augments peer : adds a declaration alongside member : adds members (properties or methods) ... Why Can't We Have Nice Macros for #Preview? 11 / 21
  12. Basics of Swift Macros // attached macro @PreviewContent struct ContentView:

    View { @Environment(\.modelContext) var modelContext @Query var items: [Item] ... } // Generated by the peer attached macro above struct ContentView_Content: View { @Environment(\.modelContext) var modelContext let items: [Item] ... } // freestanding macro #Preview { ContentView_Content(items: [.init(name: "Test Item")]) } Why Can't We Have Nice Macros for #Preview? 12 / 21
  13. Basics of Swift Macros // attached macro @PreviewContent struct ContentView:

    View { @Environment(\.modelContext) var modelContext @Query var items: [Item] ... } // Generated by the peer attached macro above struct ContentView_Content: View { @Environment(\.modelContext) var modelContext let items: [Item] ... } // freestanding macro #Preview(body: { ContentView_Content(items: [.init(name: "Test Item")]) // inside the argument }) Why Can't We Have Nice Macros for #Preview? 13 / 21
  14. Macro expansion phases Expression Macros proposal (SE-0382) 1st phase Type-checks

    macros arguments 2nd phase Expands macros & type-checks them When a macro expansion is encountered in the source code, its expansion occurs in two phases. The first phase is the type-check phase, where the arguments to the macro are type-checked against the parameters of the named macro, and the result type of the named macro is checked against the context in which the macro expansion occurs. … The second phase is the macro expansion phase, during which the syntax of the macro arguments is provided to the macro de nition. fi ’ Why Can't We Have Nice Macros for #Preview? 14 / 21
  15. The root cause 1st phase 2nd phase @PreviewContent struct ContentView:

    View { @Query var items: [Item] ... } @PreviewContent struct ContentView: View { @Query var items: [Item] ... } struct ContentView_Content: View { let items: [Item] ... } struct ContentView_Content: View { let items: [Item] ... } #Preview(body: { ContentView_Content(items: ...) }) #Preview(body: { ContentView_Content(items: ...) }) ? Why Can't We Have Nice Macros for #Preview? 15 / 21
  16. But wait... Differences @attached(extension, conformances: Observable) macro Observable() foo() @attached(member,

    names: named(access)) macro Observable() • A part of @Observable • Via conformances extension Observable { func foo() {} } @Observable class Model {} #Preview { Model().foo() // ✅ Model().access(keyPath: \.self) // 🛑 } Why Can't We Have Nice Macros for #Preview? 16 / 21
  17. But wait... Differences @attached(extension, conformances: Observable) macro Observable() foo() @attached(member,

    names: named(access)) macro Observable() • A part of @Observable extension Observable { func foo() {} } • Via conformances access @Observable class Model {} • A product of @Observable • Via names #Preview { Model().foo() // ✅ Model().access(keyPath: \.self) // 🛑 } Why Can't We Have Nice Macros for #Preview? 17 / 21
  18. Is this a bug? No, this is the design. To

    address these issues, a name introduced by a macro expansion is not visible within macro arguments for macros the same scope as the macro expansion or any of its enclosing scopes. [from SE-0389] https://github.com/swiftlang/swift-evolution/blob/main/proposals/0389attached-macros.md#visibility-of-names-used-and-introduced-by-macros Why Can't We Have Nice Macros for #Preview? 18 / 21
  19. Is this a bug? Issues (from SE-0389) 1. Self-invalidation •

    Expansion could break its own argument's type-check 2. Order dependency • Expansion order would change code meaning • Swift doesn t prefer order dependencies 3. Expansion cost • Macros should be expanded only once https://github.com/swiftlang/swift-evolution/blob/main/proposals/0389attached-macros.md#visibility-of-names-used-and-introduced-by-macros ’ Why Can't We Have Nice Macros for #Preview? 19 / 21
  20. What we can do today @PreviewContent struct ContentView: View {

    @Environment(\.modelContext) var modelContext @Query var items: [Item] ... } // Generated struct ContentView_Content: View { @Environment(\.modelContext) var modelContext let items: [Item] ... } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView_Content(items: [.init(name: "Test Item")]) } } Why Can't We Have Nice Macros for #Preview? 20 / 21