Skip to content

Commit a454989

Browse files
authored
Merge pull request #138 from liveviewnative/cached-navigation-title
Cache `navigationTitle` for smoother transitions
2 parents 3fc74fc + 81c04ef commit a454989

File tree

5 files changed

+65
-74
lines changed

5 files changed

+65
-74
lines changed

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/ViewModel.swift

Lines changed: 1 addition & 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 {

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
}

0 commit comments

Comments
 (0)