|
1 | 1 | /// Type to indicate the root of the NavigationStack. This is internal to prevent root accidentally showing instead
|
2 | 2 | /// of a detail view.
|
3 |
| -// struct NavigationStackRootPath: Codable {} |
4 |
| - |
5 |
| -// /// A view that displays a root view and enables you to present additional views over the root view. |
6 |
| -// /// |
7 |
| -// /// Use .navigationDestination(for:destination:) on this view instead of its children unlike Apples SwiftUI API. |
8 |
| -// public struct NavigationStack<Detail: View>: TypeSafeView, View { |
9 |
| -// typealias Children = NavigationStackChildren<Detail> |
10 |
| - |
11 |
| -// public var body = EmptyView() |
12 |
| - |
13 |
| -// /// A binding to the current navigation path. |
14 |
| -// var path: Binding<NavigationPath> |
15 |
| -// /// The types handled by each destination (in the same order as their |
16 |
| -// /// corresponding views in the stack). |
17 |
| -// var destinationTypes: [any Codable.Type] |
18 |
| -// /// Gets a recursive ``EitherView`` structure which will have a single view |
19 |
| -// /// visible suitable for displaying the given path element (based on its |
20 |
| -// /// type). |
21 |
| -// /// |
22 |
| -// /// It's implemented as a recursive structure because that's the best way to keep this |
23 |
| -// /// typesafe without introducing some crazy generated pseudo-variadic storage types of |
24 |
| -// /// some sort. This way we can easily have unlimited navigation destinations and there's |
25 |
| -// /// just a single simple method for adding a navigation destination. |
26 |
| -// var child: (any Codable) -> Detail? |
27 |
| -// /// The elements of the navigation path. The result can depend on |
28 |
| -// /// ``NavigationStack/destinationTypes`` which determines how the keys are |
29 |
| -// /// decoded if they haven't yet been decoded (this happens if they're loaded |
30 |
| -// /// from disk for persistence). |
31 |
| -// var elements: [any Codable] { |
32 |
| -// let resolvedPath = path.wrappedValue.path( |
33 |
| -// destinationTypes: destinationTypes |
34 |
| -// ) |
35 |
| -// return [NavigationStackRootPath()] + resolvedPath |
36 |
| -// } |
37 |
| - |
38 |
| -// /// Creates a navigation stack with heterogeneous navigation state that you can control. |
39 |
| -// /// - Parameters: |
40 |
| -// /// - path: A `Binding` to the navigation state for this stack. |
41 |
| -// /// - root: The view to display when the stack is empty. |
42 |
| -// public init( |
43 |
| -// path: Binding<NavigationPath>, |
44 |
| -// @ViewBuilder _ root: @escaping () -> Detail |
45 |
| -// ) { |
46 |
| -// self.path = path |
47 |
| -// destinationTypes = [] |
48 |
| -// child = { element in |
49 |
| -// if element is NavigationStackRootPath { |
50 |
| -// return root() |
51 |
| -// } else { |
52 |
| -// return nil |
53 |
| -// } |
54 |
| -// } |
55 |
| -// } |
56 |
| - |
57 |
| -// /// Associates a destination view with a presented data type for use within a navigation stack. |
58 |
| -// /// |
59 |
| -// /// Add this view modifer to describe the view that the stack displays when presenting a particular |
60 |
| -// /// kind of data. Use a `NavigationLink` to present the data. You can add more than one navigation |
61 |
| -// /// destination modifier to the stack if it needs to present more than one kind of data. |
62 |
| -// /// - Parameters: |
63 |
| -// /// - data: The type of data that this destination matches. |
64 |
| -// /// - destination: A view builder that defines a view to display when the stack’s navigation |
65 |
| -// /// state contains a value of type data. The closure takes one argument, which is the value |
66 |
| -// /// of the data to present. |
67 |
| -// public func navigationDestination<D: Codable, C: View>( |
68 |
| -// for data: D.Type, @ViewBuilder destination: @escaping (D) -> C |
69 |
| -// ) -> NavigationStack<EitherView<Detail, C>> { |
70 |
| -// // Adds another detail view by adding to the recursive structure of either views created |
71 |
| -// // to display details in a type-safe manner. See NavigationStack.child for details. |
72 |
| -// return NavigationStack<EitherView<Detail, C>>( |
73 |
| -// previous: self, |
74 |
| -// destination: destination |
75 |
| -// ) |
76 |
| -// } |
77 |
| - |
78 |
| -// /// Add a destination for a specific path element (by adding another layer of ``EitherView``). |
79 |
| -// private init<PreviousDetail: View, NewDetail: View, Component: Codable>( |
80 |
| -// previous: NavigationStack<PreviousDetail>, |
81 |
| -// destination: @escaping (Component) -> NewDetail? |
82 |
| -// ) where Detail == EitherView<PreviousDetail, NewDetail> { |
83 |
| -// path = previous.path |
84 |
| -// destinationTypes = previous.destinationTypes + [Component.self] |
85 |
| -// child = { |
86 |
| -// if let previous = previous.child($0) { |
87 |
| -// // Either root or previously defined destination returned a view |
88 |
| -// return EitherView(previous) |
89 |
| -// } else if let component = $0 as? Component, let new = destination(component) { |
90 |
| -// // This destination returned a detail view for the current element |
91 |
| -// return EitherView(new) |
92 |
| -// } else { |
93 |
| -// // Possibly a future .navigationDestination will handle this path element |
94 |
| -// return nil |
95 |
| -// } |
96 |
| -// } |
97 |
| -// } |
98 |
| - |
99 |
| -// /// Attempts to compute the detail view for the given element (the type of |
100 |
| -// /// the element decides which detail is shown). Crashes if no suitable detail |
101 |
| -// /// view is found. |
102 |
| -// func childOrCrash(for element: any Codable) -> Detail { |
103 |
| -// guard let child = child(element) else { |
104 |
| -// fatalError( |
105 |
| -// "Failed to find detail view for \"\(element)\", make sure you have called .navigationDestination for this type." |
106 |
| -// ) |
107 |
| -// } |
108 |
| - |
109 |
| -// return child |
110 |
| -// } |
111 |
| - |
112 |
| -// func children<Backend: AppBackend>( |
113 |
| -// backend: Backend, snapshots: [ViewGraphSnapshotter.NodeSnapshot]? |
114 |
| -// ) -> Children { |
115 |
| -// return NavigationStackChildren(from: self, backend: backend, snapshots: snapshots) |
116 |
| -// } |
117 |
| - |
118 |
| -// func updateChildren<Backend: AppBackend>(_ children: Children, backend: Backend) { |
119 |
| -// children.update(with: self, backend: backend) |
120 |
| -// } |
121 |
| - |
122 |
| -// func asWidget<Backend: AppBackend>( |
123 |
| -// _ children: Children, backend: Backend |
124 |
| -// ) -> Backend.Widget { |
125 |
| -// return children.container.into() |
126 |
| -// } |
127 |
| - |
128 |
| -// func update<Backend: AppBackend>( |
129 |
| -// _ widget: Backend.Widget, children: Children, backend: Backend |
130 |
| -// ) {} |
131 |
| -// } |
132 |
| - |
133 |
| -// /// Stores view graph nodes for the detail views of all elements in the current navigation |
134 |
| -// /// path (to allow animating back and forth as the navigation path changes). |
135 |
| -// /// |
136 |
| -// /// The nodes are simply retrieved by calling ``NavigationStack.child`` with every single |
137 |
| -// /// element in the navigation path and then type erasing the views in ``AnyViewGraphNode``s. |
138 |
| -// /// |
139 |
| -// /// Unlike most ``ViewGraphNodeChildren`` implementations (but similarly to ``ForEachChildren``), |
140 |
| -// /// this implementation manages the parent ``NavigationStack``'s one-of container as well |
141 |
| -// /// to have the complexity in a single type. Most of the complexity is from trying to add |
142 |
| -// /// and remove children in just the right way to allow animations to remain fluid even when |
143 |
| -// /// the navigation path changes drastically (e.g. 5 elements of the path getting popped at once). |
144 |
| -// class NavigationStackChildren<Child: View>: ViewGraphNodeChildren { |
145 |
| -// /// When a view is popped we store it in here to remove from the stack |
146 |
| -// /// the next time views are added. This allows them to animate out. |
147 |
| -// var widgetsQueuedForRemoval: [AnyWidget] = [] |
148 |
| -// var nodes: [AnyViewGraphNode<Child>] = [] |
149 |
| -// var container: AnyWidget |
150 |
| - |
151 |
| -// init(container: AnyWidget) { |
152 |
| -// self.container = container |
153 |
| -// } |
154 |
| - |
155 |
| -// /// This could be set to false for NavigationSplitView in the future |
156 |
| -// let alwaysShowTopView = true |
157 |
| - |
158 |
| -// var widgets: [AnyWidget] { |
159 |
| -// [container] |
160 |
| -// } |
161 |
| - |
162 |
| -// var erasedNodes: [ErasedViewGraphNode] { |
163 |
| -// nodes.map(ErasedViewGraphNode.init(wrapping:)) |
164 |
| -// } |
165 |
| - |
166 |
| -// init<Backend: AppBackend>( |
167 |
| -// from view: NavigationStack<Child>, |
168 |
| -// backend: Backend, |
169 |
| -// snapshots: [ViewGraphSnapshotter.NodeSnapshot]? |
170 |
| -// ) { |
171 |
| -// container = AnyWidget(backend.createOneOfContainer()) |
172 |
| - |
173 |
| -// nodes = view.elements |
174 |
| -// .map(view.childOrCrash) |
175 |
| -// .enumerated() |
176 |
| -// .map { (index, view) in |
177 |
| -// let snapshot = index < snapshots?.count ?? 0 ? snapshots?[index] : nil |
178 |
| -// return AnyViewGraphNode(for: view, backend: backend, snapshot: snapshot) |
179 |
| -// } |
180 |
| - |
181 |
| -// for node in nodes { |
182 |
| -// backend.addChild(node.widget.into(), toOneOfContainer: container.into()) |
183 |
| -// } |
184 |
| -// } |
185 |
| - |
186 |
| -// func update<Backend: AppBackend>(with view: NavigationStack<Child>, backend: Backend) { |
187 |
| -// // content.elements is a computed property so only get it once |
188 |
| -// let contentElements = view.elements |
189 |
| - |
190 |
| -// // Remove queued pages |
191 |
| -// for widget in widgetsQueuedForRemoval { |
192 |
| -// backend.removeChild(widget.into(), fromOneOfContainer: container.into()) |
193 |
| -// } |
194 |
| -// widgetsQueuedForRemoval = [] |
195 |
| - |
196 |
| -// // Update pages |
197 |
| -// for (i, node) in nodes.enumerated() { |
198 |
| - |
199 |
| -// guard i < contentElements.count else { |
200 |
| -// break |
201 |
| -// } |
202 |
| -// let index = contentElements.startIndex.advanced(by: i) |
203 |
| -// node.update(with: view.childOrCrash(for: contentElements[index])) |
204 |
| -// } |
205 |
| - |
206 |
| -// let remaining = contentElements.count - nodes.count |
207 |
| -// if remaining > 0 { |
208 |
| -// // Add new pages |
209 |
| -// for i in nodes.count..<(nodes.count + remaining) { |
210 |
| -// let node = AnyViewGraphNode( |
211 |
| -// for: view.childOrCrash(for: contentElements[i]), |
212 |
| -// backend: backend |
213 |
| -// ) |
214 |
| -// nodes.append(node) |
215 |
| -// backend.addChild(node.widget.into(), toOneOfContainer: container.into()) |
216 |
| -// } |
217 |
| -// // Animate showing the new top page |
218 |
| -// if alwaysShowTopView, let top = nodes.last?.widget { |
219 |
| -// backend.setVisibleChild(ofOneOfContainer: container.into(), to: top.into()) |
220 |
| -// } |
221 |
| -// } else if remaining < 0 { |
222 |
| -// // Animate back to the last page that was not popped |
223 |
| -// if alwaysShowTopView, !contentElements.isEmpty { |
224 |
| -// let top = nodes[contentElements.count - 1] |
225 |
| -// backend.setVisibleChild( |
226 |
| -// ofOneOfContainer: container.into(), to: top.widget.into() |
227 |
| -// ) |
228 |
| -// } |
229 |
| - |
230 |
| -// // Queue popped pages for removal |
231 |
| -// let unused = -remaining |
232 |
| -// for i in (nodes.count - unused)..<nodes.count { |
233 |
| -// widgetsQueuedForRemoval.append(nodes[i].widget) |
234 |
| -// } |
235 |
| -// nodes.removeLast(unused) |
236 |
| -// } |
237 |
| -// } |
238 |
| - |
239 |
| -// private func pageName(for index: Int) -> String { |
240 |
| -// return "NavigationStack page \(index)" |
241 |
| -// } |
242 |
| -// } |
| 3 | +struct NavigationStackRootPath: Codable {} |
| 4 | + |
| 5 | +/// A view that displays a root view and enables you to present additional views over the root view. |
| 6 | +/// |
| 7 | +/// Use .navigationDestination(for:destination:) on this view instead of its children unlike Apples SwiftUI API. |
| 8 | +public struct NavigationStack<Detail: View>: View { |
| 9 | + public var body: some View { |
| 10 | + if let element = elements.last { |
| 11 | + if let content = child(element) { |
| 12 | + content |
| 13 | + } else { |
| 14 | + fatalError( |
| 15 | + "Failed to find detail view for \"\(element)\", make sure you have called .navigationDestination for this type." |
| 16 | + ) |
| 17 | + } |
| 18 | + } else { |
| 19 | + Text("Empty navigation path") |
| 20 | + } |
| 21 | + } |
| 22 | + |
| 23 | + /// A binding to the current navigation path. |
| 24 | + var path: Binding<NavigationPath> |
| 25 | + /// The types handled by each destination (in the same order as their |
| 26 | + /// corresponding views in the stack). |
| 27 | + var destinationTypes: [any Codable.Type] |
| 28 | + /// Gets a recursive ``EitherView`` structure which will have a single view |
| 29 | + /// visible suitable for displaying the given path element (based on its |
| 30 | + /// type). |
| 31 | + /// |
| 32 | + /// It's implemented as a recursive structure because that's the best way to keep this |
| 33 | + /// typesafe without introducing some crazy generated pseudo-variadic storage types of |
| 34 | + /// some sort. This way we can easily have unlimited navigation destinations and there's |
| 35 | + /// just a single simple method for adding a navigation destination. |
| 36 | + var child: (any Codable) -> Detail? |
| 37 | + /// The elements of the navigation path. The result can depend on |
| 38 | + /// ``NavigationStack/destinationTypes`` which determines how the keys are |
| 39 | + /// decoded if they haven't yet been decoded (this happens if they're loaded |
| 40 | + /// from disk for persistence). |
| 41 | + var elements: [any Codable] { |
| 42 | + let resolvedPath = path.wrappedValue.path( |
| 43 | + destinationTypes: destinationTypes |
| 44 | + ) |
| 45 | + return [NavigationStackRootPath()] + resolvedPath |
| 46 | + } |
| 47 | + |
| 48 | + /// Creates a navigation stack with heterogeneous navigation state that you can control. |
| 49 | + /// - Parameters: |
| 50 | + /// - path: A `Binding` to the navigation state for this stack. |
| 51 | + /// - root: The view to display when the stack is empty. |
| 52 | + public init( |
| 53 | + path: Binding<NavigationPath>, |
| 54 | + @ViewBuilder _ root: @escaping () -> Detail |
| 55 | + ) { |
| 56 | + self.path = path |
| 57 | + destinationTypes = [] |
| 58 | + child = { element in |
| 59 | + if element is NavigationStackRootPath { |
| 60 | + return root() |
| 61 | + } else { |
| 62 | + return nil |
| 63 | + } |
| 64 | + } |
| 65 | + } |
| 66 | + |
| 67 | + /// Associates a destination view with a presented data type for use within a navigation stack. |
| 68 | + /// |
| 69 | + /// Add this view modifer to describe the view that the stack displays when presenting a particular |
| 70 | + /// kind of data. Use a `NavigationLink` to present the data. You can add more than one navigation |
| 71 | + /// destination modifier to the stack if it needs to present more than one kind of data. |
| 72 | + /// - Parameters: |
| 73 | + /// - data: The type of data that this destination matches. |
| 74 | + /// - destination: A view builder that defines a view to display when the stack’s navigation |
| 75 | + /// state contains a value of type data. The closure takes one argument, which is the value |
| 76 | + /// of the data to present. |
| 77 | + public func navigationDestination<D: Codable, C: View>( |
| 78 | + for data: D.Type, |
| 79 | + @ViewBuilder destination: @escaping (D) -> C |
| 80 | + ) -> NavigationStack<EitherView<Detail, C>> { |
| 81 | + // Adds another detail view by adding to the recursive structure of either views created |
| 82 | + // to display details in a type-safe manner. See NavigationStack.child for details. |
| 83 | + return NavigationStack<EitherView<Detail, C>>( |
| 84 | + previous: self, |
| 85 | + destination: destination |
| 86 | + ) |
| 87 | + } |
| 88 | + |
| 89 | + /// Add a destination for a specific path element (by adding another layer of ``EitherView``). |
| 90 | + private init<PreviousDetail: View, NewDetail: View, Component: Codable>( |
| 91 | + previous: NavigationStack<PreviousDetail>, |
| 92 | + destination: @escaping (Component) -> NewDetail? |
| 93 | + ) where Detail == EitherView<PreviousDetail, NewDetail> { |
| 94 | + path = previous.path |
| 95 | + destinationTypes = previous.destinationTypes + [Component.self] |
| 96 | + child = { |
| 97 | + if let previous = previous.child($0) { |
| 98 | + // Either root or previously defined destination returned a view |
| 99 | + return EitherView(previous) |
| 100 | + } else if let component = $0 as? Component, let new = destination(component) { |
| 101 | + // This destination returned a detail view for the current element |
| 102 | + return EitherView(new) |
| 103 | + } else { |
| 104 | + // Possibly a future .navigationDestination will handle this path element |
| 105 | + return nil |
| 106 | + } |
| 107 | + } |
| 108 | + } |
| 109 | + |
| 110 | + /// Attempts to compute the detail view for the given element (the type of |
| 111 | + /// the element decides which detail is shown). Crashes if no suitable detail |
| 112 | + /// view is found. |
| 113 | + func childOrCrash(for element: any Codable) -> Detail { |
| 114 | + guard let child = child(element) else { |
| 115 | + fatalError( |
| 116 | + "Failed to find detail view for \"\(element)\", make sure you have called .navigationDestination for this type." |
| 117 | + ) |
| 118 | + } |
| 119 | + |
| 120 | + return child |
| 121 | + } |
| 122 | +} |
0 commit comments