Skip to content

Commit 828235a

Browse files
authored
Merge pull request #1300 from liveview-native/combined-attribute-resolution
`@LiveElement` macro API
2 parents 55a8459 + c37dd87 commit 828235a

File tree

95 files changed

+2738
-2369
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

95 files changed

+2738
-2369
lines changed

Sources/BuiltinRegistryGenerator/BuiltinRegistryGenerator.swift

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,9 @@ struct BuiltinRegistryGenerator: ParsableCommand {
2727
]
2828

2929
static let additionalViews = [
30-
"Capsule": "Shape(shape: Capsule(from: element))",
31-
"Circle": "Shape(shape: Circle())",
32-
"ContainerRelativeShape": "Shape(shape: ContainerRelativeShape())",
30+
"Capsule": "Shape<R, Capsule>(shape: Capsule(from: element))",
31+
"Circle": "Shape<R, Circle>(shape: Circle())",
32+
"ContainerRelativeShape": "Shape<R, ContainerRelativeShape>(shape: ContainerRelativeShape())",
3333
"Divider": "Divider()",
3434
"EditButton": """
3535
#if os(iOS)
@@ -38,10 +38,10 @@ struct BuiltinRegistryGenerator: ParsableCommand {
3838
""",
3939
"Ellipse": "Ellipse()",
4040
"NamespaceContext": "NamespaceContext<R>()",
41-
"Rectangle": "Shape(shape: Rectangle())",
41+
"Rectangle": "Shape<R, Rectangle>(shape: Rectangle())",
4242
"RenameButton": "RenameButton()",
43-
"RoundedRectangle": "Shape(shape: RoundedRectangle(from: element))",
44-
"Color": "ColorView()",
43+
"RoundedRectangle": "Shape<R, RoundedRectangle>(shape: RoundedRectangle(from: element))",
44+
"Color": "ColorView<R>()",
4545
"Image": "ImageView<R>()",
4646
"phx-main": "PhxMain<R>()"
4747
]

Sources/LiveViewNative/BuiltinRegistry+applyBinding.swift

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,47 +8,56 @@
88
import SwiftUI
99
import LiveViewNativeCore
1010

11+
public enum _EventBinding: String {
12+
case windowFocus = "phx-window-focus"
13+
case windowBlur = "phx-window-blur"
14+
case focus = "phx-focus"
15+
case blur = "phx-blur"
16+
case click = "phx-click"
17+
}
18+
1119
extension BuiltinRegistry {
1220
@ViewBuilder
1321
static func applyBinding(
14-
_ binding: AttributeName,
22+
_ binding: _EventBinding,
1523
event: String,
1624
value: Payload,
1725
to view: some View,
1826
element: ElementNode
1927
) -> some View {
2028
ProvidedBindingsReader(
21-
binding: binding.rawValue,
29+
binding: binding,
2230
content: view
2331
) {
2432
switch binding {
25-
case "phx-window-focus":
33+
case .windowFocus:
2634
ScenePhaseObserver(content: view, target: .active, type: "focus", event: event, value: value)
27-
case "phx-window-blur":
35+
case .windowBlur:
2836
ScenePhaseObserver(content: view, target: .background, type: "blur", event: event, value: value)
29-
case "phx-focus":
37+
case .focus:
3038
FocusObserver(content: view, target: true, type: "focus", event: event, value: value)
31-
case "phx-blur":
39+
case .blur:
3240
FocusObserver(content: view, target: false, type: "blur", event: event, value: value)
33-
case "phx-click":
41+
case .click:
3442
TapGestureView(content: view, type: "click", event: event, value: value)
35-
default:
36-
view
3743
}
3844
}
3945
}
4046
}
4147

