Skip to content

Commit bba3b0e

Browse files
committed
Merge branch 'main' of github.com:liveviewnative/liveview-client-swiftui into progress-view
2 parents de7da73 + e304362 commit bba3b0e

File tree

13 files changed

+293
-99
lines changed

13 files changed

+293
-99
lines changed

Sources/LiveViewNative/BuiltinRegistry.swift

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ struct BuiltinRegistry {
1919
case "securefield":
2020
SecureField<R>(element: element, context: context)
2121
case "text":
22-
Text(element: element, context: context)
22+
Text(context: context)
2323
case "hstack":
2424
HStack<R>(element: element, context: context)
2525
case "vstack":
@@ -44,11 +44,24 @@ struct BuiltinRegistry {
4444
Shape(element: element, context: context, shape: Rectangle())
4545
case "roundedrectangle":
4646
Shape(element: element, context: context, shape: RoundedRectangle(from: element))
47+
case "circle":
48+
Shape(element: element, context: context, shape: Circle())
49+
case "ellipse":
50+
Shape(element: element, context: context, shape: Ellipse())
51+
case "capsule":
52+
Shape(element: element, context: context, shape: Capsule(from: element))
53+
case "containerrelativeshape":
54+
Shape(element: element, context: context, shape: ContainerRelativeShape())
4755
case "lvn-link":
4856
Link(element: element, context: context)
49-
case "progressview":
57+
case "progress-view":
5058
ProgressView(element: element, context: context)
51-
59+
case "divider":
60+
Divider()
61+
case "edit-button":
62+
EditButton()
63+
case "toggle":
64+
Toggle(element: element, context: context)
5265
case "phx-form":
5366
PhxForm<R>(element: element, context: context)
5467
case "phx-submit-button":
@@ -61,9 +74,9 @@ struct BuiltinRegistry {
6174

6275
enum ModifierType: String {
6376
case frame
64-
case listRowInsets
65-
case listRowSeparator
66-
case navigationTitle
77+
case listRowInsets = "list_row_insets"
78+
case listRowSeparator = "list_row_separator"
79+
case navigationTitle = "navigation_title"
6780
case padding
6881
case tint
6982
}

Sources/LiveViewNative/LiveView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,6 @@ public struct LiveView<R: CustomRegistry>: View {
115115
}
116116

117117
private var navigationRoot: some View {
118-
NavStackEntryView(.init(url: rootCoordinator.url, coordinator: rootCoordinator))
118+
NavStackEntryView(.init(url: session.url, coordinator: rootCoordinator))
119119
}
120120
}

Sources/LiveViewNative/Modifiers/NavigationTitleModifier.swift

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,23 +8,36 @@
88
import SwiftUI
99

1010
struct NavigationTitleModifier: ViewModifier, Decodable, Equatable {
11-
private let title: String
11+
fileprivate let title: String
12+
private let cached: Bool
1213

1314
init(from decoder: Decoder) throws {
1415
let container = try decoder.container(keyedBy: CodingKeys.self)
1516
self.title = try container.decode(String.self, forKey: .title)
17+
self.cached = false
1618
}
1719

18-
init(title: String) {
20+
init(title: String, cached: Bool = false) {
1921
self.title = title
22+
self.cached = cached
2023
}
2124

2225
func body(content: Content) -> some View {
2326
content
2427
.navigationTitle(title)
28+
.preference(key: NavigationTitleModifierKey.self, value: cached ? nil : self)
2529
}
2630

2731
enum CodingKeys: String, CodingKey {
2832
case title
2933
}
3034
}
35+
36+
/// A key that passes the navigation title up the View hierarchy via preferences.
37+
enum NavigationTitleModifierKey: PreferenceKey {
38+
static var defaultValue: NavigationTitleModifier?
39+
40+
static func reduce(value: inout NavigationTitleModifier?, nextValue: () -> NavigationTitleModifier?) {
41+
value = nextValue().flatMap({ .init(title: $0.title, cached: true) }) ?? value
42+
}
43+
}

Sources/LiveViewNative/NavStackEntryView.swift

Lines changed: 39 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -41,33 +41,48 @@ struct NavStackEntryView<R: CustomRegistry>: View {
4141

4242
@ViewBuilder
4343
private var elementTree: some View {
44-
switch coordinator.state {
45-
case .connected:
46-
if let doc = coordinator.document {
47-
coordinator.builder.fromNodes(doc[doc.root()].children(), coordinator: coordinator, url: coordinator.url)
48-
.environment(\.coordinatorEnvironment, CoordinatorEnvironment(coordinator, document: doc))
49-
} else {
50-
fatalError("State is `.connected`, but no `Document` was found.")
51-
}
52-
default:
53-
if R.LoadingView.self == Never.self {
54-
switch coordinator.state {
55-
case .connected:
56-
fatalError()
57-
case .notConnected:
58-
SwiftUI.Text("Not Connected")
59-
case .connecting:
60-
SwiftUI.Text("Connecting")
61-
case .connectionFailed(let error):
62-
SwiftUI.VStack {
63-
SwiftUI.Text("Connection Failed")
64-
.font(.subheadline)
65-
SwiftUI.Text(error.localizedDescription)
44+
if coordinator.url == entry.url {
45+
switch coordinator.state {
46+
case .connected:
47+
if let doc = coordinator.document {
48+
coordinator.builder.fromNodes(doc[doc.root()].children(), coordinator: coordinator, url: coordinator.url)
49+
.environment(\.coordinatorEnvironment, CoordinatorEnvironment(coordinator, document: doc))
50+
.onPreferenceChange(NavigationTitleModifierKey.self) { navigationTitle in
51+
self.liveViewModel.cachedNavigationTitle = navigationTitle
52+
print("Nav title changed")
53+
}
54+
} else {
55+
fatalError("State is `.connected`, but no `Document` was found.")
56+
}
57+
default:
58+
let content = Group {
59+
if R.LoadingView.self == Never.self {
60+
switch coordinator.state {
61+
case .connected:
62+
fatalError()
63+
case .notConnected:
64+
SwiftUI.Text("Not Connected")
65+
case .connecting:
66+
SwiftUI.Text("Connecting")
67+
case .connectionFailed(let error):
68+
SwiftUI.VStack {
69+
SwiftUI.Text("Connection Failed")
70+
.font(.subheadline)
71+
SwiftUI.Text(error.localizedDescription)
72+
}
73+
}
74+
} else {
75+
R.loadingView(for: coordinator.url, state: coordinator.state)
6676
}
6777
}
68-
} else {
69-
R.loadingView(for: coordinator.url, state: coordinator.state)
78+
if let cachedNavigationTitle = liveViewModel.cachedNavigationTitle {
79+
content.modifier(cachedNavigationTitle)
80+
} else {
81+
content
82+
}
7083
}
84+
} else if let cachedNavigationTitle = liveViewModel.cachedNavigationTitle {
85+
SwiftUI.Text("").modifier(cachedNavigationTitle)
7186
}
7287
}
7388
}

Sources/LiveViewNative/Property Wrappers/ObservedElement.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import Combine
2424
/// ## Example
2525
/// ```swift
2626
/// struct MyView: View {
27-
/// @ObservedElemenet private var element: ElementNode
27+
/// @ObservedElement private var element: ElementNode
2828
///
2929
/// var body: some View {
3030
/// Text("Value: \(element.attributeValue(for: "my-attr") ?? "<none>")")

Sources/LiveViewNative/ViewModel.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import LiveViewNativeCore
1414
/// In a view in the LiveView tree, a model can be obtained using `@EnvironmentObject`.
1515
public class LiveViewModel<R: CustomRegistry>: ObservableObject {
1616
private var forms = [String: FormModel]()
17+
var cachedNavigationTitle: NavigationTitleModifier?
1718

1819
/// Get or create a ``FormModel`` for the `<form>` element with the given ID.
1920
public func getForm(elementID id: String) -> FormModel {
@@ -206,3 +207,15 @@ extension String: FormValue {
206207
self = formValue
207208
}
208209
}
210+
211+
extension Bool: FormValue {
212+
public var formValue: String {
213+
self.description
214+
}
215+
216+
public init?(formValue: String) {
217+
guard let value = Bool(formValue)
218+
else { return nil }
219+
self = value
220+
}
221+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
//
2+
// Toggle.swift
3+
//
4+
//
5+
// Created by Carson Katri on 1/17/23.
6+
//
7+
8+
import SwiftUI
9+
10+
struct Toggle<R: CustomRegistry>: View {
11+
@ObservedElement private var element: ElementNode
12+
let context: LiveContext<R>
13+
14+
@FormState(default: false) var value: Bool
15+
16+
init(element: ElementNode, context: LiveContext<R>) {
17+
self.context = context
18+
}
19+
20+
public var body: some View {
21+
SwiftUI.Toggle(isOn: $value) {
22+
context.buildChildren(of: element)
23+
}
24+
.applyToggleStyle(
25+
element.attributeValue(for: "toggle-style").flatMap(ToggleStyle.init) ?? .automatic
26+
)
27+
}
28+
}
29+
30+
fileprivate enum ToggleStyle: String {
31+
case automatic
32+
case button
33+
case `switch`
34+
#if os(macOS)
35+
case checkbox
36+
#endif
37+
}
38+
39+
fileprivate extension View {
40+
@ViewBuilder
41+
func applyToggleStyle(_ style: ToggleStyle) -> some View {
42+
switch style {
43+
case .automatic:
44+
self.toggleStyle(.automatic)
45+
case .button:
46+
self.toggleStyle(.button)
47+
case .`switch`:
48+
self.toggleStyle(.switch)
49+
#if os(macOS)
50+
case .checkbox:
51+
self.toggleStyle(.checkbox)
52+
#endif
53+
}
54+
}
55+
}

Sources/LiveViewNative/Views/Layout Containers/Presentation Containers/NavigationLink.swift

Lines changed: 9 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -21,56 +21,18 @@ struct NavigationLink<R: CustomRegistry>: View {
2121

2222
@ViewBuilder
2323
public var body: some View {
24-
if let linkOpts = LinkOptions(element: element),
25-
context.coordinator.session.config.navigationMode.supportsLinkState(linkOpts.state),
26-
let url = linkOpts.url(in: context)
27-
{
28-
SwiftUI.NavigationLink(value: LiveNavigationEntry(url: url, coordinator: context.coordinator)) {
24+
if let href = element.attributeValue(for: "destination").flatMap({
25+
URL(string: $0, relativeTo: context.coordinator.url)?.appending(path: "").absoluteURL
26+
}) {
27+
SwiftUI.NavigationLink(
28+
value: LiveNavigationEntry(
29+
url: href,
30+
coordinator: context.coordinator
31+
)
32+
) {
2933
context.buildChildren(of: element)
3034
}
3135
.disabled(element.attribute(named: "disabled") != nil)
32-
} else {
33-
// if there are no link options, or the coordinator doesn't support the required navigation, we don't show anything
34-
}
35-
}
36-
}
37-
38-
struct LinkOptions {
39-
let kind: LinkKind
40-
let state: LiveRedirect.Kind
41-
let href: String
42-
43-
init?(element: ElementNode) {
44-
guard let kindStr = element.attributeValue(for: "data-phx-link"),
45-
let kind = LinkKind(rawValue: kindStr),
46-
let stateStr = element.attributeValue(for: "data-phx-link-state"),
47-
let state = LiveRedirect.Kind(rawValue: stateStr) else {
48-
return nil
49-
}
50-
self.kind = kind
51-
self.state = state
52-
self.href = element.attributeValue(for: "data-phx-href")!
53-
}
54-
55-
@MainActor
56-
func url<R: CustomRegistry>(in context: LiveContext<R>) -> URL? {
57-
.init(string: href, relativeTo: context.coordinator.url)?.appending(path: "/").absoluteURL
58-
}
59-
}
60-
61-
enum LinkKind: String {
62-
case redirect
63-
}
64-
65-
extension LiveSessionConfiguration.NavigationMode {
66-
func supportsLinkState(_ state: LiveRedirect.Kind) -> Bool {
67-
switch self {
68-
case .disabled:
69-
return false
70-
case .replaceOnly:
71-
return state == .replace
72-
case .enabled, .splitView:
73-
return true
7436
}
7537
}
7638
}

Sources/LiveViewNative/Views/Layout Containers/Scroll Views/ScrollView.swift

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,28 @@ struct ScrollView<R: CustomRegistry>: View {
1616
}
1717

1818
public var body: some View {
19-
SwiftUI.ScrollView {
19+
SwiftUI.ScrollView(axes, showsIndicators: showsIndicators) {
2020
context.buildChildren(of: element)
2121
}
2222
}
23+
24+
private var axes: Axis.Set {
25+
switch element.attributeValue(for: "axes") {
26+
case "all":
27+
return [.horizontal, .vertical]
28+
case "horizontal":
29+
return .horizontal
30+
default:
31+
return .vertical
32+
}
33+
}
34+
35+
private var showsIndicators: Bool {
36+
if let attr = element.attributeValue(for: "shows-indicators") {
37+
return attr == "true"
38+
} else {
39+
return true
40+
}
41+
}
42+
2343
}

Sources/LiveViewNative/Views/Shapes/Shape.swift

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,33 @@ struct Shape<S: SwiftUI.Shape>: View {
2828

2929
extension RoundedRectangle {
3030
init(from element: ElementNode) {
31-
let radius: Double
32-
if let s = element.attributeValue(for: "corner-radius"), let d = Double(s) {
33-
radius = d
34-
} else {
35-
radius = 5
31+
let radius = element.attributeValue(for: "corner-radius").flatMap(Double.init) ?? 0
32+
self.init(
33+
cornerSize: .init(
34+
width: element.attributeValue(for: "corner-width").flatMap(Double.init) ?? radius,
35+
height: element.attributeValue(for: "corner-height").flatMap(Double.init) ?? radius
36+
),
37+
style: (element.attributeValue(for: "style").flatMap(RoundedCornerStyle.init) ?? .circular).style
38+
)
39+
}
40+
}
41+
42+
extension Capsule {
43+
init(from element: ElementNode) {
44+
self.init(
45+
style: (element.attributeValue(for: "style").flatMap(RoundedCornerStyle.init) ?? .circular).style
46+
)
47+
}
48+
}
49+
50+
private enum RoundedCornerStyle: String {
51+
case circular
52+
case continuous
53+
54+
var style: SwiftUI.RoundedCornerStyle {
55+
switch self {
56+
case .circular: return .circular
57+
case .continuous: return .continuous
3658
}
37-
self.init(cornerRadius: radius)
3859
}
3960
}

0 commit comments

Comments
 (0)