Skip to content

Commit fa5314b

Browse files
committed
Reimplement NavigationStack under new layout system and fix EitherView/OptionalView sizing
1 parent 4f74562 commit fa5314b

File tree

4 files changed

+126
-241
lines changed

4 files changed

+126
-241
lines changed

Sources/SwiftCrossUI/Scenes/WindowGroupNode.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,13 @@ public final class WindowGroupNode<Content: View>: SceneGraphNode {
3131

3232
backend.setResizeHandler(ofWindow: window) { [weak self] newSize in
3333
guard let self else { return .zero }
34-
return self.update(
34+
self.update(
3535
nil,
3636
proposedWindowSize: newSize,
3737
backend: backend,
3838
environment: parentEnvironment
3939
)
40+
return newSize
4041
}
4142
}
4243

Sources/SwiftCrossUI/Views/EitherView.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,8 @@ public struct EitherView<A: View, B: View>: TypeSafeView, View {
116116
children.isFirstUpdate = false
117117
}
118118

119+
backend.setSize(of: widget, to: size)
120+
119121
return size
120122
}
121123
}
Lines changed: 120 additions & 240 deletions
Original file line numberDiff line numberDiff line change
@@ -1,242 +1,122 @@
11
/// Type to indicate the root of the NavigationStack. This is internal to prevent root accidentally showing instead
22
/// 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+
}

Sources/SwiftCrossUI/Views/OptionalView.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ public struct OptionalView<V: View>: TypeSafeView, View {
8080
children.isFirstUpdate = false
8181
}
8282

83+
backend.setSize(of: widget, to: size)
84+
8385
return size
8486
}
8587
}

0 commit comments

Comments
 (0)