Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Sources/SlotExamples/ActionButton.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Slots
import SwiftUI

@Slots public struct ActionButton<Label: View>: View {
@Slots(.trailingViewBuilders) public struct ActionButton<Label: View>: View {
var action: () -> Void
@Slot(.text, .unlabeled) var label: Label

Expand Down
2 changes: 1 addition & 1 deletion Sources/SlotExamples/Previews.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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") }
})
Expand Down
48 changes: 41 additions & 7 deletions Sources/SlotMacros/SlotMacro.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 [] }
Expand All @@ -33,7 +34,7 @@ public struct SlotMacro: MemberMacro, ExtensionMacro {
declarationIndex: slot.declarationIndex
)
}
entries.sort { ($0.tier, $0.declarationIndex) < ($1.tier, $1.declarationIndex) }
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) " } ?? ""
Expand All @@ -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 [] }
Expand All @@ -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,
trailingViewBuilders: slotsOptions.trailingViewBuilders)
return groups.compactMap { (whereClause, specs) in
buildExtension(type: type, whereClause: whereClause, specs: specs)
}
Expand Down Expand Up @@ -214,10 +218,30 @@ private func isFunctionType(_ type: TypeSyntax) -> Bool {
return false
}

// MARK: - Parsing @Slots options

private struct SlotsOptions {
var trailingViewBuilders = 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
== "trailingViewBuilders"
{
result.trailingViewBuilders = 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 `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 {
case value
case closure
Expand All @@ -231,6 +255,14 @@ private struct ParamEntry {
let declarationIndex: Int
}

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 }
}
}

private func paramEntry(for p: PlainProperty) -> ParamEntry {
if p.isGenericView {
return ParamEntry(
Expand Down Expand Up @@ -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 `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
access: String? = nil,
trailingViewBuilders: Bool = false
) -> [(whereClause: String, specs: [InitSpec])] {
// Use an ordered structure to preserve natural combo order
var order: [String] = []
Expand Down Expand Up @@ -606,7 +640,7 @@ private func extensionGroups(
}
}

entries.sort { ($0.tier, $0.declarationIndex) < ($1.tier, $1.declarationIndex) }
sortEntries(&entries, trailingViewBuilders: trailingViewBuilders)

let key = constraints.joined(separator: ", ")
let spec = InitSpec(
Expand Down
2 changes: 1 addition & 1 deletion Sources/Slots/Slots.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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")
13 changes: 13 additions & 0 deletions Sources/Slots/SlotsOption.swift
Original file line number Diff line number Diff line change
@@ -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 trailingViewBuilders = SlotsOption(id: 0)
}
2 changes: 1 addition & 1 deletion Tests/SlotTests/SlotIntegrationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<Image, Text, Text> = 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<Image, Text, Text> = Row(
isSelected: false, leadingSystemName: "star", content: "hi", trailing: { Text("→") })
Expand Down
Loading
Loading