Skip to content

Commit 0284e90

Browse files
Merge branch 'feature/navigationstack'
2 parents 863e114 + 48fb20e commit 0284e90

30 files changed

+414
-385
lines changed

Docs/Nesting FlowStacks.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ If the child FlowStack is instantiated without its own data binding, it can shar
1616

1717
- Both parent and child can push new routes onto the path, and the parent's path will include the ones its child has pushed.
1818
- Calling `goBackToRoot` from the child will go all the way back to the parent's root screen.
19+
- The parent is responsible for whether the child should be shown with navigation or not.
1920

2021

2122
## Approach 2: Nested FlowStack holds its own state and takes over navigation duties from its parent FlowStack
@@ -26,3 +27,4 @@ That means:
2627

2728
- Only the child can push new routes onto the path: it assumes responsibility for navigation until it is removed from its parent's path.
2829
- Calling `goBackToRoot` from the child will go back to the child's root screen.
30+
- The child is responsible for whether its root should be shown with navigation or not.

FlowStacksApp.xcodeproj/project.pbxproj

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@
2222
521D35CD2C8721E400C6B369 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 52AB994D2C7A5A7900AC6BA5 /* Assets.xcassets */; };
2323
523FF3102A79998800411CF7 /* FlowStacks in Frameworks */ = {isa = PBXBuildFile; productRef = 52E2C8F52A7268AB0042C495 /* FlowStacks */; };
2424
523FF3112A79998800411CF7 /* FlowStacks in Frameworks */ = {isa = PBXBuildFile; productRef = 52E2C8F52A7268AB0042C495 /* FlowStacks */; };
25+
5256734D2CE413E500221DEA /* ProcessArguments.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5256734C2CE413E500221DEA /* ProcessArguments.swift */; };
26+
5256734E2CE413E500221DEA /* ProcessArguments.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5256734C2CE413E500221DEA /* ProcessArguments.swift */; };
27+
5256734F2CE413E500221DEA /* ProcessArguments.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5256734C2CE413E500221DEA /* ProcessArguments.swift */; };
28+
525673502CE413E500221DEA /* ProcessArguments.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5256734C2CE413E500221DEA /* ProcessArguments.swift */; };
2529
525C73372774BA6B009CBD67 /* NumberCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 525C73362774BA6B009CBD67 /* NumberCoordinator.swift */; };
2630
525C73382774BA6B009CBD67 /* NumberCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 525C73362774BA6B009CBD67 /* NumberCoordinator.swift */; };
2731
526D9F3326AF667000B6B882 /* FlowStacksApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 526D9F3226AF667000B6B882 /* FlowStacksApp.swift */; };
@@ -93,6 +97,7 @@
9397
521D35AB2C871FFD00C6B369 /* FlowStacksApp (watchOS).app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "FlowStacksApp (watchOS).app"; sourceTree = BUILT_PRODUCTS_DIR; };
9498
5241BF3326AA1D3B002D6892 /* FlowStacksApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = FlowStacksApp.app; sourceTree = BUILT_PRODUCTS_DIR; };
9599
5241BF3926AA1D3B002D6892 /* FlowStacksApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = FlowStacksApp.app; sourceTree = BUILT_PRODUCTS_DIR; };
100+
5256734C2CE413E500221DEA /* ProcessArguments.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProcessArguments.swift; sourceTree = "<group>"; };
96101
525C73362774BA6B009CBD67 /* NumberCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NumberCoordinator.swift; sourceTree = "<group>"; };
97102
526D9F3226AF667000B6B882 /* FlowStacksApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlowStacksApp.swift; sourceTree = "<group>"; };
98103
5285BBAC2B6408F500197CE7 /* FlowStacksAppUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = FlowStacksAppUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -207,6 +212,7 @@
207212
526D9F1D26AF661F00B6B882 /* Shared */ = {
208213
isa = PBXGroup;
209214
children = (
215+
5256734C2CE413E500221DEA /* ProcessArguments.swift */,
210216
528AE0EF2BF377C300E143C5 /* ArrayBindingView.swift */,
211217
529CB4432C6ABC7B00B0AFE9 /* View+indexedA11y.swift */,
212218
528AE0F12BF377C300E143C5 /* FlowPathView.swift */,
@@ -466,6 +472,7 @@
466472
521D35C32C87204600C6B369 /* ArrayBindingView.swift in Sources */,
467473
521D35C42C87204600C6B369 /* NumberVMFlow.swift in Sources */,
468474
521D35C52C87204600C6B369 /* SimpleStepper.swift in Sources */,
475+
5256734D2CE413E500221DEA /* ProcessArguments.swift in Sources */,
469476
521D35C62C87204600C6B369 /* NumberCoordinator.swift in Sources */,
470477
521D35C72C87204600C6B369 /* FlowStacksApp.swift in Sources */,
471478
521D35C82C87204600C6B369 /* Deeplink.swift in Sources */,
@@ -482,6 +489,7 @@
482489
528AE0F32BF377C400E143C5 /* ArrayBindingView.swift in Sources */,
483490
529CB4442C6ABC7B00B0AFE9 /* View+indexedA11y.swift in Sources */,
484491
528AE0F92BF377C400E143C5 /* NumberVMFlow.swift in Sources */,
492+
5256734F2CE413E500221DEA /* ProcessArguments.swift in Sources */,
485493
52AB99532C83C90300AC6BA5 /* SimpleStepper.swift in Sources */,
486494
528AE0F52BF377C400E143C5 /* NoBindingView.swift in Sources */,
487495
525C73372774BA6B009CBD67 /* NumberCoordinator.swift in Sources */,
@@ -498,6 +506,7 @@
498506
528AE0F42BF377C400E143C5 /* ArrayBindingView.swift in Sources */,
499507
529CB4452C6ABC7B00B0AFE9 /* View+indexedA11y.swift in Sources */,
500508
528AE0FA2BF377C400E143C5 /* NumberVMFlow.swift in Sources */,
509+
5256734E2CE413E500221DEA /* ProcessArguments.swift in Sources */,
501510
52AB99542C83C90300AC6BA5 /* SimpleStepper.swift in Sources */,
502511
528AE0F62BF377C400E143C5 /* NoBindingView.swift in Sources */,
503512
525C73382774BA6B009CBD67 /* NumberCoordinator.swift in Sources */,
@@ -524,6 +533,7 @@
524533
529CB45B2C6AD09500B0AFE9 /* NoBindingView.swift in Sources */,
525534
529CB45C2C6AD09500B0AFE9 /* FlowStacksApp.swift in Sources */,
526535
529CB45D2C6AD09500B0AFE9 /* View+indexedA11y.swift in Sources */,
536+
525673502CE413E500221DEA /* ProcessArguments.swift in Sources */,
527537
52AB99522C83C90300AC6BA5 /* SimpleStepper.swift in Sources */,
528538
529CB45E2C6AD09500B0AFE9 /* NumberCoordinator.swift in Sources */,
529539
529CB45F2C6AD09500B0AFE9 /* Deeplink.swift in Sources */,

FlowStacksApp/Shared/ArrayBindingView.swift

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ private enum Screen: Hashable {
55
case number(Int)
66
case numberList(NumberList)
77
case visualisation(EmojiVisualisation)
8+
case child(ChildFlowStack.ChildType)
89
}
910

1011
struct ArrayBindingView: View {
@@ -29,6 +30,8 @@ struct ArrayBindingView: View {
2930
NumberView(number: number)
3031
case let .visualisation(visualisation):
3132
EmojiView(visualisation: visualisation)
33+
case let .child(child):
34+
ChildFlowStack(childType: child)
3235
}
3336
})
3437
}
@@ -76,7 +79,7 @@ private struct NumberListView: View {
7679
var body: some View {
7780
List {
7881
ForEach(numberList.range, id: \.self) { number in
79-
FlowLink("\(number)", value: Screen.number(number), style: .sheet(withNavigation: true))
82+
FlowLink("\(number)", value: Screen.number(number), style: .push)
8083
.indexedA11y("Show \(number)")
8184
}
8285
Button("Go back", action: { navigator.goBack() })
@@ -102,7 +105,13 @@ private struct NumberView: View {
102105
style: .sheet,
103106
label: { Text("Visualise with sheep") }
104107
)
108+
// NOTE: When presenting a child that handles its own state, the child determines whether its root is shown with navigation.
109+
FlowLink(value: Screen.child(.flowPath), style: .sheet(withNavigation: false), label: { Text("FlowPath Child") })
110+
.indexedA11y("FlowPath Child")
111+
FlowLink(value: Screen.child(.noBinding), style: .sheet(withNavigation: false), label: { Text("NoBinding Child") })
112+
.indexedA11y("NoBinding Child")
105113
Button("Go back to root", action: { navigator.goBackToRoot() })
114+
.indexedA11y("Go back to root")
106115
}.navigationTitle("\(number)")
107116
}
108117
}

