Skip to content

Commit d42f48f

Browse files
Merge branch 'main' into client_refactor_0.4.1-rc-2
2 parents 3e22f4b + 0bcff68 commit d42f48f

File tree

29 files changed

+380
-220
lines changed

29 files changed

+380
-220
lines changed

.github/workflows/elixir.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ env:
1414
jobs:
1515
build:
1616
name: Build and test
17-
runs-on: ubuntu-20.04
17+
runs-on: ubuntu-latest
1818
steps:
1919
- uses: actions/checkout@v3
2020
- name: Set up Elixir

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3333

3434
### Fixed
3535
- Views will now update properly when the server changes the value of a form field (#1483)
36+
- Fixed float parsing for stylesheet rules
3637

3738
## [0.3.1] 2024-10-02
3839

Package.resolved

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Sources/ASTDecodableImplementation/Decoders/InitializerClausesDecoder.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ struct InitializerClausesDecoder<TypeSyntaxType: TypeSyntaxProtocol> {
3434
let errorReference = DeclReferenceExprSyntax(baseName: errorName)
3535

3636
return StructDeclSyntax(
37+
attributes: [.attribute(AttributeSyntax(attributeName: IdentifierTypeSyntax(name: .identifier("MainActor"))))],
3738
name: name,
3839
inheritanceClause: InheritanceClauseSyntax(inheritedTypes: [
3940
// @preconcurrency Swift.Decodable

Sources/LiveViewNative/LiveViewNative.docc/AddCustomModifier.md

Lines changed: 213 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -23,21 +23,27 @@ public extension Addons {
2323
```
2424

2525
Then, add an enum called `CustomModifier` that has cases for each modifier to include.
26-
The framework uses this type to parse modifiers in a stylesheet.
26+
The framework uses this type to decode modifiers in a stylesheet.
2727

28-
Use the ``CustomModifierGroupParser`` to include multiple modifiers.
28+
Conform to the `Decodable` protocol and attempt to decode each modifier type in the `init(from:)` implementation.
29+
30+
`init(from:)` *must* throw if no modifiers can be decoded.
31+
If your `CustomModifier` catches unknown modifiers, modifiers from other addons will get ignored.
2932

3033
```swift
3134
@Addon
3235
struct MyAddon<Root: RootRegistry> {
33-
enum CustomModifier: ViewModifier, ParseableModifierValue {
36+
enum CustomModifier: ViewModifier, @preconcurrency Decodable {
3437
case myFirstModifier(MyFirstModifier<Root>)
3538
case mySecondModifier(MySecondModifier<Root>)
36-
37-
static func parser(in context: ParseableModifierContext) -> some Parser<Substring.UTF8View, Self> {
38-
CustomModifierGroupParser(output: Self.self) {
39-
MyFirstModifier<Root>.parser(in: context).map(Self.myFirstModifier)
40-
MySecondModifier<Root>.parser(in: context).map(Self.mySecondModifier)
39+
40+
init(from decoder: any Decoder) throws {
41+
let container = try decoder.singleValueContainer()
42+
43+
if let modifier = try? container.decode(MyFirstModifier<Root>.self) {
44+
self = .myFirstModifier(modifier)
45+
} else {
46+
self = .mySecondModifier(try container.decode(MySecondModifier<Root>.self))
4147
}
4248
}
4349

@@ -53,118 +59,264 @@ struct MyAddon<Root: RootRegistry> {
5359
}
5460
```
5561

56-
## Parseable Modifiers
62+
## Decoding Modifiers
5763

58-
Each modifier should conform to ``LiveViewNativeStylesheet/ParseableModifierValue``.
59-
You can use the ``LiveViewNativeStylesheet/ParseableExpression()`` macro to synthesize this conformance.
64+
LiveView Native allows you to write modifiers in a stylesheet or `style` attribute with Swift syntax.
65+
These modifiers are converted to an abstract syntax tree format and encoded to JSON.
6066

61-
The macro will synthesize a parser for each `init` clause.
67+
```swift
68+
// stylesheet:
69+
foregroundStyle(Color.red)
70+
```
6271

63-
Any type conforming to ``LiveViewNativeStylesheet/ParseableModifierValue`` can be used as an argument in an `init` clause.
72+
```json
73+
["foregroundStyle", { ... }, [[".", { ... }, ["Color", "red"]]]]
74+
```
6475

65-
```swift
66-
@ParseableExpression
67-
struct MyFirstModifier<Root: RootRegistry>: ViewModifier {
68-
static var name: String { "myFirstModifier" }
76+
Each modifier conforms to `Decodable`, and is expected to decode itself from this JSON format.
77+
78+
### Automatic Decoding
79+
The ``ASTDecodable(_:options:)`` macro can synthesize a decoder directly from your Swift code.
80+
81+
Add the ``ASTDecodable(_:options:)`` macro to your struct, and pass it the name of the modifier.
6982

70-
let color: Color
83+
It will synthesize a decoder for each `init` clause in the struct.
7184

72-
init(_ color: Color) {
73-
self.color = color
85+
```swift
86+
@ASTDecodable("labeled")
87+
struct LabeledModifier<Root: RootRegistry>: ViewModifier, @preconcurrency Decodable {
88+
let label: String
89+
90+
init(as label: Int) {
91+
self.label = String(label)
7492
}
7593

76-
init(red: Double, green: Double, blue: Double) {
77-
self.color = Color(.sRGB, red: red, green: green, blue: blue)
94+
init(as label: String) {
95+
self.label = label
7896
}
7997

8098
func body(content: Content) -> some View {
81-
content
82-
.bold()
83-
.background(color)
99+
LabeledContent {
100+
content
101+
} label: {
102+
Text(label)
103+
}
84104
}
85105
}
86106
```
87107

88-
In the stylesheet, you can use either clause:
108+
In the stylesheet, you can use either `init`:
89109

90110
```swift
91-
// myFirstModifier(_:)
92-
myFirstModifier(.red)
93-
myFirstModifier(.blue)
94-
myFirstModifier(Color(white: 0.5))
111+
labeled(as: 5)
112+
labeled(as: "Label")
113+
```
114+
115+
``ASTDecodable(_:options:)`` will also synthesize decoders for enum cases, static functions, static members, properties, and member functions.
116+
To exclude any of these declarations from the decoder, either prefix them with an underscore (`_`), or define them in an extension.
117+
Static functions, static members, properties, and member functions will only receive a synthesized decoder if they return `Self` (or a type name that matches the declaration ``ASTDecodable(_:options:)`` is attached to).
118+
119+
```swift
120+
@ASTDecodable("MyType")
121+
enum MyType: Decodable {
122+
init() {} // decodable
123+
124+
case enumCase // decodable
125+
126+
static func staticFunction() -> Self { ... } // decodable
127+
static func staticFunction() -> OtherType { ... } // not decodable
128+
static func _staticFunction() -> OtherType { ... } // not decodable
129+
130+
static var staticMember: Self { ... } // decodable
131+
static var staticMember: OtherType { ... } // not decodable
132+
static var _staticMember: OtherType { ... } // not decodable
133+
134+
func memberFunction() -> Self { ... } // decodable
135+
func memberFunction() -> OtherType { ... } // not decodable
136+
func _memberFunction() -> OtherType { ... } // not decodable
137+
138+
var property: Self { ... } // decodable
139+
var property: OtherType { ... } // not decodable
140+
var _property: OtherType { ... } // not decodable
141+
}
95142

96-
// myFirstModifier(red:green:blue:)
97-
myFirstModifier(red: 1, green: 0.5, blue: 0)
143+
extension MyType {
144+
static func staticFunction() -> Self { ... } // not decodable
145+
static var staticMember: Self { ... } // not decodable
146+
func memberFunction() -> Self { ... } // not decodable
147+
var property: Self { ... } // not decodable
148+
}
98149
```
99150

100-
### Nested Content
101-
Use ``ObservedElement`` and ``LiveContext`` to access properties/children of the element the modifier is applied to.
151+
### Attribute References
152+
Any type that conforms to ``AttributeDecodable`` can be wrapped with ``AttributeReference``.
102153

103-
The ``ViewReference`` type can be used to refer to nested children with a `template` attribute.
154+
This will make it usable with the `attr(<name>)` helper in a stylesheet.
104155

105-
```swift
106-
@ParseableExpression
107-
struct MyBackgroundModifier<Root: RootRegistry>: ViewModifier {
108-
static var name: String { "myBackground" }
156+
Use the ``ObservedElement`` and ``LiveContext`` property wrappers to access the element and context needed to resolve these references.
109157

158+
```swift
159+
@ASTDecodable("labeled")
160+
struct LabeledModifier<Root: RootRegistry>: ViewModifier, @preconcurrency Decodable {
110161
@ObservedElement private var element
111162
@LiveContext<Root> private var context
112163

113-
let content: ViewReference
164+
let label: AttributeReference<String>
114165

115-
init(_ content: ViewReference) {
116-
self.content = content
166+
init(as label: AttributeReference<String>) {
167+
self.label = label
117168
}
118169

119170
func body(content: Content) -> some View {
120-
content.background(content.resolve(on: element, in: context))
171+
LabeledContent {
172+
content
173+
} label: {
174+
Text(label.resolve(on: element, in: context))
175+
}
121176
}
122177
}
123178
```
124179

125180
```elixir
126-
"my-background" do
127-
myBackground(:my_content)
181+
"my-title" do
182+
labeled(as: attr("label"))
128183
end
129184
```
130185

131186
```html
132-
<Element class="my-background">
133-
<Text template="my_content">Nested Content</Text>
134-
</Element>
187+
<Element class="my-title" label="My Label" />
135188
```
136189

137-
### Attribute References
138-
Any type that conforms to ``AttributeDecodable`` can be wrapped with ``AttributeReference``.
190+
### Resolvable Types
191+
Stylesheets are static assets, and the modifiers defined in your templates do not change after the application loads.
192+
However, some of their properties can be dynamic using helpers like `attr(<name>)` or `gesture_state(...)`.
139193

140-
This will make it usable with the `attr(<name>)` helper in a stylesheet.
194+
The ``StylesheetResolvable`` protocol defines a type that must be resolved before it can be used.
195+
Many types built-in to SwiftUI have been given a nested `Resolvable` type that conforms to this protocol.
196+
This nested type can be used to decode a SwiftUI primitive in your modifier.
197+
198+
Call ``StylesheetResolvable/resolve(on:in:)`` to get the resolved value for an ``ElementNode`` in a ``LiveContext``.
199+
Use the ``ObservedElement`` and ``LiveContext`` property wrappers to access the element and context needed to resolve these types.
200+
201+
For example, SwiftUI has a built-in `HorizontalAlignment` type. We can use `HorizontalAlignment.Resolvable` to include this in our custom modifier.
141202

142203
```swift
143-
@ParseableExpression
144-
struct MyTitleModifier<Root: RootRegistry>: ViewModifier {
145-
static var name: String { "myTitle" }
204+
@ASTDecodable("labeled")
205+
struct LabeledModifier<Root: RootRegistry>: ViewModifier, @preconcurrency Decodable {
206+
let label: String
207+
let alignment: HorizontalAlignment.Resolvable
208+
209+
init(as label: String, alignment: HorizontalAlignment.Resolvable) {
210+
self.label = label
211+
self.alignment = alignment
212+
}
213+
214+
func body(content: Content) -> some View {
215+
VStack(alignment: alignment.resolve(on: element, in: context)) {
216+
Text(label)
217+
content
218+
}
219+
}
220+
}
221+
```
222+
```swift
223+
// stylesheet:
224+
labeled(as: "Label", alignment: .trailing)
225+
```
226+
227+
#### Resolvable Protocols
228+
229+
Some protocols also have ``StylesheetResolvable`` implementations.
230+
For example, to use `some ShapeStyle` in your modifier, use the ``StylesheetResolvableShapeStyle`` type.
231+
It will resolve to a type-erased `ShapeStyle`.
232+
233+
```swift
234+
@ASTDecodable("fillBackground")
235+
struct FillBackgroundModifier<Root: RootRegistry>: ViewModifier, @preconcurrency Decodable {
236+
let fill: StylesheetResolvableShapeStyle
237+
238+
init(_ fill: StylesheetResolvableShapeStyle) {
239+
self.fill = fill
240+
}
241+
242+
func body(content: Content) -> some View {
243+
content.background(fill)
244+
}
245+
}
246+
```
247+
```swift
248+
// stylesheet:
249+
fillBackground(.regularMaterial)
250+
fillBackground(.red.opacity(attr("opacity")))
251+
```
252+
253+
#### Custom Resolvable Types
254+
255+
You can also conform your own types to `StylesheetResolvable`. This is most useful when you want some properties of your type to support the `attr(<name>)` helper.
256+
257+
```swift
258+
struct Video {
259+
let url: String
260+
let resolution: Int
261+
262+
@ASTDecodable("Video")
263+
struct Resolvable: StylesheetResolvable, Decodable {
264+
let url: AttributeReference<String>
265+
let resolution: AttributeReference<Int>
266+
267+
init(_ url: AttributeReference<String>, in resolution: AttributeReference<Int>) {
268+
self.url = url
269+
self.resolution = resolution
270+
}
271+
272+
func resolve(on element: ElementNode, in context: LiveContext<some RootRegistry>) -> Video {
273+
Video(
274+
url: url.resolve(on: element, in: context),
275+
resolution: resolution.resolve(on: element, in: context)
276+
)
277+
}
278+
}
279+
}
280+
```
281+
```swift
282+
// stylesheet:
283+
backgroundVideo(Video("...", in: 1080))
284+
backgroundVideo(Video(attr("url"), in: attr("resolution")))
285+
```
286+
287+
### Nested Content
288+
Use ``ObservedElement`` and ``LiveContext`` to access properties/children of the element the modifier is applied to.
289+
290+
The ``ViewReference`` type can be used to refer to nested children with a `template` attribute.
146291

292+
```swift
293+
@ASTDecodable("myBackground")
294+
struct MyBackgroundModifier<Root: RootRegistry>: ViewModifier, @preconcurrency Decodable {
147295
@ObservedElement private var element
148296
@LiveContext<Root> private var context
149297

150-
let title: AttributeReference<String>
298+
let content: ViewReference
151299

152-
init(_ title: AttributeReference<String>) {
153-
self.title = title
300+
init(_ content: ViewReference) {
301+
self.content = content
154302
}
155303

156304
func body(content: Content) -> some View {
157-
content.navigationTitle(title.resolve(on: element, in: context))
305+
content.background {
306+
content.resolve(on: element, in: context)
307+
}
158308
}
159309
}
160310
```
161311

162312
```elixir
163-
"my-title" do
164-
myTitle(attr("title"))
313+
"my-background" do
314+
myBackground(:my_content)
165315
end
166316
```
167317

168318
```html
169-
<Element class="my-title" title="My Title" />
319+
<Element class="my-background">
320+
<Text template="my_content">Nested Content</Text>
321+
</Element>
170322
```

0 commit comments

Comments
 (0)