Skip to content

Commit 5e62bde

Browse files
authored
Add #LiveView macro for registering add-on libraries (#1004)
* Add `#LiveView` macro for registering add-on libraries * Suggest using placeholder types
1 parent fd3231a commit 5e62bde

File tree

4 files changed

+178
-1
lines changed

4 files changed

+178
-1
lines changed

Sources/LiveViewNative/LiveView.swift

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,26 @@ import Foundation
99
import SwiftUI
1010
import Combine
1111

12+
#if swift(>=5.9)
13+
/// Create a ``LiveView`` with a list of addons.
14+
///
15+
/// Use this macro to automatically register any addons.
16+
/// Use a placeholder type (`_`) as the root for each registry.
17+
///
18+
/// ```swift
19+
/// #LiveView(.localhost, addons: [ChartsRegistry<_>.self, AVKitRegistry<_>.self])
20+
/// ```
21+
///
22+
/// - Note: This macro erases the underlying ``LiveView`` to `AnyView`.
23+
/// This may incur a minor performance hit when updating the `View` containing the ``LiveView``.
24+
@freestanding(expression)
25+
public macro LiveView<Host: LiveViewHost>(
26+
_ host: Host,
27+
configuration: LiveSessionConfiguration = .init(),
28+
addons: [any CustomRegistry<EmptyRegistry>.Type]
29+
) -> AnyView = #externalMacro(module: "LiveViewNativeMacros", type: "LiveViewMacro")
30+
#endif
31+
1232
/// The SwiftUI root view for a Phoenix LiveView.
1333
///
1434
/// The `LiveView` attempts to connect immediately when it appears.
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
//
2+
// LiveViewMacro.swift
3+
//
4+
//
5+
// Created by Carson Katri on 7/6/23.
6+
//
7+
8+
import SwiftCompilerPlugin
9+
import SwiftSyntax
10+
import SwiftSyntaxBuilder
11+
import SwiftSyntaxMacros
12+
13+
public enum LiveViewMacro {}
14+
15+
extension LiveViewMacro: ExpressionMacro {
16+
public static func expansion<Node, Context>(
17+
of node: Node,
18+
in context: Context
19+
) throws -> ExprSyntax where Node : FreestandingMacroExpansionSyntax, Context : MacroExpansionContext {
20+
let registryName = context.makeUniqueName("Registry")
21+
22+
guard let addons = try node.argumentList.last?
23+
.expression.as(ArrayExprSyntax.self)?
24+
.elements.map(transformAddon(_:))
25+
else { throw LiveViewMacroError.invalidAddonsSyntax }
26+
27+
let registries: DeclSyntax
28+
switch addons.count {
29+
case 0:
30+
throw LiveViewMacroError.missingAddons
31+
case 1:
32+
registries = "typealias Registries = \(addons.first!)"
33+
default:
34+
func multiRegistry(_ addons: some RandomAccessCollection<SimpleTypeIdentifierSyntax>) -> SimpleTypeIdentifierSyntax {
35+
switch addons.count {
36+
case 2:
37+
return SimpleTypeIdentifierSyntax(
38+
name: "_MultiRegistry",
39+
genericArgumentClause: .init(arguments: .init([
40+
.init(argumentType: addons.first!, trailingComma: .commaToken()),
41+
.init(argumentType: addons.last!)
42+
]))
43+
)
44+
default:
45+
return SimpleTypeIdentifierSyntax(
46+
name: "_MultiRegistry",
47+
genericArgumentClause: .init(arguments: .init([
48+
.init(argumentType: addons.first!, trailingComma: .commaToken()),
49+
.init(argumentType: multiRegistry(addons.dropFirst()))
50+
]))
51+
)
52+
}
53+
}
54+
registries = "typealias Registries = \(multiRegistry(addons))"
55+
}
56+
57+
let liveViewArguments = node.argumentList
58+
.removingLast()
59+
.replacing(childAt: node.argumentList.count - 2, with: node.argumentList.removingLast().last!.with(\.trailingComma, nil))
60+
61+
return """
62+
{ () -> AnyView in
63+
enum \(registryName): AggregateRegistry {
64+
\(registries)
65+
}
66+
67+
return AnyView(LiveView<\(registryName)>(\(liveViewArguments)))
68+
}()
69+
"""
70+
}
71+
72+
private static func transformAddon(_ element: ArrayElementSyntax) throws -> SimpleTypeIdentifierSyntax {
73+
guard let registry = element.expression.as(MemberAccessExprSyntax.self)?.base?.as(SpecializeExprSyntax.self),
74+
let name = registry.expression.as(IdentifierExprSyntax.self)
75+
else { throw LiveViewMacroError.invalidAddonElement }
76+
return SimpleTypeIdentifierSyntax(
77+
name: name.identifier,
78+
genericArgumentClause: .init(.init(arguments: .init([.init(argumentType: SimpleTypeIdentifierSyntax(name: .identifier("Self")))])))
79+
)
80+
}
81+
}
82+
83+
enum LiveViewMacroError: Error, CustomStringConvertible {
84+
case invalidAddonsSyntax
85+
case invalidAddonElement
86+
case missingAddons
87+
88+
var description: String {
89+
switch self {
90+
case .invalidAddonsSyntax:
91+
return "Invalid value specified for 'addons'. Expected a static array literal."
92+
case .invalidAddonElement:
93+
return "Invalid addon provided. Expected a specialized registry type, such as 'AddonRegistry<Self>.self'"
94+
case .missingAddons:
95+
return "'addons' must not be empty."
96+
}
97+
}
98+
}