FlowStacksApp/Shared/FlowPathView.swift

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,9 +117,14 @@ private struct NumberView: View {
117117
style: .sheet,
118118
label: { Text("Visualise with sheep") }
119119
)
120-
FlowLink(value: ChildFlowStack.ChildType.flowPath, style: .push, label: { Text("FlowPath Child") })
121-
FlowLink(value: ChildFlowStack.ChildType.noBinding, style: .push, label: { Text("NoBinding Child") })
120+
// NOTE: When presenting a child that handles its own state, the child determines whether its root is shown with navigation.
121+
FlowLink(value: ChildFlowStack.ChildType.flowPath, style: .sheet(withNavigation: false), label: { Text("FlowPath Child") })
122+
.indexedA11y("FlowPath Child")
123+
// NOTE: When presenting a child that defers to the parent state, the parent determines whether it is shown with navigation.
124+
FlowLink(value: ChildFlowStack.ChildType.noBinding, style: .sheet(withNavigation: true), label: { Text("NoBinding Child") })
125+
.indexedA11y("NoBinding Child")
122126
Button("Go back to root", action: { navigator.goBackToRoot() })
127+
.indexedA11y("Go back to root")
123128
}.navigationTitle("\(number)")
124129
}
125130
}

FlowStacksApp/Shared/FlowStacksApp.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,12 @@ struct FlowStacksApp: App {
3030
NumberVMFlow(viewModel: .init(initialNumber: 64))
3131
.tabItem { Text("ViewModel") }
3232
.tag(Tab.viewModel)
33-
}.onOpenURL { url in
33+
}
34+
.onOpenURL { url in
3435
guard let deeplink = Deeplink(url: url) else { return }
3536
follow(deeplink)
3637
}
38+
.useNavigationStack(ProcessArguments.navigationStackPolicy)
3739
}
3840
}
3941

