Skip to content

Commit b8da354

Browse files
authored
Move Text/Image attributes to modifiers (#972)
* Add TextModifier type and move text attributes to modifiers * Document Text attributes * Update other docs, examples, and tests * Add image modifiers * Document modifiers and label * Fix watchOS build * Apply suggestions from code review * Attempt test fix
1 parent 633d634 commit b8da354

36 files changed

+933
-355
lines changed
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
//
2+
// AntialiasedModifier.swift
3+
// LiveViewNative
4+
//
5+
// Created by Carson Katri on 6/1/23.
6+
//
7+
8+
import SwiftUI
9+
10+
/// Enables/disables antialiasing.
11+
///
12+
/// ```html
13+
/// <Image system-name="heart.fill" modifiers={antialiased(@native, is_active: true)} />
14+
/// ```
15+
///
16+
/// ## Arguments
17+
/// * ``isActive``
18+
#if swift(>=5.8)
19+
@_documentation(visibility: public)
20+
#endif
21+
struct AntialiasedModifier: ImageModifier, Decodable {
22+
/// Specifies if antialiasing is enabled.
23+
#if swift(>=5.8)
24+
@_documentation(visibility: public)
25+
#endif
26+
private let isActive: Bool
27+
28+
func apply(to image: SwiftUI.Image) -> SwiftUI.Image {
29+
image.antialiased(isActive)
30+
}
31+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
//
2+
// ImageModifier.swift
3+
// LiveViewNative
4+
//
5+
// Created by Carson Katri on 6/1/23.
6+
//
7+
8+
import SwiftUI
9+
import LiveViewNativeCore
10+
11+
enum ImageModifierType: String, Decodable {
12+
case resizable
13+
case antialiased
14+
case symbolRenderingMode = "symbol_rendering_mode"
15+
case renderingMode = "rendering_mode"
16+
case interpolation
17+
18+
func decode(from decoder: Decoder) throws -> any ImageModifier {
19+
switch self {
20+
case .resizable:
21+
return try ResizableModifier(from: decoder)
22+
case .antialiased:
23+
return try AntialiasedModifier(from: decoder)
24+
case .symbolRenderingMode:
25+
return try SymbolRenderingModeModifier(from: decoder)
26+
case .renderingMode:
27+
return try RenderingModeModifier(from: decoder)
28+
case .interpolation:
29+
return try InterpolationModifier(from: decoder)
30+
}
31+
}
32+
}
33+
34+
/// A modifier that applies to an ``Image``.
35+
protocol ImageModifier {
36+
/// Modify the `Image` and return the new `Image` type.
37+
func apply(to image: SwiftUI.Image) -> SwiftUI.Image
38+
}
39+
40+
struct ImageModifierStack: Decodable, AttributeDecodable {
41+
var stack: [any ImageModifier]
42+
43+
init(_ stack: [any ImageModifier]) {
44+
self.stack = stack
45+
}
46+
47+
init(from attribute: LiveViewNativeCore.Attribute?) throws {
48+
guard let value = attribute?.value else { throw AttributeDecodingError.missingAttribute(Self.self) }
49+
self = try makeJSONDecoder().decode(Self.self, from: Data(value.utf8))
50+
}
51+
52+
enum ImageModifierContainer: Decodable {
53+
case modifier(any ImageModifier)
54+
case end
55+
56+
init(from decoder: Decoder) throws {
57+
let type = try decoder.container(keyedBy: CodingKeys.self).decode(String.self, forKey: .type)
58+
if let modifierType = ImageModifierType(rawValue: type) {
59+
self = .modifier(try modifierType.decode(from: decoder))
60+
} else {
61+
self = .end
62+
}
63+
}
64+
65+
enum CodingKeys: CodingKey {
66+
case type
67+
}
68+
}
69+
70+
init(from decoder: Decoder) throws {
71+
var container = try decoder.unkeyedContainer()
72+
self.stack = []
73+
while !container.isAtEnd {
74+
switch try container.decode(ImageModifierContainer.self) {
75+
case let .modifier(modifier):
76+
self.stack.append(modifier)
77+
case .end:
78+
return
79+
}
80+
}
81+
}
82+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
//
2+
// InterpolationModifier.swift
3+
//
4+
//
5+
// Created by Carson.Katri on 6/1/23.
6+
//
7+
8+
import SwiftUI
9+
10+
/// Set the quality level for an ``Image`` that requires interpolation.
11+
///
12+
/// When an image is resized (such as with the ``ResizableModifier`` modifier), it is interpolated.
13+
/// Use this modifier to set the quality of interpolation.
14+
///
15+
/// ```html
16+
/// <Image
17+
/// name="dot_green"
18+
/// modifiers={@native |> resizable() |> interpolation(interpolation: :none)}
19+
/// />
20+
/// <Image
21+
/// name="dot_green"
22+
/// modifiers={@native |> resizable() |> interpolation(interpolation: :medium)}
23+
/// />
24+
/// ```
25+
///
26+
/// ## Arguments
27+
/// * ``interpolation``
28+
#if swift(>=5.8)
29+
@_documentation(visibility: public)
30+
#endif
31+
struct InterpolationModifier: ImageModifier, Decodable {
32+
/// The interpolation quality.
33+
///
34+
/// See ``LiveViewNative/SwiftUI/Image/Interpolation`` for a list of possible values.
35+
#if swift(>=5.8)
36+
@_documentation(visibility: public)
37+
#endif
38+
private let interpolation: SwiftUI.Image.Interpolation
39+
40+
func apply(to image: SwiftUI.Image) -> SwiftUI.Image {
41+
image.interpolation(interpolation)
42+
}
43+
}
44+
45+
/// The quality level used to render an interpolated ``Image``.
46+
///
47+
/// Possible values:
48+
/// * `low`
49+
/// * `medium`
50+
/// * `high`
51+
/// * `none`
52+
#if swift(>=5.8)
53+
@_documentation(visibility: public)
54+
#endif
55+
extension SwiftUI.Image.Interpolation: Decodable {
56+
public init(from decoder: Decoder) throws {
57+
let container = try decoder.singleValueContainer()
58+
switch try container.decode(String.self) {
59+
case "low":
60+
self = .low
61+
case "medium":
62+
self = .medium
63+
case "high":
64+
self = .high
65+
case "none":
66+
self = .none
67+
case let `default`: throw DecodingError.dataCorrupted(.init(codingPath: container.codingPath, debugDescription: "Unknown interpolation '\(`default`)'"))
68+
}
69+
}
70+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
//
2+
// RenderingModeModifier.swift
3+
// LiveViewNative
4+
//
5+
// Created by Carson Katri on 6/1/23.
6+
//
7+
8+
import SwiftUI
9+
10+
/// Specifies how ``Image`` elements are rendered.
11+
///
12+
/// The `original` mode renders pixels as they appear in the original image.
13+
/// The `template` mode renders nontransparent pixels as the foreground color.
14+
///
15+
/// ```html
16+
/// <Image name="dot_green" modifiers={rendering_mode(@native, mode: :original)} />
17+
/// <Image name="dot_green" modifiers={rendering_mode(@native, mode: :template)} />
18+
/// ```
19+
///
20+
/// This modifier can also be used to render multicolor SF Symbols.
21+
/// The `original` mode allows the symbol to use its predefined colors.
22+
///
23+
/// ```html
24+
/// <Image
25+
/// system-name="person.crop.circle.badge.plus"
26+
/// modifiers={rendering_mode(@native, mode: :original)}
27+
/// />
28+
/// ```
29+
///
30+
/// ## Arguments
31+
/// * ``mode``
32+
#if swift(>=5.8)
33+
@_documentation(visibility: public)
34+
#endif
35+
struct RenderingModeModifier: ImageModifier, Decodable {
36+
/// The rendering mode to use.
37+
///
38+
/// See ``LiveViewNative/SwiftUI/Image/TemplateRenderingMode`` for a list of possible values.
39+
#if swift(>=5.8)
40+
@_documentation(visibility: public)
41+
#endif
42+
private let mode: SwiftUI.Image.TemplateRenderingMode
43+
44+
func apply(to image: SwiftUI.Image) -> SwiftUI.Image {
45+
image.renderingMode(mode)
46+
}
47+
}
48+
49+
/// The mode used to render an ``Image``.
50+
///
51+
/// Possible values:
52+
/// * `original`
53+
/// * `template`
54+
#if swift(>=5.8)
55+
@_documentation(visibility: public)
56+
#endif
57+
extension SwiftUI.Image.TemplateRenderingMode: Decodable {
58+
public init(from decoder: Decoder) throws {
59+
let container = try decoder.singleValueContainer()
60+
switch try container.decode(String.self) {
61+
case "original":
62+
self = .original
63+
case "template":
64+
self = .template
65+
case let `default`: throw DecodingError.dataCorrupted(.init(codingPath: container.codingPath, debugDescription: "Unknown rendering mode '\(`default`)'"))
66+
}
67+
}
68+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
//
2+
// ResizableModifier.swift
3+
// LiveViewNative
4+
//
5+
// Created by Carson Katri on 6/1/23.
6+
//
7+
8+
import SwiftUI
9+
10+
/// Enables an image to fill the available space.
11+
///
12+
/// ```html
13+
/// <Image system-name="heart.fill" modifiers={resizable(@native)} />
14+
/// <Image system-name="heart.fill" modifiers={resizable(@native, resizing_mode: :tile)} />
15+
/// ```
16+
///
17+
/// ## Arguments
18+
/// * ``capInsets``
19+
/// * ``resizingMode``
20+
#if swift(>=5.8)
21+
@_documentation(visibility: public)
22+
#endif
23+
struct ResizableModifier: ImageModifier, Decodable {
24+
/// Marks an inset that is not resized.
25+
///
26+
/// See ``LiveViewNative/SwiftUI/EdgeInsets`` for more details.
27+
#if swift(>=5.8)
28+
@_documentation(visibility: public)
29+
#endif
30+
private let capInsets: EdgeInsets?
31+
32+
/// The mode for resizing.
33+
///
34+
/// See ``LiveViewNative/SwiftUI/Image/ResizingMode`` for a list of possible values.
35+
#if swift(>=5.8)
36+
@_documentation(visibility: public)
37+
#endif
38+
private let resizingMode: SwiftUI.Image.ResizingMode
39+
40+
func apply(to image: SwiftUI.Image) -> SwiftUI.Image {
41+
image.resizable(capInsets: capInsets ?? .init(), resizingMode: resizingMode)
42+
}
43+
}
44+
45+
/// The mode used to resize an ``Image``.
46+
///
47+
/// Possible values:
48+
/// * `stretch`
49+
/// * `tile`
50+
#if swift(>=5.8)
51+
@_documentation(visibility: public)
52+
#endif
53+
extension SwiftUI.Image.ResizingMode: Decodable {
54+
public init(from decoder: Decoder) throws {
55+
let container = try decoder.singleValueContainer()
56+
switch try container.decode(String.self) {
57+
case "stretch":
58+
self = .stretch
59+
case "tile":
60+
self = .tile
61+
case let `default`: throw DecodingError.dataCorrupted(.init(codingPath: container.codingPath, debugDescription: "Unknown resizing mode '\(`default`)'"))
62+
}
63+
}
64+
}

Sources/LiveViewNative/Modifiers/Drawing and Graphics Modifiers/ForegroundColorModifier.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import SwiftUI
1919
#if swift(>=5.8)
2020
@_documentation(visibility: public)
2121
#endif
22-
struct ForegroundColorModifier: ViewModifier, Decodable {
22+
struct ForegroundColorModifier: ViewModifier, Decodable, TextModifier {
2323
/// The foreground color to use when rendering the view.
2424
#if swift(>=5.8)
2525
@_documentation(visibility: public)
@@ -29,4 +29,8 @@ struct ForegroundColorModifier: ViewModifier, Decodable {
2929
func body(content: Content) -> some View {
3030
content.foregroundColor(color)
3131
}
32+
33+
func apply(to text: SwiftUI.Text) -> SwiftUI.Text {
34+
text.foregroundColor(color)
35+
}
3236
}

Sources/LiveViewNative/Modifiers/Images Modifiers/SymbolRenderingModeModifier.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import SwiftUI
2020
#if swift(>=5.8)
2121
@_documentation(visibility: public)
2222
#endif
23-
struct SymbolRenderingModeModifier: ViewModifier, Decodable {
23+
struct SymbolRenderingModeModifier: ViewModifier, Decodable, ImageModifier {
2424
/// A symbol rendering mode.
2525
#if swift(>=5.8)
2626
@_documentation(visibility: public)
@@ -48,6 +48,10 @@ struct SymbolRenderingModeModifier: ViewModifier, Decodable {
4848
content.symbolRenderingMode(mode)
4949
}
5050

51+
func apply(to image: SwiftUI.Image) -> SwiftUI.Image {
52+
image.symbolRenderingMode(mode)
53+
}
54+
5155
enum CodingKeys: String, CodingKey {
5256
case mode
5357
}

Sources/LiveViewNative/Modifiers/Layout Adjustments Modifiers/SafeAreaInsetModifier.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import SwiftUI
1919
/// ...
2020
/// <GroupBox template={:bottom_bar} id="bottom_bar">
2121
/// <HStack template={:label}>
22-
/// <Text font-weight="bold" font="title2">Bottom Bar</Text>
22+
/// <Text>Bottom Bar</Text>
2323
/// <Spacer />
2424
/// </HStack>
2525
/// <Text>This will allow the list to scroll further up so no rows are covered.</Text>

Sources/LiveViewNative/Modifiers/Lists Modifiers/BadgeModifier.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ import SwiftUI
3939
/// <List>
4040
/// <Text id="a" modifiers={@native |> badge(content: :error)}>
4141
/// Server A
42-
/// <Text template={:error} color="system-red">
42+
/// <Text template={:error} modifiers={foreground_color(:red)}>
4343
/// Down
4444
/// </Text>
4545
/// </Text>
@@ -86,7 +86,7 @@ struct BadgeModifier<R: RootRegistry>: ViewModifier, Decodable {
8686
#if os(iOS) || os(macOS)
8787
if let reference = self.content {
8888
content
89-
.badge(context.children(of: element, forTemplate: reference).first?.asElement().flatMap(Text<R>.init(overrideElement:))?.body)
89+
.badge(context.children(of: element, forTemplate: reference).first?.asElement().flatMap(Text<R>.init(element:))?.body)
9090
} else if let label {
9191
content.badge(label)
9292
} else if let count {

0 commit comments

Comments
 (0)