From 1d7f8a3c52d7fe757964e63d364ecbd13f8296ae Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 11 Apr 2026 18:39:54 +0000 Subject: [PATCH 1/5] Make @ViewBuilder trailing parameter positioning opt-in via .viewBuilderTrailing Previously, @Slots always reordered generated init parameters by tier (values, closures, then @ViewBuilder), which doesn't always match the author's intent. Now declaration order is preserved by default. Use @Slots(.viewBuilderTrailing) to opt in to the old behavior when trailing closure syntax is desired. https://claude.ai/code/session_01FLoLtxvKf5acuAYipuYwka --- Sources/SlotExamples/ActionButton.swift | 2 +- Sources/SlotMacros/SlotMacro.swift | 48 ++++++++++++-- Sources/Slots/Slots.swift | 2 +- Sources/Slots/SlotsOption.swift | 13 ++++ Tests/SlotTests/SlotTests.swift | 86 +++++++++++++++++++++++-- 5 files changed, 137 insertions(+), 14 deletions(-) create mode 100644 Sources/Slots/SlotsOption.swift diff --git a/Sources/SlotExamples/ActionButton.swift b/Sources/SlotExamples/ActionButton.swift index a55817b..866232b 100644 --- a/Sources/SlotExamples/ActionButton.swift +++ b/Sources/SlotExamples/ActionButton.swift @@ -1,7 +1,7 @@ import Slots import SwiftUI -@Slots public struct ActionButton: View { +@Slots(.viewBuilderTrailing) public struct ActionButton: View { var action: () -> Void @Slot(.text, .unlabeled) var label: Label diff --git a/Sources/SlotMacros/SlotMacro.swift b/Sources/SlotMacros/SlotMacro.swift index aa15e4f..1bdc64f 100644 --- a/Sources/SlotMacros/SlotMacro.swift +++ b/Sources/SlotMacros/SlotMacro.swift @@ -15,6 +15,7 @@ public struct SlotMacro: MemberMacro, ExtensionMacro { in context: some MacroExpansionContext ) throws -> [DeclSyntax] { let access = accessModifier(of: declaration) + let slotsOptions = parseSlotsOptions(from: node) let plain = collectPlainProperties(from: declaration) let slots = collectSlots(from: declaration, node: node, context: context) guard !slots.isEmpty else { return [] } @@ -33,7 +34,7 @@ public struct SlotMacro: MemberMacro, ExtensionMacro { declarationIndex: slot.declarationIndex ) } - entries.sort { ($0.tier, $0.declarationIndex) < ($1.tier, $1.declarationIndex) } + sortEntries(&entries, viewBuilderTrailing: slotsOptions.viewBuilderTrailing) let params = entries.map(\.param).joined(separator: ", ") let assignments = entries.map(\.assignment).joined(separator: "\n ") let accessPrefix = access.map { "\($0) " } ?? "" @@ -57,6 +58,7 @@ public struct SlotMacro: MemberMacro, ExtensionMacro { in context: some MacroExpansionContext ) throws -> [ExtensionDeclSyntax] { let access = accessModifier(of: declaration) + let slotsOptions = parseSlotsOptions(from: node) let plain = collectPlainProperties(from: declaration) let slots = collectSlots(from: declaration, node: node, context: context) guard !slots.isEmpty else { return [] } @@ -70,7 +72,9 @@ public struct SlotMacro: MemberMacro, ExtensionMacro { return [] } - let groups = extensionGroups(for: slots, plain: plain, access: access) + let groups = extensionGroups( + for: slots, plain: plain, access: access, + viewBuilderTrailing: slotsOptions.viewBuilderTrailing) return groups.compactMap { (whereClause, specs) in buildExtension(type: type, whereClause: whereClause, specs: specs) } @@ -214,10 +218,30 @@ private func isFunctionType(_ type: TypeSyntax) -> Bool { return false } +// MARK: - Parsing @Slots options + +private struct SlotsOptions { + var viewBuilderTrailing = false +} + +private func parseSlotsOptions(from attr: AttributeSyntax) -> SlotsOptions { + var result = SlotsOptions() + guard case .argumentList(let args) = attr.arguments else { return result } + for arg in args { + if arg.expression.as(MemberAccessExprSyntax.self)?.declName.baseName.text + == "viewBuilderTrailing" + { + result.viewBuilderTrailing = true + } + } + return result +} + // MARK: - Parameter ordering -/// Parameters are sorted by tier to match SwiftUI conventions: -/// value params first, then closures, then @ViewBuilder closures last. +/// When `viewBuilderTrailing` is enabled, parameters are sorted by tier to match +/// SwiftUI conventions: value params first, then closures, then @ViewBuilder closures last. +/// Otherwise, declaration order is preserved. private enum ParamTier: Comparable { case value case closure @@ -231,6 +255,14 @@ private struct ParamEntry { let declarationIndex: Int } +private func sortEntries(_ entries: inout [ParamEntry], viewBuilderTrailing: Bool) { + if viewBuilderTrailing { + entries.sort { ($0.tier, $0.declarationIndex) < ($1.tier, $1.declarationIndex) } + } else { + entries.sort { $0.declarationIndex < $1.declarationIndex } + } +} + private func paramEntry(for p: PlainProperty) -> ParamEntry { if p.isGenericView { return ParamEntry( @@ -483,11 +515,13 @@ private func allCombinations(for slots: [SlotDescriptor]) -> [[SlotMode]] { /// Groups all non-trivial combinations by their where-clause key so that `.text` and `.string` /// variants for the same set of fixed generics land in the same extension. -/// Parameters are sorted by tier: value params, then closures, then @ViewBuilder closures. +/// When `viewBuilderTrailing` is true, parameters are sorted by tier: value params, then +/// closures, then @ViewBuilder closures. Otherwise, declaration order is preserved. private func extensionGroups( for slots: [SlotDescriptor], plain: [PlainProperty] = [], - access: String? = nil + access: String? = nil, + viewBuilderTrailing: Bool = false ) -> [(whereClause: String, specs: [InitSpec])] { // Use an ordered structure to preserve natural combo order var order: [String] = [] @@ -606,7 +640,7 @@ private func extensionGroups( } } - entries.sort { ($0.tier, $0.declarationIndex) < ($1.tier, $1.declarationIndex) } + sortEntries(&entries, viewBuilderTrailing: viewBuilderTrailing) let key = constraints.joined(separator: ", ") let spec = InitSpec( diff --git a/Sources/Slots/Slots.swift b/Sources/Slots/Slots.swift index 45035f3..39528a1 100644 --- a/Sources/Slots/Slots.swift +++ b/Sources/Slots/Slots.swift @@ -17,4 +17,4 @@ import SwiftUI /// ``` @attached(member, names: named(init)) @attached(extension, names: named(init)) -public macro Slots() = #externalMacro(module: "SlotMacros", type: "SlotMacro") +public macro Slots(_ options: SlotsOption...) = #externalMacro(module: "SlotMacros", type: "SlotMacro") diff --git a/Sources/Slots/SlotsOption.swift b/Sources/Slots/SlotsOption.swift new file mode 100644 index 0000000..44f8c9d --- /dev/null +++ b/Sources/Slots/SlotsOption.swift @@ -0,0 +1,13 @@ +import SwiftUI + +public struct SlotsOption: Sendable, Equatable { + private let id: Int + + private init(id: Int) { + self.id = id + } + + /// Reorder generated init parameters so `@ViewBuilder` closures appear last, + /// enabling trailing closure syntax. + public static let viewBuilderTrailing = SlotsOption(id: 0) +} diff --git a/Tests/SlotTests/SlotTests.swift b/Tests/SlotTests/SlotTests.swift index 173f627..4871e84 100644 --- a/Tests/SlotTests/SlotTests.swift +++ b/Tests/SlotTests/SlotTests.swift @@ -1172,15 +1172,91 @@ final class SlotTests: XCTestCase { } extension ActionButton where Label == Text { - init(label: LocalizedStringResource, action: @escaping () -> Void) { + init(action: @escaping () -> Void, label: LocalizedStringResource) { + self.action = action self.label = Text(label) + } + + @_disfavoredOverload + init(action: @escaping () -> Void, label: String) { self.action = action + self.label = Text(label) + } + } + """, + macros: testMacros + ) + } + + func testParameterDeclarationOrdering() { + // Verifies: without .viewBuilderTrailing, parameters preserve declaration order + assertMacroExpansion( + """ + @Slots + struct Composed: View { + var style: Int + var onTap: () -> Void + @Slot(.text) var label: Label + var trailing: Trailing? + var body: some View { EmptyView() } + } + """, + expandedSource: """ + struct Composed: View { + var style: Int + var onTap: () -> Void + var label: Label + var trailing: Trailing? + var body: some View { EmptyView() } + + init(style: Int, onTap: @escaping () -> Void, @ViewBuilder label: () -> Label, @ViewBuilder trailing: () -> Trailing) { + self.style = style + self.onTap = onTap + self.label = label() + self.trailing = Optional(trailing()) + } + } + + extension Composed where Trailing == Never { + init(style: Int, onTap: @escaping () -> Void, @ViewBuilder label: () -> Label) { + self.style = style + self.onTap = onTap + self.label = label() + self.trailing = nil + } + } + + extension Composed where Label == Text { + init(style: Int, onTap: @escaping () -> Void, label: LocalizedStringResource, @ViewBuilder trailing: () -> Trailing) { + self.style = style + self.onTap = onTap + self.label = Text(label) + self.trailing = Optional(trailing()) + } + + @_disfavoredOverload + init(style: Int, onTap: @escaping () -> Void, label: String, @ViewBuilder trailing: () -> Trailing) { + self.style = style + self.onTap = onTap + self.label = Text(label) + self.trailing = Optional(trailing()) + } + } + + extension Composed where Label == Text, Trailing == Never { + init(style: Int, onTap: @escaping () -> Void, label: LocalizedStringResource) { + self.style = style + self.onTap = onTap + self.label = Text(label) + self.trailing = nil } @_disfavoredOverload - init(label: String, action: @escaping () -> Void) { + init(style: Int, onTap: @escaping () -> Void, label: String) { + self.style = style + self.onTap = onTap self.label = Text(label) - self.action = action + self.trailing = nil } } """, @@ -1189,10 +1265,10 @@ final class SlotTests: XCTestCase { } func testParameterTierOrdering() { - // Verifies: value params → closure params → @ViewBuilder params + // Verifies: with .viewBuilderTrailing, value params → closure params → @ViewBuilder params assertMacroExpansion( """ - @Slots + @Slots(.viewBuilderTrailing) struct Composed: View { var style: Int var onTap: () -> Void From 9f92b2fcc54cd94ec8ee36108a4d933f69d37793 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 11 Apr 2026 18:42:33 +0000 Subject: [PATCH 2/5] Rename .viewBuilderTrailing to .trailingViewBuilders https://claude.ai/code/session_01FLoLtxvKf5acuAYipuYwka --- Sources/SlotExamples/ActionButton.swift | 2 +- Sources/SlotMacros/SlotMacro.swift | 22 +++++++++++----------- Sources/Slots/SlotsOption.swift | 2 +- Tests/SlotTests/SlotTests.swift | 6 +++--- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/Sources/SlotExamples/ActionButton.swift b/Sources/SlotExamples/ActionButton.swift index 866232b..c249d03 100644 --- a/Sources/SlotExamples/ActionButton.swift +++ b/Sources/SlotExamples/ActionButton.swift @@ -1,7 +1,7 @@ import Slots import SwiftUI -@Slots(.viewBuilderTrailing) public struct ActionButton: View { +@Slots(.trailingViewBuilders) public struct ActionButton: View { var action: () -> Void @Slot(.text, .unlabeled) var label: Label diff --git a/Sources/SlotMacros/SlotMacro.swift b/Sources/SlotMacros/SlotMacro.swift index 1bdc64f..6a7482c 100644 --- a/Sources/SlotMacros/SlotMacro.swift +++ b/Sources/SlotMacros/SlotMacro.swift @@ -34,7 +34,7 @@ public struct SlotMacro: MemberMacro, ExtensionMacro { declarationIndex: slot.declarationIndex ) } - sortEntries(&entries, viewBuilderTrailing: slotsOptions.viewBuilderTrailing) + sortEntries(&entries, trailingViewBuilders: slotsOptions.trailingViewBuilders) let params = entries.map(\.param).joined(separator: ", ") let assignments = entries.map(\.assignment).joined(separator: "\n ") let accessPrefix = access.map { "\($0) " } ?? "" @@ -74,7 +74,7 @@ public struct SlotMacro: MemberMacro, ExtensionMacro { let groups = extensionGroups( for: slots, plain: plain, access: access, - viewBuilderTrailing: slotsOptions.viewBuilderTrailing) + trailingViewBuilders: slotsOptions.trailingViewBuilders) return groups.compactMap { (whereClause, specs) in buildExtension(type: type, whereClause: whereClause, specs: specs) } @@ -221,7 +221,7 @@ private func isFunctionType(_ type: TypeSyntax) -> Bool { // MARK: - Parsing @Slots options private struct SlotsOptions { - var viewBuilderTrailing = false + var trailingViewBuilders = false } private func parseSlotsOptions(from attr: AttributeSyntax) -> SlotsOptions { @@ -229,9 +229,9 @@ private func parseSlotsOptions(from attr: AttributeSyntax) -> SlotsOptions { guard case .argumentList(let args) = attr.arguments else { return result } for arg in args { if arg.expression.as(MemberAccessExprSyntax.self)?.declName.baseName.text - == "viewBuilderTrailing" + == "trailingViewBuilders" { - result.viewBuilderTrailing = true + result.trailingViewBuilders = true } } return result @@ -239,7 +239,7 @@ private func parseSlotsOptions(from attr: AttributeSyntax) -> SlotsOptions { // MARK: - Parameter ordering -/// When `viewBuilderTrailing` is enabled, parameters are sorted by tier to match +/// When `trailingViewBuilders` is enabled, parameters are sorted by tier to match /// SwiftUI conventions: value params first, then closures, then @ViewBuilder closures last. /// Otherwise, declaration order is preserved. private enum ParamTier: Comparable { @@ -255,8 +255,8 @@ private struct ParamEntry { let declarationIndex: Int } -private func sortEntries(_ entries: inout [ParamEntry], viewBuilderTrailing: Bool) { - if viewBuilderTrailing { +private func sortEntries(_ entries: inout [ParamEntry], trailingViewBuilders: Bool) { + if trailingViewBuilders { entries.sort { ($0.tier, $0.declarationIndex) < ($1.tier, $1.declarationIndex) } } else { entries.sort { $0.declarationIndex < $1.declarationIndex } @@ -515,13 +515,13 @@ private func allCombinations(for slots: [SlotDescriptor]) -> [[SlotMode]] { /// Groups all non-trivial combinations by their where-clause key so that `.text` and `.string` /// variants for the same set of fixed generics land in the same extension. -/// When `viewBuilderTrailing` is true, parameters are sorted by tier: value params, then +/// When `trailingViewBuilders` is true, parameters are sorted by tier: value params, then /// closures, then @ViewBuilder closures. Otherwise, declaration order is preserved. private func extensionGroups( for slots: [SlotDescriptor], plain: [PlainProperty] = [], access: String? = nil, - viewBuilderTrailing: Bool = false + trailingViewBuilders: Bool = false ) -> [(whereClause: String, specs: [InitSpec])] { // Use an ordered structure to preserve natural combo order var order: [String] = [] @@ -640,7 +640,7 @@ private func extensionGroups( } } - sortEntries(&entries, viewBuilderTrailing: viewBuilderTrailing) + sortEntries(&entries, trailingViewBuilders: trailingViewBuilders) let key = constraints.joined(separator: ", ") let spec = InitSpec( diff --git a/Sources/Slots/SlotsOption.swift b/Sources/Slots/SlotsOption.swift index 44f8c9d..5aff1cc 100644 --- a/Sources/Slots/SlotsOption.swift +++ b/Sources/Slots/SlotsOption.swift @@ -9,5 +9,5 @@ public struct SlotsOption: Sendable, Equatable { /// Reorder generated init parameters so `@ViewBuilder` closures appear last, /// enabling trailing closure syntax. - public static let viewBuilderTrailing = SlotsOption(id: 0) + public static let trailingViewBuilders = SlotsOption(id: 0) } diff --git a/Tests/SlotTests/SlotTests.swift b/Tests/SlotTests/SlotTests.swift index 4871e84..e2fb89f 100644 --- a/Tests/SlotTests/SlotTests.swift +++ b/Tests/SlotTests/SlotTests.swift @@ -1189,7 +1189,7 @@ final class SlotTests: XCTestCase { } func testParameterDeclarationOrdering() { - // Verifies: without .viewBuilderTrailing, parameters preserve declaration order + // Verifies: without .trailingViewBuilders, parameters preserve declaration order assertMacroExpansion( """ @Slots @@ -1265,10 +1265,10 @@ final class SlotTests: XCTestCase { } func testParameterTierOrdering() { - // Verifies: with .viewBuilderTrailing, value params → closure params → @ViewBuilder params + // Verifies: with .trailingViewBuilders, value params → closure params → @ViewBuilder params assertMacroExpansion( """ - @Slots(.viewBuilderTrailing) + @Slots(.trailingViewBuilders) struct Composed: View { var style: Int var onTap: () -> Void From 14ce529fc6b32491553c21451759daa0774646fe Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 11 Apr 2026 18:51:03 +0000 Subject: [PATCH 3/5] Fix test expectations for declaration-order parameter sorting Update unit and integration tests to match the new default behavior where parameters preserve declaration order instead of being sorted by tier. https://claude.ai/code/session_01FLoLtxvKf5acuAYipuYwka --- Tests/SlotTests/SlotIntegrationTests.swift | 2 +- Tests/SlotTests/SlotTests.swift | 100 ++++++++++----------- 2 files changed, 51 insertions(+), 51 deletions(-) diff --git a/Tests/SlotTests/SlotIntegrationTests.swift b/Tests/SlotTests/SlotIntegrationTests.swift index cb494fb..b5c48d8 100644 --- a/Tests/SlotTests/SlotIntegrationTests.swift +++ b/Tests/SlotTests/SlotIntegrationTests.swift @@ -119,7 +119,7 @@ final class SlotIntegrationTests: XCTestCase { isSelected: false, leading: { Image(systemName: "star") }, content: { Text("hi") }, trailing: { Text("→") }) // LocalizedStringResource content, generic leading + trailing let _: Row = Row( - isSelected: true, content: "hi", leading: { Image(systemName: "star") }, trailing: { Text("→") }) + isSelected: true, leading: { Image(systemName: "star") }, content: "hi", trailing: { Text("→") }) // image leading, LocalizedStringResource content, generic trailing let _: Row = Row( isSelected: false, leadingSystemName: "star", content: "hi", trailing: { Text("→") }) diff --git a/Tests/SlotTests/SlotTests.swift b/Tests/SlotTests/SlotTests.swift index e2fb89f..9e47c38 100644 --- a/Tests/SlotTests/SlotTests.swift +++ b/Tests/SlotTests/SlotTests.swift @@ -417,31 +417,31 @@ final class SlotTests: XCTestCase { } extension Card where Subtitle == Text { - init(subtitle: LocalizedStringResource, @ViewBuilder title: () -> Title, @ViewBuilder actions: () -> Actions) { - self.subtitle = Text(subtitle) + init(@ViewBuilder title: () -> Title, subtitle: LocalizedStringResource, @ViewBuilder actions: () -> Actions) { self.title = title() + self.subtitle = Text(subtitle) self.actions = Optional(actions()) } @_disfavoredOverload - init(subtitle: String, @ViewBuilder title: () -> Title, @ViewBuilder actions: () -> Actions) { - self.subtitle = Text(subtitle) + init(@ViewBuilder title: () -> Title, subtitle: String, @ViewBuilder actions: () -> Actions) { self.title = title() + self.subtitle = Text(subtitle) self.actions = Optional(actions()) } } extension Card where Subtitle == Text, Actions == Never { - init(subtitle: LocalizedStringResource, @ViewBuilder title: () -> Title) { - self.subtitle = Text(subtitle) + init(@ViewBuilder title: () -> Title, subtitle: LocalizedStringResource) { self.title = title() + self.subtitle = Text(subtitle) self.actions = nil } @_disfavoredOverload - init(subtitle: String, @ViewBuilder title: () -> Title) { - self.subtitle = Text(subtitle) + init(@ViewBuilder title: () -> Title, subtitle: String) { self.title = title() + self.subtitle = Text(subtitle) self.actions = nil } } @@ -578,35 +578,35 @@ final class SlotTests: XCTestCase { } extension Card where Body == Text { - init(body_: LocalizedStringResource, @ViewBuilder title: () -> Title, @ViewBuilder subtitle: () -> Subtitle, @ViewBuilder footer: () -> Footer) { - self.body_ = Text(body_) + init(@ViewBuilder title: () -> Title, @ViewBuilder subtitle: () -> Subtitle, body_: LocalizedStringResource, @ViewBuilder footer: () -> Footer) { self.title = title() self.subtitle = Optional(subtitle()) + self.body_ = Text(body_) self.footer = Optional(footer()) } @_disfavoredOverload - init(body_: String, @ViewBuilder title: () -> Title, @ViewBuilder subtitle: () -> Subtitle, @ViewBuilder footer: () -> Footer) { - self.body_ = Text(body_) + init(@ViewBuilder title: () -> Title, @ViewBuilder subtitle: () -> Subtitle, body_: String, @ViewBuilder footer: () -> Footer) { self.title = title() self.subtitle = Optional(subtitle()) + self.body_ = Text(body_) self.footer = Optional(footer()) } } extension Card where Body == Text, Footer == Never { - init(body_: LocalizedStringResource, @ViewBuilder title: () -> Title, @ViewBuilder subtitle: () -> Subtitle) { - self.body_ = Text(body_) + init(@ViewBuilder title: () -> Title, @ViewBuilder subtitle: () -> Subtitle, body_: LocalizedStringResource) { self.title = title() self.subtitle = Optional(subtitle()) + self.body_ = Text(body_) self.footer = nil } @_disfavoredOverload - init(body_: String, @ViewBuilder title: () -> Title, @ViewBuilder subtitle: () -> Subtitle) { - self.body_ = Text(body_) + init(@ViewBuilder title: () -> Title, @ViewBuilder subtitle: () -> Subtitle, body_: String) { self.title = title() self.subtitle = Optional(subtitle()) + self.body_ = Text(body_) self.footer = nil } } @@ -630,34 +630,34 @@ final class SlotTests: XCTestCase { } extension Card where Subtitle == Never, Body == Text { - init(body_: LocalizedStringResource, @ViewBuilder title: () -> Title, @ViewBuilder footer: () -> Footer) { - self.body_ = Text(body_) + init(@ViewBuilder title: () -> Title, body_: LocalizedStringResource, @ViewBuilder footer: () -> Footer) { self.title = title() + self.body_ = Text(body_) self.footer = Optional(footer()) self.subtitle = nil } @_disfavoredOverload - init(body_: String, @ViewBuilder title: () -> Title, @ViewBuilder footer: () -> Footer) { - self.body_ = Text(body_) + init(@ViewBuilder title: () -> Title, body_: String, @ViewBuilder footer: () -> Footer) { self.title = title() + self.body_ = Text(body_) self.footer = Optional(footer()) self.subtitle = nil } } extension Card where Subtitle == Never, Body == Text, Footer == Never { - init(body_: LocalizedStringResource, @ViewBuilder title: () -> Title) { - self.body_ = Text(body_) + init(@ViewBuilder title: () -> Title, body_: LocalizedStringResource) { self.title = title() + self.body_ = Text(body_) self.subtitle = nil self.footer = nil } @_disfavoredOverload - init(body_: String, @ViewBuilder title: () -> Title) { - self.body_ = Text(body_) + init(@ViewBuilder title: () -> Title, body_: String) { self.title = title() + self.body_ = Text(body_) self.subtitle = nil self.footer = nil } @@ -698,67 +698,67 @@ final class SlotTests: XCTestCase { } extension Card where Title == Text, Body == Text { - init(title: LocalizedStringResource, body_: LocalizedStringResource, @ViewBuilder subtitle: () -> Subtitle, @ViewBuilder footer: () -> Footer) { + init(title: LocalizedStringResource, @ViewBuilder subtitle: () -> Subtitle, body_: LocalizedStringResource, @ViewBuilder footer: () -> Footer) { self.title = Text(title) - self.body_ = Text(body_) self.subtitle = Optional(subtitle()) + self.body_ = Text(body_) self.footer = Optional(footer()) } @_disfavoredOverload - init(title: LocalizedStringResource, body_: String, @ViewBuilder subtitle: () -> Subtitle, @ViewBuilder footer: () -> Footer) { + init(title: LocalizedStringResource, @ViewBuilder subtitle: () -> Subtitle, body_: String, @ViewBuilder footer: () -> Footer) { self.title = Text(title) - self.body_ = Text(body_) self.subtitle = Optional(subtitle()) + self.body_ = Text(body_) self.footer = Optional(footer()) } @_disfavoredOverload - init(title: String, body_: LocalizedStringResource, @ViewBuilder subtitle: () -> Subtitle, @ViewBuilder footer: () -> Footer) { + init(title: String, @ViewBuilder subtitle: () -> Subtitle, body_: LocalizedStringResource, @ViewBuilder footer: () -> Footer) { self.title = Text(title) - self.body_ = Text(body_) self.subtitle = Optional(subtitle()) + self.body_ = Text(body_) self.footer = Optional(footer()) } @_disfavoredOverload - init(title: String, body_: String, @ViewBuilder subtitle: () -> Subtitle, @ViewBuilder footer: () -> Footer) { + init(title: String, @ViewBuilder subtitle: () -> Subtitle, body_: String, @ViewBuilder footer: () -> Footer) { self.title = Text(title) - self.body_ = Text(body_) self.subtitle = Optional(subtitle()) + self.body_ = Text(body_) self.footer = Optional(footer()) } } extension Card where Title == Text, Body == Text, Footer == Never { - init(title: LocalizedStringResource, body_: LocalizedStringResource, @ViewBuilder subtitle: () -> Subtitle) { + init(title: LocalizedStringResource, @ViewBuilder subtitle: () -> Subtitle, body_: LocalizedStringResource) { self.title = Text(title) - self.body_ = Text(body_) self.subtitle = Optional(subtitle()) + self.body_ = Text(body_) self.footer = nil } @_disfavoredOverload - init(title: LocalizedStringResource, body_: String, @ViewBuilder subtitle: () -> Subtitle) { + init(title: LocalizedStringResource, @ViewBuilder subtitle: () -> Subtitle, body_: String) { self.title = Text(title) - self.body_ = Text(body_) self.subtitle = Optional(subtitle()) + self.body_ = Text(body_) self.footer = nil } @_disfavoredOverload - init(title: String, body_: LocalizedStringResource, @ViewBuilder subtitle: () -> Subtitle) { + init(title: String, @ViewBuilder subtitle: () -> Subtitle, body_: LocalizedStringResource) { self.title = Text(title) - self.body_ = Text(body_) self.subtitle = Optional(subtitle()) + self.body_ = Text(body_) self.footer = nil } @_disfavoredOverload - init(title: String, body_: String, @ViewBuilder subtitle: () -> Subtitle) { + init(title: String, @ViewBuilder subtitle: () -> Subtitle, body_: String) { self.title = Text(title) - self.body_ = Text(body_) self.subtitle = Optional(subtitle()) + self.body_ = Text(body_) self.footer = nil } } @@ -941,19 +941,19 @@ final class SlotTests: XCTestCase { } extension Banner where Label == Text { - init(isEnabled: Bool, badge: Int = 0, label: LocalizedStringResource, @ViewBuilder icon: () -> Icon) { + init(isEnabled: Bool, badge: Int = 0, @ViewBuilder icon: () -> Icon, label: LocalizedStringResource) { self.isEnabled = isEnabled self.badge = badge - self.label = Text(label) self.icon = Optional(icon()) + self.label = Text(label) } @_disfavoredOverload - init(isEnabled: Bool, badge: Int = 0, label: String, @ViewBuilder icon: () -> Icon) { + init(isEnabled: Bool, badge: Int = 0, @ViewBuilder icon: () -> Icon, label: String) { self.isEnabled = isEnabled self.badge = badge - self.label = Text(label) self.icon = Optional(icon()) + self.label = Text(label) } } @@ -1085,15 +1085,15 @@ final class SlotTests: XCTestCase { } extension Chip where Label == Text { - init(label: LocalizedStringResource, @ViewBuilder icon: () -> Icon) { - self.label = Text(label) + init(@ViewBuilder icon: () -> Icon, label: LocalizedStringResource) { self.icon = Optional(icon()) + self.label = Text(label) } @_disfavoredOverload - init(label: String, @ViewBuilder icon: () -> Icon) { - self.label = Text(label) + init(@ViewBuilder icon: () -> Icon, label: String) { self.icon = Optional(icon()) + self.label = Text(label) } } @@ -1508,9 +1508,9 @@ final class SlotTests: XCTestCase { } extension EventCard where When == DateResolver.Output { - init(when_: DateResolver.Input, @ViewBuilder title: () -> Title) { - self.when_ = DateResolver.resolve(when_) + init(@ViewBuilder title: () -> Title, when_: DateResolver.Input) { self.title = title() + self.when_ = DateResolver.resolve(when_) } } From dc97be7038f581072b2ff712c72a6854ef9f63e1 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 11 Apr 2026 18:57:05 +0000 Subject: [PATCH 4/5] Fix ToolbarRow preview call site for declaration-order params The ToolbarRow preview passed title before leading, but with declaration order preserved, leading (idx 0) now comes before title (idx 1). https://claude.ai/code/session_01FLoLtxvKf5acuAYipuYwka --- Sources/SlotExamples/Previews.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/SlotExamples/Previews.swift b/Sources/SlotExamples/Previews.swift index 9aaef07..78bbec9 100644 --- a/Sources/SlotExamples/Previews.swift +++ b/Sources/SlotExamples/Previews.swift @@ -47,10 +47,10 @@ struct Examples_Previews: PreviewProvider { // ToolbarRow examples ToolbarRow(title: "Inbox") ToolbarRow( - title: "Details", leading: { Button(action: {}) { Image(systemName: "chevron.left") } }, + title: "Details", trailing: { Button(action: {}) { Image(systemName: "ellipsis") } }) From d159f35a0e3599e5cd12fce6518d364d5b5ad520 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 11 Apr 2026 19:01:58 +0000 Subject: [PATCH 5/5] Rename testClosurePropertyOrdering to testClosurePropertyDeclarationOrder The test now verifies declaration order is preserved, not that closures get reordered. https://claude.ai/code/session_01FLoLtxvKf5acuAYipuYwka --- Tests/SlotTests/SlotTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/SlotTests/SlotTests.swift b/Tests/SlotTests/SlotTests.swift index 9e47c38..b93ac9e 100644 --- a/Tests/SlotTests/SlotTests.swift +++ b/Tests/SlotTests/SlotTests.swift @@ -1149,7 +1149,7 @@ final class SlotTests: XCTestCase { // MARK: - Parameter ordering tests - func testClosurePropertyOrdering() { + func testClosurePropertyDeclarationOrder() { assertMacroExpansion( """ @Slots