FlowStacksApp/Shared/NoBindingView.swift

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,11 +90,16 @@ private struct NumberView: View {
9090
style: .sheet,
9191
label: { Text("Visualise with sheep") }
9292
)
93-
FlowLink(value: ChildFlowStack.ChildType.flowPath, style: .push, label: { Text("FlowPath Child") })
94-
FlowLink(value: ChildFlowStack.ChildType.noBinding, style: .push, label: { Text("NoBinding Child") })
93+
// NOTE: When presenting a child that handles its own state, the child determines whether its root is shown with navigation.
94+
FlowLink(value: ChildFlowStack.ChildType.flowPath, style: .sheet(withNavigation: false), label: { Text("FlowPath Child") })
95+
.indexedA11y("FlowPath Child")
96+
// NOTE: When presenting a child that defers to the parent state, the parent determines whether it is shown with navigation.
97+
FlowLink(value: ChildFlowStack.ChildType.noBinding, style: .sheet(withNavigation: true), label: { Text("NoBinding Child") })
98+
.indexedA11y("NoBinding Child")
9599
Button("Go back to root") {
96100
navigator.goBackToRoot()
97101
}
102+
.indexedA11y("Go back to root")
98103
}.navigationTitle("\(number)")
99104
}
100105
}

FlowStacksApp/Shared/NumberCoordinator.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ private struct NumberView: View {
8787
}
8888
}
8989
.padding()
90+
.background(Color.white)
9091
.flowDestination(item: $colorShown, style: .sheet(withNavigation: true)) { color in
9192
Text(String(describing: color)).foregroundColor(color)
9293
.navigationTitle("Color")
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import Foundation
2+
import FlowStacks
3+
4+
enum ProcessArguments {
5+
static var navigationStackPolicy: UseNavigationStackPolicy {
6+
// Allows the policy to be set from UI tests.
7+
ProcessInfo.processInfo.arguments.contains("USE_NAVIGATIONSTACK") ? .whenAvailable : .never
8+
}
9+
}

FlowStacksAppUITests/FlowStacksUITests.swift

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,43 @@ final class FlowStacksUITests: XCTestCase {
55
continueAfterFailure = false
66
}
77

8-
func testNavigationViaPathWithFlowStack() {
8+
func testNavigationViaPathWithNavigationView() {
99
launchAndRunNavigationTests(tabTitle: "FlowPath", useNavigationStack: false, app: XCUIApplication())
1010
}
1111

12-
func testNavigationViaArrayWithFlowStack() {
12+
func testNavigationViaArrayWithNavigationView() {
1313
launchAndRunNavigationTests(tabTitle: "ArrayBinding", useNavigationStack: false, app: XCUIApplication())
1414
}
1515

16-
func testNavigationViaNoneWithFlowStack() {
16+
func testNavigationViaNoneWithNavigationView() {
1717
launchAndRunNavigationTests(tabTitle: "NoBinding", useNavigationStack: false, app: XCUIApplication())
1818
}
1919

20+
func testNavigationViaPathWithNavigationStack() {
21+
launchAndRunNavigationTests(tabTitle: "FlowPath", useNavigationStack: true, app: XCUIApplication())
22+
}
23+
24+
func testNavigationViaArrayWithNavigationStack() {
25+
launchAndRunNavigationTests(tabTitle: "ArrayBinding", useNavigationStack: true, app: XCUIApplication())
26+
}
27+
28+
func testNavigationViaNoneWithNavigationStack() {
29+
launchAndRunNavigationTests(tabTitle: "NoBinding", useNavigationStack: true, app: XCUIApplication())
30+
}
31+
2032
func launchAndRunNavigationTests(tabTitle: String, useNavigationStack: Bool, app: XCUIApplication) {
2133
if useNavigationStack {
22-
// This currently has no effect, but may do so in future.
23-
app.launchArguments = ["USE_NAVIGATIONSTACK"]
34+
if #available(iOS 16.0, *, macOS 13.0, *, watchOS 9.0, *, tvOS 16.0, *) {
35+
app.launchArguments = ["USE_NAVIGATIONSTACK"]
36+
} else {
37+
// Navigation Stack unavailable, so test can be skipped
38+
return
39+
}
40+
} else if #available(iOS 26.0, *, macOS 26.0, *, watchOS 26.0, *, tvOS 26.0, *) {
41+
// NavigationView has issues on v26.0, so it is not supported.
42+
return
2443
}
44+
2545
app.launch()
2646

2747
let navigationTimeout = 0.8

FlowStacksAppUITests/NestedFlowStacksUITests.swift

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,41 @@ final class NestedFlowStacksUITests: XCTestCase {
55
continueAfterFailure = false
66
}
77

8-
func testNestedNavigationViaPathWithFlowStack() {
8+
func testNestedNavigationViaPathWithNavigationView() {
99
launchAndRunNestedNavigationTests(tabTitle: "FlowPath", useNavigationStack: false, app: XCUIApplication())
1010
}
1111

12-
func testNestedNavigationViaNoneWithFlowStack() {
12+
func testNestedNavigationViaNoneWithNavigationView() {
1313
launchAndRunNestedNavigationTests(tabTitle: "NoBinding", useNavigationStack: false, app: XCUIApplication())
1414
}
1515

16+
func testNestedNavigationViaArrayWithNavigationView() {
17+
launchAndRunNestedNavigationTests(tabTitle: "ArrayBinding", useNavigationStack: false, app: XCUIApplication())
18+
}
19+
20+
func testNestedNavigationViaPathWithNavigationStack() {
21+
launchAndRunNestedNavigationTests(tabTitle: "FlowPath", useNavigationStack: true, app: XCUIApplication())
22+
}
23+
24+
func testNestedNavigationViaNoneWithNavigationStack() {
25+
launchAndRunNestedNavigationTests(tabTitle: "NoBinding", useNavigationStack: true, app: XCUIApplication())
26+
}
27+
28+
func testNestedNavigationViaArrayWithNavigationStack() {
29+
launchAndRunNestedNavigationTests(tabTitle: "ArrayBinding", useNavigationStack: true, app: XCUIApplication())
30+
}
31+
1632
func launchAndRunNestedNavigationTests(tabTitle: String, useNavigationStack: Bool, app: XCUIApplication) {
1733
if useNavigationStack {
18-
// This currently has no effect, but may do so in future.
19-
app.launchArguments = ["USE_NAVIGATIONSTACK"]
34+
if #available(iOS 16.0, *, macOS 13.0, *, watchOS 9.0, *, tvOS 16.0, *) {
35+
app.launchArguments = ["USE_NAVIGATIONSTACK"]
36+
} else {
37+
// Navigation Stack unavailable, so test can be skipped
38+
return
39+
}
40+
} else if #available(iOS 26.0, *, macOS 26.0, *, watchOS 26.0, *, tvOS 26.0, *) {
41+
// NavigationView has issues on v26.0, so it is not supported.
42+
return
2043
}
2144
app.launch()
2245

@@ -32,7 +55,7 @@ final class NestedFlowStacksUITests: XCTestCase {
3255
app.buttons["Show 1 - route 1:0"].tap()
3356
XCTAssertTrue(app.navigationBars["1"].waitForExistence(timeout: navigationTimeout))
3457

35-
app.buttons["FlowPath Child"].tap()
58+
app.buttons["FlowPath Child - route 1:1"].tap()
3659
XCTAssertTrue(app.navigationBars["Home"].waitForExistence(timeout: navigationTimeout))
3760

3861
app.buttons["Pick a number - route 2:-1"].firstMatch.tap()
@@ -41,7 +64,7 @@ final class NestedFlowStacksUITests: XCTestCase {
4164
app.buttons["Show 1 - route 2:0"].tap()
4265
XCTAssertTrue(app.navigationBars["1"].waitForExistence(timeout: navigationTimeout))
4366

44-
app.buttons["NoBinding Child"].tap()
67+
app.buttons["NoBinding Child - route 2:1"].firstMatch.tap()
4568
XCTAssertTrue(app.navigationBars["Home"].waitForExistence(timeout: navigationTimeout))
4669

4770
app.buttons["Pick a number - route 2:2"].firstMatch.tap()
@@ -50,7 +73,7 @@ final class NestedFlowStacksUITests: XCTestCase {
5073
app.buttons["Show 1 - route 2:3"].tap()
5174
XCTAssertTrue(app.navigationBars["1"].waitForExistence(timeout: navigationTimeout))
5275

53-
app.buttons["Go back to root"].tap()
76+
app.buttons["Go back to root - route 2:4"].tap()
5477
XCTAssertTrue(app.navigationBars["Home"].waitForExistence(timeout: navigationTimeout))
5578

5679
// Goes back to root of FlowPath child.

0 commit comments

Comments
 (0)