Sources/LiveViewNativeMacros/LiveViewNativeMacros.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import SwiftSyntaxMacros
1111
@main
1212
struct LiveViewNativeMacrosPlugin: CompilerPlugin {
1313
let providingMacros: [Macro.Type] = [
14-
RegistriesMacro.self
14+
RegistriesMacro.self,
15+
LiveViewMacro.self
1516
]
1617
}

Tests/LiveViewNativeMacrosTests/LiveViewNativeMacrosTests.swift

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import LiveViewNativeMacros
1212

1313
let testMacros: [String: Macro.Type] = [
1414
"Registries": RegistriesMacro.self,
15+
"LiveView": LiveViewMacro.self
1516
]
1617

1718
final class LiveViewNativeMacrosTests: XCTestCase {
@@ -37,4 +38,61 @@ final class LiveViewNativeMacrosTests: XCTestCase {
3738
macros: testMacros
3839
)
3940
}
41+
42+
func testLiveViewMacro() {
43+
assertMacroExpansion(
44+
"""
45+
#LiveView(.localhost, configuration: .init(), addons: [AVKitRegistry<EmptyRegistry>.self])
46+
""",
47+
expandedSource: """
48+
{ () -> AnyView in
49+
enum __macro_local_8RegistryfMu_: AggregateRegistry {
50+
typealias Registries = AVKitRegistry<Self>
51+
}
52+
53+
return AnyView(LiveView<__macro_local_8RegistryfMu_>(.localhost, configuration: .init()))
54+
}()
55+
""",
56+
macros: testMacros
57+
)
58+
assertMacroExpansion(
59+
"""
60+
#LiveView(
61+
.automatic(development: .localhost(port: 5000), production: .custom(URL(string: "example.com")!)),
62+
addons: [
63+
AVKitRegistry<EmptyRegistry>.self,
64+
ChartsRegistry<EmptyRegistry>.self
65+
]
66+
)
67+
""",
68+
expandedSource: """
69+
{ () -> AnyView in
70+
enum __macro_local_8RegistryfMu_: AggregateRegistry {
71+
typealias Registries = _MultiRegistry<
72+
AVKitRegistry<Self>,
73+
ChartsRegistry<Self>>
74+
}
75+
76+
return AnyView(LiveView<__macro_local_8RegistryfMu_>(
77+
.automatic(development: .localhost(port: 5000), production: .custom(URL(string: "example.com")!))))
78+
}()
79+
""",
80+
macros: testMacros
81+
)
82+
assertMacroExpansion(
83+
"""
84+
#LiveView(.localhost, configuration: .init(), addons: [AVKitRegistry<_>.self, ChartsRegistry<Self>.self, PhotoKitRegistry<_>.self])
85+
""",
86+
expandedSource: """
87+
{ () -> AnyView in
88+
enum __macro_local_8RegistryfMu_: AggregateRegistry {
89+
typealias Registries = _MultiRegistry<AVKitRegistry<Self>, _MultiRegistry<ChartsRegistry<Self>, PhotoKitRegistry<Self>>>
90+
}
91+
92+
return AnyView(LiveView<__macro_local_8RegistryfMu_>(.localhost, configuration: .init()))
93+
}()
94+
""",
95+
macros: testMacros
96+
)
97+
}
4098
}

0 commit comments

Comments
 (0)