4248
/// A preference key that specifies what bindings the View handles internally.
4349
public enum _ProvidedBindingsKey: PreferenceKey {
44-
public static var defaultValue: Set<String> { [] }
45-
public static func reduce(value: inout Set<String>, nextValue: () -> Set<String>) {
50+
public static var defaultValue: Set<_EventBinding> { [] }
51+
public static func reduce(
52+
value: inout Set<_EventBinding>,
53+
nextValue: () -> Set<_EventBinding>
54+
) {
4655
value = nextValue()
4756
}
4857
}
4958

5059
fileprivate struct ProvidedBindingsReader<Content: View, ApplyBinding: View>: View {
51-
let binding: String
60+
let binding: _EventBinding
5261
let content: Content
5362
@ViewBuilder let applyBinding: () -> ApplyBinding
5463
@State private var providesBinding: Bool = false

Sources/LiveViewNative/Environment.swift

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -50,15 +50,32 @@ private struct LiveContextStorageKey: EnvironmentKey {
5050
/// Provides access to ``LiveViewCoordinator`` properties via the environment.
5151
/// This exists to type-erase the coordinator, since environment properties can't be generic.
5252
struct CoordinatorEnvironment {
53-
let pushEvent: @MainActor (String, String, Any, Int?) async throws -> Void
54-
let elementChanged: (NodeRef) -> ObservableObjectPublisher
55-
let document: Document
53+
private final class Storage {
54+
let pushEvent: @MainActor (String, String, Any, Int?) async throws -> Void
55+
let elementChanged: @MainActor (NodeRef) -> ObservableObjectPublisher
56+
let document: Document
57+
58+
init<R: CustomRegistry>(_ coordinator: LiveViewCoordinator<R>, document: Document) {
59+
self.pushEvent = coordinator.pushEvent
60+
self.document = document
61+
self.elementChanged = coordinator.elementChanged(_:)
62+
}
63+
}
64+
private let storage: Storage
65+
66+
var pushEvent: @MainActor (String, String, Any, Int?) async throws -> Void {
67+
storage.pushEvent
68+
}
69+
var elementChanged: @MainActor (NodeRef) -> ObservableObjectPublisher {
70+
storage.elementChanged
71+
}
72+
var document: Document {
73+
storage.document
74+
}
5675

5776
@MainActor
5877
init<R: CustomRegistry>(_ coordinator: LiveViewCoordinator<R>, document: Document) {
59-
self.pushEvent = coordinator.pushEvent
60-
self.document = document
61-
self.elementChanged = coordinator.elementChanged(_:)
78+
self.storage = .init(coordinator, document: document)
6279
}
6380

6481
fileprivate struct Key: EnvironmentKey {
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
//
2+
// LiveElement.swift
3+
//
4+
//
5+
// Created by Carson Katri on 4/2/24.
6+
//
7+
8+
import SwiftUI
9+
import LiveViewNativeCore
10+
11+
@attached(member, names: named(liveElement), named(_TrackedContent))
12+
@attached(memberAttribute)
13+
public macro LiveElement() = #externalMacro(module: "LiveViewNativeMacros", type: "LiveElementMacro")
14+
15+
@attached(accessor, names: named(get))
16+
public macro LiveAttribute(_ name: AttributeName? = nil) = #externalMacro(module: "LiveViewNativeMacros", type: "LiveAttributeMacro")
17+
18+
@attached(accessor, names: named(willSet))
19+
public macro LiveElementIgnored() = #externalMacro(module: "LiveViewNativeMacros", type: "LiveElementIgnoredMacro")
20+
21+
public protocol _LiveElementTrackedContent {
22+
init()
23+
init(from element: ElementNode) throws
24+
}
25+
26+
@propertyWrapper public struct _LiveElementTracked<R: RootRegistry, T: _LiveElementTrackedContent>: DynamicProperty {
27+
@ObservedElement public var element
28+
@LiveContext<R> public var context
29+
30+
public var wrappedValue: T
31+
32+
public var projectedValue: Self {
33+
self
34+
}
35+
36+
public init(wrappedValue: T) {
37+
self.wrappedValue = wrappedValue
38+
}
39+
40+
public init(wrappedValue: T? = nil, element: ElementNode) {
41+
self._element = .init(element: element)
42+
self.wrappedValue = wrappedValue ?? (try? T.init(from: element)) ?? T.init()
43+
}
44+
45+
public mutating func update() {
46+
self.wrappedValue = try! T.init(from: element)
47+
}
48+
49+
var isConstant: Bool {
50+
_element.isConstant
51+
}
52+
}
53+
54+
public extension _LiveElementTracked {
55+
func children(_ predicate: (Node) -> Bool = { node in
56+
!node.attributes.contains(where: { $0.name.namespace == nil && $0.name.name == "template" })
57+
}) -> some View {
58+
context.coordinator.builder.fromNodes(_element.children.filter(predicate), context: context.storage)
59+
}
60+
61+
func children(in template: Template, default includeDefault: Bool = false) -> some View {
62+
children {
63+
$0.attributes.contains(where: {
64+
$0 == template
65+
}) || (includeDefault && !$0.attributes.contains(where: { $0.name.namespace == nil && $0.name.name == "template" }))
66+
}
67+
}
68+
69+
func hasTemplate(_ template: Template, default includeDefault: Bool = false) -> Bool {
70+
_element.children.contains(where: {
71+
for attribute in $0.attributes {
72+
if attribute == template {
73+
return true
74+
} else if includeDefault && attribute.name.namespace == nil && attribute.name.name == "template" {
75+
return false
76+
}
77+
}
78+
return includeDefault
79+
})
80+
}
81+
82+
func hasTemplate(_ name: String, value: String) -> Bool {
83+
hasTemplate(.init(name, value: value))
84+
}
85+
86+
var childNodes: [LiveViewNativeCore.Node] {
87+
_element.children
88+
}
89+
}
90+
91+
public struct Template: RawRepresentable, ExpressibleByStringLiteral {
92+
let name: String
93+
let value: String?
94+
95+
public init(_ name: String, value: String? = nil) {
96+
self.name = name
97+
self.value = value
98+
}
99+
100+
public init(rawValue: String) {
101+
var components = rawValue.split(separator: "." as Character)
102+
self.name = String(components.removeFirst())
103+
self.value = components.isEmpty ? nil : components.joined(separator: ".")
104+
}
105+
106+
public init(stringLiteral value: String) {
107+
self.init(rawValue: value)
108+
}
109+
110+
public var rawValue: String {
111+
if let value {
112+
return "\(name).\(value)"
113+
} else {
114+
return name
115+
}
116+
}
117+
118+
static func == (_ lhs: LiveViewNativeCore.Attribute, _ rhs: Self) -> Bool {
119+
lhs.name.namespace == nil
120+
&& lhs.name.name == "template"
121+
&& lhs.value == rhs.rawValue
122+
}
123+
}

Sources/LiveViewNative/LiveView.swift renamed to Sources/LiveViewNative/Live/LiveView.swift

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -190,11 +190,7 @@ public struct LiveView<
190190
AnyView(buildPhaseView(phase as! LiveViewPhase<R>))
191191
}
192192
}
193-
.transformEnvironment(\.stylesheets) { stylesheets in
194-
guard let stylesheet = session.stylesheet
195-
else { return }
196-
stylesheets[ObjectIdentifier(R.self)] = stylesheet
197-
}
193+
.environment(\.stylesheet, session.stylesheet ?? .init(content: [], classes: [:]))
198194
.environmentObject(session)
199195
.environmentObject(liveViewModel)
200196
.task {

Sources/LiveViewNative/LiveViewNative.docc/Resources/04-03-02-initializer.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import SwiftUI
22
import LiveViewNative
33

44
struct CatRatingView: View {
5-
@Attribute("score") private var score: Int
5+
@Attribute(.init(name: "score") private var score: Int
66
@LiveContext<MyRegistry> private var context
77

88
var body: some View {

Sources/LiveViewNative/LiveViewNative.docc/Resources/04-03-03-properties.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import SwiftUI
22
import LiveViewNative
33

44
struct CatRatingView: View {
5-
@Attribute("score") private var score: Int
5+
@Attribute(.init(name: "score") private var score: Int
66
@LiveContext<MyRegistry> private var context
77
@State var editedScore: Int?
88
@State var width: CGFloat = 0

0 commit comments

Comments
 (0)