├── .gitignore
├── .swift-format.json
├── .swiftpm
└── xcode
│ └── package.xcworkspace
│ └── xcshareddata
│ └── IDEWorkspaceChecks.plist
├── LICENSE
├── NavigationBackport.podspec
├── NavigationBackportApp
├── NavigationBackportApp.xcodeproj
│ ├── project.pbxproj
│ ├── project.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ │ └── IDEWorkspaceChecks.plist
│ └── xcshareddata
│ │ └── xcschemes
│ │ └── NavigationBackportApp (iOS).xcscheme
├── NavigationBackportUITests
│ ├── InitialisationUITests.swift
│ ├── LeakUITests.swift
│ ├── LocalDestinationUITests.swift
│ └── NavigationUITests.swift
├── NavigationBackportWatchApp Watch App
│ ├── Assets.xcassets
│ │ ├── AccentColor.colorset
│ │ │ └── Contents.json
│ │ ├── AppIcon.appiconset
│ │ │ └── Contents.json
│ │ └── Contents.json
│ └── Preview Content
│ │ └── Preview Assets.xcassets
│ │ └── Contents.json
├── Package.swift
├── Shared
│ ├── ArrayBindingView.swift
│ ├── Assets.xcassets
│ │ ├── AccentColor.colorset
│ │ │ └── Contents.json
│ │ ├── AppIcon.appiconset
│ │ │ └── Contents.json
│ │ └── Contents.json
│ ├── ContentView.swift
│ ├── LeakTest
│ │ ├── LeakTestView.swift
│ │ ├── MainView.swift
│ │ └── Presentable.swift
│ ├── NBNavigationPathView.swift
│ ├── NavigationBackportApp.swift
│ ├── NoBindingView.swift
│ ├── ProcessArguments.swift
│ └── TrafficLight.swift
├── macOS
│ └── macOS.entitlements
└── run_ui_tests.sh
├── Package.swift
├── README.md
├── Sources
└── NavigationBackport
│ ├── Array+utilities.swift
│ ├── Binding+withDelaysIfUnsupported.swift
│ ├── CodableRepresentation.swift
│ ├── ConditionalViewBuilder.swift
│ ├── DestinationBuilderHolder.swift
│ ├── DestinationBuilderModifier.swift
│ ├── DestinationBuilderView.swift
│ ├── EnvironmentValues+navigationStack.swift
│ ├── LocalDestinationBuilderModifier.swift
│ ├── NBNavigationLink.swift
│ ├── NBNavigationPath+utilities.swift
│ ├── NBNavigationPath.swift
│ ├── NBNavigationStack.swift
│ ├── NBScreen.swift
│ ├── NavigationBackport.swift
│ ├── NavigationLinkRowStyle.swift
│ ├── NavigationPathHolder.swift
│ ├── Navigator+utilities.swift
│ ├── Navigator+withDelaysIfUnsupported.swift
│ ├── Navigator.swift
│ ├── Node.swift
│ ├── NonReactiveState.swift
│ ├── ObservableObject+withDelaysIfUnsupported.swift
│ ├── Router.swift
│ ├── Unobserved.swift
│ ├── UseNavigationStackPolicy.swift
│ ├── View+_navigationDestination.swift
│ ├── View+nbNavigationDestination.swift
│ ├── View+nbUseNavigationStack.swift
│ ├── View+onFirstAppear.swift
│ └── apply.swift
└── Tests
└── NavigationBackportTests
└── NavigationBackportTests.swift
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | /*.xcodeproj
5 | xcuserdata/
6 | DerivedData/
7 | .swiftpm/config/registries.json
8 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
9 | .netrc
10 |
--------------------------------------------------------------------------------
/.swift-format.json:
--------------------------------------------------------------------------------
1 | {
2 | "fileScopedDeclarationPrivacy" : {
3 | "accessLevel" : "private"
4 | },
5 | "indentation" : {
6 | "spaces" : 2
7 | },
8 | "indentConditionalCompilationBlocks" : true,
9 | "indentSwitchCaseLabels" : false,
10 | "lineBreakAroundMultilineExpressionChainComponents" : false,
11 | "lineBreakBeforeControlFlowKeywords" : false,
12 | "lineBreakBeforeEachArgument" : false,
13 | "lineBreakBeforeEachGenericRequirement" : false,
14 | "lineLength" : 100,
15 | "maximumBlankLines" : 1,
16 | "prioritizeKeepingFunctionOutputTogether" : false,
17 | "respectsExistingLineBreaks" : true,
18 | "rules" : {
19 | "AllPublicDeclarationsHaveDocumentation" : false,
20 | "AlwaysUseLowerCamelCase" : true,
21 | "AmbiguousTrailingClosureOverload" : true,
22 | "BeginDocumentationCommentWithOneLineSummary" : false,
23 | "DoNotUseSemicolons" : true,
24 | "DontRepeatTypeInStaticProperties" : true,
25 | "FileScopedDeclarationPrivacy" : true,
26 | "FullyIndirectEnum" : true,
27 | "GroupNumericLiterals" : true,
28 | "IdentifiersMustBeASCII" : true,
29 | "NeverForceUnwrap" : false,
30 | "NeverUseForceTry" : false,
31 | "NeverUseImplicitlyUnwrappedOptionals" : false,
32 | "NoAccessLevelOnExtensionDeclaration" : false,
33 | "NoBlockComments" : true,
34 | "NoCasesWithOnlyFallthrough" : true,
35 | "NoEmptyTrailingClosureParentheses" : true,
36 | "NoLabelsInCasePatterns" : true,
37 | "NoLeadingUnderscores" : false,
38 | "NoParensAroundConditions" : true,
39 | "NoVoidReturnOnFunctionSignature" : true,
40 | "OneCasePerLine" : true,
41 | "OneVariableDeclarationPerLine" : true,
42 | "OnlyOneTrailingClosureArgument" : true,
43 | "OrderedImports" : false,
44 | "ReturnVoidInsteadOfEmptyTuple" : true,
45 | "UseLetInEveryBoundCaseVariable" : true,
46 | "UseShorthandTypeNames" : true,
47 | "UseSingleLinePropertyGetter" : true,
48 | "UseSynthesizedInitializer" : true,
49 | "UseTripleSlashForDocumentationComments" : true,
50 | "ValidateDocumentationComments" : false
51 | },
52 | "tabWidth" : 8,
53 | "version" : 1
54 | }
55 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2022 johnpatrickmorgan
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in
11 | all copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | THE SOFTWARE.
20 |
--------------------------------------------------------------------------------
/NavigationBackport.podspec:
--------------------------------------------------------------------------------
1 | Pod::Spec.new do |spec|
2 | spec.name = "NavigationBackport"
3 | spec.version = "0.11.2"
4 | spec.summary = "Backports NavigationStack for older SwiftUI versions."
5 | spec.description = <<-DESC
6 | This package uses the navigation APIs available in older SwiftUI versions
7 | (such as `NavigationView` and `NavigationLink`) to recreate the new `NavigationStack` APIs
8 | introduced in WWDC22, so that you can start targeting those APIs on older versions of iOS, tvOS,
9 | macOS and watchOS. When running on an OS version that supports `NavigationStack`, `NavigationStack`
10 | will be used under the hood.
11 | DESC
12 | spec.homepage = "https://github.com/johnpatrickmorgan/NavigationBackport"
13 | spec.license = { :type => "MIT", :file => "LICENSE" }
14 | spec.author = { "John Patrick Morgan" => "johnpatrickmorganuk@gmail.com" }
15 | spec.ios.deployment_target = "14.0"
16 | spec.osx.deployment_target = "11.0"
17 | spec.watchos.deployment_target = "7.0"
18 | spec.tvos.deployment_target = "14.0"
19 | spec.source = { :git => "https://github.com/johnpatrickmorgan/NavigationBackport.git", :tag => "#{spec.version}" }
20 | spec.source_files = "Sources/NavigationBackport/*.swift"
21 | spec.framework = "SwiftUI", "Foundation"
22 | spec.swift_version = "5.6"
23 | end
24 |
--------------------------------------------------------------------------------
/NavigationBackportApp/NavigationBackportApp.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 55;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 520A1E592BCF294000BD60A8 /* InitialisationUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 520A1E572BCF294000BD60A8 /* InitialisationUITests.swift */; };
11 | 520A1E5A2BCF294000BD60A8 /* LocalDestinationUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 520A1E582BCF294000BD60A8 /* LocalDestinationUITests.swift */; };
12 | 520A1E5D2BCF347900BD60A8 /* TrafficLight.swift in Sources */ = {isa = PBXBuildFile; fileRef = 520A1E5B2BCF347200BD60A8 /* TrafficLight.swift */; };
13 | 520A1E5E2BCF347A00BD60A8 /* TrafficLight.swift in Sources */ = {isa = PBXBuildFile; fileRef = 520A1E5B2BCF347200BD60A8 /* TrafficLight.swift */; };
14 | 520A1E5F2BCF347A00BD60A8 /* TrafficLight.swift in Sources */ = {isa = PBXBuildFile; fileRef = 520A1E5B2BCF347200BD60A8 /* TrafficLight.swift */; };
15 | 5252ED502C510686004CB25A /* NavigationBackport in Frameworks */ = {isa = PBXBuildFile; productRef = 5252ED4F2C510686004CB25A /* NavigationBackport */; };
16 | 52925EBF28549A62001B9190 /* NavigationBackportApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52925EAF28549A60001B9190 /* NavigationBackportApp.swift */; };
17 | 52925EC028549A62001B9190 /* NavigationBackportApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52925EAF28549A60001B9190 /* NavigationBackportApp.swift */; };
18 | 52925EC128549A62001B9190 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52925EB028549A60001B9190 /* ContentView.swift */; };
19 | 52925EC228549A62001B9190 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52925EB028549A60001B9190 /* ContentView.swift */; };
20 | 52925EC328549A62001B9190 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 52925EB128549A62001B9190 /* Assets.xcassets */; };
21 | 52925EC428549A62001B9190 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 52925EB128549A62001B9190 /* Assets.xcassets */; };
22 | 529673CE286BB44400C01BCF /* NBNavigationPathView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 529673CD286BB44400C01BCF /* NBNavigationPathView.swift */; };
23 | 529673CF286BB44400C01BCF /* NBNavigationPathView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 529673CD286BB44400C01BCF /* NBNavigationPathView.swift */; };
24 | 529673D1286BB50200C01BCF /* ArrayBindingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 529673D0286BB50200C01BCF /* ArrayBindingView.swift */; };
25 | 529673D2286BB50200C01BCF /* ArrayBindingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 529673D0286BB50200C01BCF /* ArrayBindingView.swift */; };
26 | 529673D4286BC48600C01BCF /* NoBindingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 529673D3286BC48600C01BCF /* NoBindingView.swift */; };
27 | 529673D5286BC48600C01BCF /* NoBindingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 529673D3286BC48600C01BCF /* NoBindingView.swift */; };
28 | 529B5DBF2A40F33E006C6779 /* NavigationBackportWatchApp Watch App.app in Embed Watch Content */ = {isa = PBXBuildFile; fileRef = 529B5DBE2A40F33E006C6779 /* NavigationBackportWatchApp Watch App.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
29 | 529B5DC82A40F340006C6779 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 529B5DC72A40F340006C6779 /* Assets.xcassets */; };
30 | 529B5DCB2A40F340006C6779 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 529B5DCA2A40F340006C6779 /* Preview Assets.xcassets */; };
31 | 529B5DD32A40F37E006C6779 /* NBNavigationPathView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 529673CD286BB44400C01BCF /* NBNavigationPathView.swift */; };
32 | 529B5DD42A40F37E006C6779 /* NoBindingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 529673D3286BC48600C01BCF /* NoBindingView.swift */; };
33 | 529B5DD52A40F37E006C6779 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52925EB028549A60001B9190 /* ContentView.swift */; };
34 | 529B5DD62A40F37E006C6779 /* NavigationBackportApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52925EAF28549A60001B9190 /* NavigationBackportApp.swift */; };
35 | 529B5DD72A40F37E006C6779 /* ArrayBindingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 529673D0286BB50200C01BCF /* ArrayBindingView.swift */; };
36 | 529B5DD92A40F3BD006C6779 /* NavigationBackport in Frameworks */ = {isa = PBXBuildFile; productRef = 529B5DD82A40F3BD006C6779 /* NavigationBackport */; };
37 | 529DD2812CB7D7E6002AA978 /* LeakUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 529DD2802CB7D7E6002AA978 /* LeakUITests.swift */; };
38 | 52B3F1622A01BE4200EC5EA9 /* NavigationUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52B3F1612A01BE4200EC5EA9 /* NavigationUITests.swift */; };
39 | 52D540FE2CA602320077D871 /* LeakTestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52D540F52CA602320077D871 /* LeakTestView.swift */; };
40 | 52D540FF2CA602320077D871 /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52D540EC2CA602320077D871 /* MainView.swift */; };
41 | 52D541012CA602320077D871 /* Presentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52D540F72CA602320077D871 /* Presentable.swift */; };
42 | 52D541072CA602320077D871 /* LeakTestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52D540F52CA602320077D871 /* LeakTestView.swift */; };
43 | 52D541082CA602320077D871 /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52D540EC2CA602320077D871 /* MainView.swift */; };
44 | 52D5410A2CA602320077D871 /* Presentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52D540F72CA602320077D871 /* Presentable.swift */; };
45 | 52D541102CA602320077D871 /* LeakTestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52D540F52CA602320077D871 /* LeakTestView.swift */; };
46 | 52D541112CA602320077D871 /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52D540EC2CA602320077D871 /* MainView.swift */; };
47 | 52D541132CA602320077D871 /* Presentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52D540F72CA602320077D871 /* Presentable.swift */; };
48 | 52DD85F82895C984004B5344 /* NavigationBackport in Frameworks */ = {isa = PBXBuildFile; productRef = 52DD85F72895C984004B5344 /* NavigationBackport */; };
49 | 52E0D8A22A45002D008963E7 /* ProcessArguments.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52E0D8A12A45002D008963E7 /* ProcessArguments.swift */; };
50 | 52E0D8A32A45002D008963E7 /* ProcessArguments.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52E0D8A12A45002D008963E7 /* ProcessArguments.swift */; };
51 | 52E0D8A42A45002D008963E7 /* ProcessArguments.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52E0D8A12A45002D008963E7 /* ProcessArguments.swift */; };
52 | /* End PBXBuildFile section */
53 |
54 | /* Begin PBXContainerItemProxy section */
55 | 529B5DC02A40F33E006C6779 /* PBXContainerItemProxy */ = {
56 | isa = PBXContainerItemProxy;
57 | containerPortal = 52925EAA28549A60001B9190 /* Project object */;
58 | proxyType = 1;
59 | remoteGlobalIDString = 529B5DBD2A40F33E006C6779;
60 | remoteInfo = "NavigationBackportWatchApp Watch App";
61 | };
62 | 52B3F1652A01BE4200EC5EA9 /* PBXContainerItemProxy */ = {
63 | isa = PBXContainerItemProxy;
64 | containerPortal = 52925EAA28549A60001B9190 /* Project object */;
65 | proxyType = 1;
66 | remoteGlobalIDString = 52925EB528549A62001B9190;
67 | remoteInfo = "NavigationBackportApp (iOS)";
68 | };
69 | /* End PBXContainerItemProxy section */
70 |
71 | /* Begin PBXCopyFilesBuildPhase section */
72 | 529B5DCE2A40F340006C6779 /* Embed Watch Content */ = {
73 | isa = PBXCopyFilesBuildPhase;
74 | buildActionMask = 2147483647;
75 | dstPath = "$(CONTENTS_FOLDER_PATH)/Watch";
76 | dstSubfolderSpec = 16;
77 | files = (
78 | 529B5DBF2A40F33E006C6779 /* NavigationBackportWatchApp Watch App.app in Embed Watch Content */,
79 | );
80 | name = "Embed Watch Content";
81 | runOnlyForDeploymentPostprocessing = 0;
82 | };
83 | /* End PBXCopyFilesBuildPhase section */
84 |
85 | /* Begin PBXFileReference section */
86 | 520A1E572BCF294000BD60A8 /* InitialisationUITests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InitialisationUITests.swift; sourceTree = ""; };
87 | 520A1E582BCF294000BD60A8 /* LocalDestinationUITests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocalDestinationUITests.swift; sourceTree = ""; };
88 | 520A1E5B2BCF347200BD60A8 /* TrafficLight.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrafficLight.swift; sourceTree = ""; };
89 | 52925EAF28549A60001B9190 /* NavigationBackportApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationBackportApp.swift; sourceTree = ""; };
90 | 52925EB028549A60001B9190 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; };
91 | 52925EB128549A62001B9190 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
92 | 52925EB628549A62001B9190 /* NavigationBackportApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = NavigationBackportApp.app; sourceTree = BUILT_PRODUCTS_DIR; };
93 | 52925EBC28549A62001B9190 /* NavigationBackportApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = NavigationBackportApp.app; sourceTree = BUILT_PRODUCTS_DIR; };
94 | 52925EBE28549A62001B9190 /* macOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = macOS.entitlements; sourceTree = ""; };
95 | 52925ED128549C15001B9190 /* NavigationBackport */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = NavigationBackport; path = ..; sourceTree = ""; };
96 | 529673CD286BB44400C01BCF /* NBNavigationPathView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NBNavigationPathView.swift; sourceTree = ""; };
97 | 529673D0286BB50200C01BCF /* ArrayBindingView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ArrayBindingView.swift; sourceTree = ""; };
98 | 529673D3286BC48600C01BCF /* NoBindingView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NoBindingView.swift; sourceTree = ""; };
99 | 529B5DB92A40F33E006C6779 /* NavigationBackportWatchApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = NavigationBackportWatchApp.app; sourceTree = BUILT_PRODUCTS_DIR; };
100 | 529B5DBE2A40F33E006C6779 /* NavigationBackportWatchApp Watch App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "NavigationBackportWatchApp Watch App.app"; sourceTree = BUILT_PRODUCTS_DIR; };
101 | 529B5DC72A40F340006C6779 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
102 | 529B5DCA2A40F340006C6779 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; };
103 | 529DD2802CB7D7E6002AA978 /* LeakUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LeakUITests.swift; sourceTree = ""; };
104 | 52B3F15F2A01BE4200EC5EA9 /* NavigationBackportUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = NavigationBackportUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
105 | 52B3F1612A01BE4200EC5EA9 /* NavigationUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationUITests.swift; sourceTree = ""; };
106 | 52D540EC2CA602320077D871 /* MainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainView.swift; sourceTree = ""; };
107 | 52D540F52CA602320077D871 /* LeakTestView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LeakTestView.swift; sourceTree = ""; };
108 | 52D540F72CA602320077D871 /* Presentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Presentable.swift; sourceTree = ""; };
109 | 52E0D8A12A45002D008963E7 /* ProcessArguments.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProcessArguments.swift; sourceTree = ""; };
110 | /* End PBXFileReference section */
111 |
112 | /* Begin PBXFrameworksBuildPhase section */
113 | 52925EB328549A62001B9190 /* Frameworks */ = {
114 | isa = PBXFrameworksBuildPhase;
115 | buildActionMask = 2147483647;
116 | files = (
117 | 5252ED502C510686004CB25A /* NavigationBackport in Frameworks */,
118 | );
119 | runOnlyForDeploymentPostprocessing = 0;
120 | };
121 | 52925EB928549A62001B9190 /* Frameworks */ = {
122 | isa = PBXFrameworksBuildPhase;
123 | buildActionMask = 2147483647;
124 | files = (
125 | 52DD85F82895C984004B5344 /* NavigationBackport in Frameworks */,
126 | );
127 | runOnlyForDeploymentPostprocessing = 0;
128 | };
129 | 529B5DBB2A40F33E006C6779 /* Frameworks */ = {
130 | isa = PBXFrameworksBuildPhase;
131 | buildActionMask = 2147483647;
132 | files = (
133 | 529B5DD92A40F3BD006C6779 /* NavigationBackport in Frameworks */,
134 | );
135 | runOnlyForDeploymentPostprocessing = 0;
136 | };
137 | 52B3F15C2A01BE4200EC5EA9 /* Frameworks */ = {
138 | isa = PBXFrameworksBuildPhase;
139 | buildActionMask = 2147483647;
140 | files = (
141 | );
142 | runOnlyForDeploymentPostprocessing = 0;
143 | };
144 | /* End PBXFrameworksBuildPhase section */
145 |
146 | /* Begin PBXGroup section */
147 | 52925EA928549A60001B9190 = {
148 | isa = PBXGroup;
149 | children = (
150 | 52925ED028549C15001B9190 /* Packages */,
151 | 52925EAE28549A60001B9190 /* Shared */,
152 | 52925EBD28549A62001B9190 /* macOS */,
153 | 52B3F1602A01BE4200EC5EA9 /* NavigationBackportUITests */,
154 | 529B5DC22A40F33E006C6779 /* NavigationBackportWatchApp Watch App */,
155 | 52925EB728549A62001B9190 /* Products */,
156 | 52DD85F62895C984004B5344 /* Frameworks */,
157 | );
158 | sourceTree = "";
159 | };
160 | 52925EAE28549A60001B9190 /* Shared */ = {
161 | isa = PBXGroup;
162 | children = (
163 | 52D540F82CA602320077D871 /* LeakTest */,
164 | 52925EAF28549A60001B9190 /* NavigationBackportApp.swift */,
165 | 520A1E5B2BCF347200BD60A8 /* TrafficLight.swift */,
166 | 52E0D8A12A45002D008963E7 /* ProcessArguments.swift */,
167 | 52925EB028549A60001B9190 /* ContentView.swift */,
168 | 529673D0286BB50200C01BCF /* ArrayBindingView.swift */,
169 | 529673D3286BC48600C01BCF /* NoBindingView.swift */,
170 | 529673CD286BB44400C01BCF /* NBNavigationPathView.swift */,
171 | 52925EB128549A62001B9190 /* Assets.xcassets */,
172 | );
173 | path = Shared;
174 | sourceTree = "";
175 | };
176 | 52925EB728549A62001B9190 /* Products */ = {
177 | isa = PBXGroup;
178 | children = (
179 | 52925EB628549A62001B9190 /* NavigationBackportApp.app */,
180 | 52925EBC28549A62001B9190 /* NavigationBackportApp.app */,
181 | 52B3F15F2A01BE4200EC5EA9 /* NavigationBackportUITests.xctest */,
182 | 529B5DB92A40F33E006C6779 /* NavigationBackportWatchApp.app */,
183 | 529B5DBE2A40F33E006C6779 /* NavigationBackportWatchApp Watch App.app */,
184 | );
185 | name = Products;
186 | sourceTree = "";
187 | };
188 | 52925EBD28549A62001B9190 /* macOS */ = {
189 | isa = PBXGroup;
190 | children = (
191 | 52925EBE28549A62001B9190 /* macOS.entitlements */,
192 | );
193 | path = macOS;
194 | sourceTree = "";
195 | };
196 | 52925ED028549C15001B9190 /* Packages */ = {
197 | isa = PBXGroup;
198 | children = (
199 | 52925ED128549C15001B9190 /* NavigationBackport */,
200 | );
201 | name = Packages;
202 | sourceTree = "";
203 | };
204 | 529B5DC22A40F33E006C6779 /* NavigationBackportWatchApp Watch App */ = {
205 | isa = PBXGroup;
206 | children = (
207 | 529B5DC72A40F340006C6779 /* Assets.xcassets */,
208 | 529B5DC92A40F340006C6779 /* Preview Content */,
209 | );
210 | path = "NavigationBackportWatchApp Watch App";
211 | sourceTree = "";
212 | };
213 | 529B5DC92A40F340006C6779 /* Preview Content */ = {
214 | isa = PBXGroup;
215 | children = (
216 | 529B5DCA2A40F340006C6779 /* Preview Assets.xcassets */,
217 | );
218 | path = "Preview Content";
219 | sourceTree = "";
220 | };
221 | 52B3F1602A01BE4200EC5EA9 /* NavigationBackportUITests */ = {
222 | isa = PBXGroup;
223 | children = (
224 | 529DD2802CB7D7E6002AA978 /* LeakUITests.swift */,
225 | 520A1E572BCF294000BD60A8 /* InitialisationUITests.swift */,
226 | 520A1E582BCF294000BD60A8 /* LocalDestinationUITests.swift */,
227 | 52B3F1612A01BE4200EC5EA9 /* NavigationUITests.swift */,
228 | );
229 | path = NavigationBackportUITests;
230 | sourceTree = "";
231 | };
232 | 52D540F82CA602320077D871 /* LeakTest */ = {
233 | isa = PBXGroup;
234 | children = (
235 | 52D540EC2CA602320077D871 /* MainView.swift */,
236 | 52D540F52CA602320077D871 /* LeakTestView.swift */,
237 | 52D540F72CA602320077D871 /* Presentable.swift */,
238 | );
239 | path = LeakTest;
240 | sourceTree = "";
241 | };
242 | 52DD85F62895C984004B5344 /* Frameworks */ = {
243 | isa = PBXGroup;
244 | children = (
245 | );
246 | name = Frameworks;
247 | sourceTree = "";
248 | };
249 | /* End PBXGroup section */
250 |
251 | /* Begin PBXNativeTarget section */
252 | 52925EB528549A62001B9190 /* NavigationBackportApp (iOS) */ = {
253 | isa = PBXNativeTarget;
254 | buildConfigurationList = 52925EC728549A62001B9190 /* Build configuration list for PBXNativeTarget "NavigationBackportApp (iOS)" */;
255 | buildPhases = (
256 | 52925EB228549A62001B9190 /* Sources */,
257 | 52925EB328549A62001B9190 /* Frameworks */,
258 | 52925EB428549A62001B9190 /* Resources */,
259 | );
260 | buildRules = (
261 | );
262 | dependencies = (
263 | );
264 | name = "NavigationBackportApp (iOS)";
265 | packageProductDependencies = (
266 | 5252ED4F2C510686004CB25A /* NavigationBackport */,
267 | );
268 | productName = "NavigationBackportApp (iOS)";
269 | productReference = 52925EB628549A62001B9190 /* NavigationBackportApp.app */;
270 | productType = "com.apple.product-type.application";
271 | };
272 | 52925EBB28549A62001B9190 /* NavigationBackportApp (macOS) */ = {
273 | isa = PBXNativeTarget;
274 | buildConfigurationList = 52925ECA28549A62001B9190 /* Build configuration list for PBXNativeTarget "NavigationBackportApp (macOS)" */;
275 | buildPhases = (
276 | 52925EB828549A62001B9190 /* Sources */,
277 | 52925EB928549A62001B9190 /* Frameworks */,
278 | 52925EBA28549A62001B9190 /* Resources */,
279 | );
280 | buildRules = (
281 | );
282 | dependencies = (
283 | );
284 | name = "NavigationBackportApp (macOS)";
285 | packageProductDependencies = (
286 | 52DD85F72895C984004B5344 /* NavigationBackport */,
287 | );
288 | productName = "NavigationBackportApp (macOS)";
289 | productReference = 52925EBC28549A62001B9190 /* NavigationBackportApp.app */;
290 | productType = "com.apple.product-type.application";
291 | };
292 | 529B5DB82A40F33E006C6779 /* NavigationBackportWatchApp */ = {
293 | isa = PBXNativeTarget;
294 | buildConfigurationList = 529B5DD22A40F340006C6779 /* Build configuration list for PBXNativeTarget "NavigationBackportWatchApp" */;
295 | buildPhases = (
296 | 529B5DB72A40F33E006C6779 /* Resources */,
297 | 529B5DCE2A40F340006C6779 /* Embed Watch Content */,
298 | );
299 | buildRules = (
300 | );
301 | dependencies = (
302 | 529B5DC12A40F33E006C6779 /* PBXTargetDependency */,
303 | );
304 | name = NavigationBackportWatchApp;
305 | productName = NavigationBackportWatchApp;
306 | productReference = 529B5DB92A40F33E006C6779 /* NavigationBackportWatchApp.app */;
307 | productType = "com.apple.product-type.application.watchapp2-container";
308 | };
309 | 529B5DBD2A40F33E006C6779 /* NavigationBackportWatchApp Watch App */ = {
310 | isa = PBXNativeTarget;
311 | buildConfigurationList = 529B5DD12A40F340006C6779 /* Build configuration list for PBXNativeTarget "NavigationBackportWatchApp Watch App" */;
312 | buildPhases = (
313 | 529B5DBA2A40F33E006C6779 /* Sources */,
314 | 529B5DBB2A40F33E006C6779 /* Frameworks */,
315 | 529B5DBC2A40F33E006C6779 /* Resources */,
316 | );
317 | buildRules = (
318 | );
319 | dependencies = (
320 | );
321 | name = "NavigationBackportWatchApp Watch App";
322 | packageProductDependencies = (
323 | 529B5DD82A40F3BD006C6779 /* NavigationBackport */,
324 | );
325 | productName = "NavigationBackportWatchApp Watch App";
326 | productReference = 529B5DBE2A40F33E006C6779 /* NavigationBackportWatchApp Watch App.app */;
327 | productType = "com.apple.product-type.application";
328 | };
329 | 52B3F15E2A01BE4200EC5EA9 /* NavigationBackportUITests */ = {
330 | isa = PBXNativeTarget;
331 | buildConfigurationList = 52B3F1692A01BE4200EC5EA9 /* Build configuration list for PBXNativeTarget "NavigationBackportUITests" */;
332 | buildPhases = (
333 | 52B3F15B2A01BE4200EC5EA9 /* Sources */,
334 | 52B3F15C2A01BE4200EC5EA9 /* Frameworks */,
335 | 52B3F15D2A01BE4200EC5EA9 /* Resources */,
336 | );
337 | buildRules = (
338 | );
339 | dependencies = (
340 | 52B3F1662A01BE4200EC5EA9 /* PBXTargetDependency */,
341 | );
342 | name = NavigationBackportUITests;
343 | productName = NavigationBackportUITests;
344 | productReference = 52B3F15F2A01BE4200EC5EA9 /* NavigationBackportUITests.xctest */;
345 | productType = "com.apple.product-type.bundle.ui-testing";
346 | };
347 | /* End PBXNativeTarget section */
348 |
349 | /* Begin PBXProject section */
350 | 52925EAA28549A60001B9190 /* Project object */ = {
351 | isa = PBXProject;
352 | attributes = {
353 | BuildIndependentTargetsInParallel = 1;
354 | LastSwiftUpdateCheck = 1430;
355 | LastUpgradeCheck = 1420;
356 | TargetAttributes = {
357 | 52925EB528549A62001B9190 = {
358 | CreatedOnToolsVersion = 13.4.1;
359 | };
360 | 52925EBB28549A62001B9190 = {
361 | CreatedOnToolsVersion = 13.4.1;
362 | };
363 | 529B5DB82A40F33E006C6779 = {
364 | CreatedOnToolsVersion = 14.3.1;
365 | };
366 | 529B5DBD2A40F33E006C6779 = {
367 | CreatedOnToolsVersion = 14.3.1;
368 | };
369 | 52B3F15E2A01BE4200EC5EA9 = {
370 | CreatedOnToolsVersion = 14.2;
371 | TestTargetID = 52925EB528549A62001B9190;
372 | };
373 | };
374 | };
375 | buildConfigurationList = 52925EAD28549A60001B9190 /* Build configuration list for PBXProject "NavigationBackportApp" */;
376 | compatibilityVersion = "Xcode 13.0";
377 | developmentRegion = en;
378 | hasScannedForEncodings = 0;
379 | knownRegions = (
380 | en,
381 | Base,
382 | );
383 | mainGroup = 52925EA928549A60001B9190;
384 | packageReferences = (
385 | );
386 | productRefGroup = 52925EB728549A62001B9190 /* Products */;
387 | projectDirPath = "";
388 | projectRoot = "";
389 | targets = (
390 | 52925EB528549A62001B9190 /* NavigationBackportApp (iOS) */,
391 | 52925EBB28549A62001B9190 /* NavigationBackportApp (macOS) */,
392 | 52B3F15E2A01BE4200EC5EA9 /* NavigationBackportUITests */,
393 | 529B5DB82A40F33E006C6779 /* NavigationBackportWatchApp */,
394 | 529B5DBD2A40F33E006C6779 /* NavigationBackportWatchApp Watch App */,
395 | );
396 | };
397 | /* End PBXProject section */
398 |
399 | /* Begin PBXResourcesBuildPhase section */
400 | 52925EB428549A62001B9190 /* Resources */ = {
401 | isa = PBXResourcesBuildPhase;
402 | buildActionMask = 2147483647;
403 | files = (
404 | 52925EC328549A62001B9190 /* Assets.xcassets in Resources */,
405 | );
406 | runOnlyForDeploymentPostprocessing = 0;
407 | };
408 | 52925EBA28549A62001B9190 /* Resources */ = {
409 | isa = PBXResourcesBuildPhase;
410 | buildActionMask = 2147483647;
411 | files = (
412 | 52925EC428549A62001B9190 /* Assets.xcassets in Resources */,
413 | );
414 | runOnlyForDeploymentPostprocessing = 0;
415 | };
416 | 529B5DB72A40F33E006C6779 /* Resources */ = {
417 | isa = PBXResourcesBuildPhase;
418 | buildActionMask = 2147483647;
419 | files = (
420 | );
421 | runOnlyForDeploymentPostprocessing = 0;
422 | };
423 | 529B5DBC2A40F33E006C6779 /* Resources */ = {
424 | isa = PBXResourcesBuildPhase;
425 | buildActionMask = 2147483647;
426 | files = (
427 | 529B5DCB2A40F340006C6779 /* Preview Assets.xcassets in Resources */,
428 | 529B5DC82A40F340006C6779 /* Assets.xcassets in Resources */,
429 | );
430 | runOnlyForDeploymentPostprocessing = 0;
431 | };
432 | 52B3F15D2A01BE4200EC5EA9 /* Resources */ = {
433 | isa = PBXResourcesBuildPhase;
434 | buildActionMask = 2147483647;
435 | files = (
436 | );
437 | runOnlyForDeploymentPostprocessing = 0;
438 | };
439 | /* End PBXResourcesBuildPhase section */
440 |
441 | /* Begin PBXSourcesBuildPhase section */
442 | 52925EB228549A62001B9190 /* Sources */ = {
443 | isa = PBXSourcesBuildPhase;
444 | buildActionMask = 2147483647;
445 | files = (
446 | 520A1E5F2BCF347A00BD60A8 /* TrafficLight.swift in Sources */,
447 | 52E0D8A22A45002D008963E7 /* ProcessArguments.swift in Sources */,
448 | 529673CE286BB44400C01BCF /* NBNavigationPathView.swift in Sources */,
449 | 529673D1286BB50200C01BCF /* ArrayBindingView.swift in Sources */,
450 | 529673D4286BC48600C01BCF /* NoBindingView.swift in Sources */,
451 | 52925EC128549A62001B9190 /* ContentView.swift in Sources */,
452 | 52925EBF28549A62001B9190 /* NavigationBackportApp.swift in Sources */,
453 | 52D540FE2CA602320077D871 /* LeakTestView.swift in Sources */,
454 | 52D540FF2CA602320077D871 /* MainView.swift in Sources */,
455 | 52D541012CA602320077D871 /* Presentable.swift in Sources */,
456 | );
457 | runOnlyForDeploymentPostprocessing = 0;
458 | };
459 | 52925EB828549A62001B9190 /* Sources */ = {
460 | isa = PBXSourcesBuildPhase;
461 | buildActionMask = 2147483647;
462 | files = (
463 | 520A1E5E2BCF347A00BD60A8 /* TrafficLight.swift in Sources */,
464 | 52E0D8A32A45002D008963E7 /* ProcessArguments.swift in Sources */,
465 | 529673CF286BB44400C01BCF /* NBNavigationPathView.swift in Sources */,
466 | 529673D2286BB50200C01BCF /* ArrayBindingView.swift in Sources */,
467 | 529673D5286BC48600C01BCF /* NoBindingView.swift in Sources */,
468 | 52925EC228549A62001B9190 /* ContentView.swift in Sources */,
469 | 52925EC028549A62001B9190 /* NavigationBackportApp.swift in Sources */,
470 | 52D541072CA602320077D871 /* LeakTestView.swift in Sources */,
471 | 52D541082CA602320077D871 /* MainView.swift in Sources */,
472 | 52D5410A2CA602320077D871 /* Presentable.swift in Sources */,
473 | );
474 | runOnlyForDeploymentPostprocessing = 0;
475 | };
476 | 529B5DBA2A40F33E006C6779 /* Sources */ = {
477 | isa = PBXSourcesBuildPhase;
478 | buildActionMask = 2147483647;
479 | files = (
480 | 520A1E5D2BCF347900BD60A8 /* TrafficLight.swift in Sources */,
481 | 52E0D8A42A45002D008963E7 /* ProcessArguments.swift in Sources */,
482 | 529B5DD32A40F37E006C6779 /* NBNavigationPathView.swift in Sources */,
483 | 529B5DD62A40F37E006C6779 /* NavigationBackportApp.swift in Sources */,
484 | 529B5DD52A40F37E006C6779 /* ContentView.swift in Sources */,
485 | 529B5DD72A40F37E006C6779 /* ArrayBindingView.swift in Sources */,
486 | 529B5DD42A40F37E006C6779 /* NoBindingView.swift in Sources */,
487 | 52D541102CA602320077D871 /* LeakTestView.swift in Sources */,
488 | 52D541112CA602320077D871 /* MainView.swift in Sources */,
489 | 52D541132CA602320077D871 /* Presentable.swift in Sources */,
490 | );
491 | runOnlyForDeploymentPostprocessing = 0;
492 | };
493 | 52B3F15B2A01BE4200EC5EA9 /* Sources */ = {
494 | isa = PBXSourcesBuildPhase;
495 | buildActionMask = 2147483647;
496 | files = (
497 | 52B3F1622A01BE4200EC5EA9 /* NavigationUITests.swift in Sources */,
498 | 520A1E592BCF294000BD60A8 /* InitialisationUITests.swift in Sources */,
499 | 529DD2812CB7D7E6002AA978 /* LeakUITests.swift in Sources */,
500 | 520A1E5A2BCF294000BD60A8 /* LocalDestinationUITests.swift in Sources */,
501 | );
502 | runOnlyForDeploymentPostprocessing = 0;
503 | };
504 | /* End PBXSourcesBuildPhase section */
505 |
506 | /* Begin PBXTargetDependency section */
507 | 529B5DC12A40F33E006C6779 /* PBXTargetDependency */ = {
508 | isa = PBXTargetDependency;
509 | target = 529B5DBD2A40F33E006C6779 /* NavigationBackportWatchApp Watch App */;
510 | targetProxy = 529B5DC02A40F33E006C6779 /* PBXContainerItemProxy */;
511 | };
512 | 52B3F1662A01BE4200EC5EA9 /* PBXTargetDependency */ = {
513 | isa = PBXTargetDependency;
514 | target = 52925EB528549A62001B9190 /* NavigationBackportApp (iOS) */;
515 | targetProxy = 52B3F1652A01BE4200EC5EA9 /* PBXContainerItemProxy */;
516 | };
517 | /* End PBXTargetDependency section */
518 |
519 | /* Begin XCBuildConfiguration section */
520 | 52925EC528549A62001B9190 /* Debug */ = {
521 | isa = XCBuildConfiguration;
522 | buildSettings = {
523 | ALWAYS_SEARCH_USER_PATHS = NO;
524 | CLANG_ANALYZER_NONNULL = YES;
525 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
526 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
527 | CLANG_ENABLE_MODULES = YES;
528 | CLANG_ENABLE_OBJC_ARC = YES;
529 | CLANG_ENABLE_OBJC_WEAK = YES;
530 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
531 | CLANG_WARN_BOOL_CONVERSION = YES;
532 | CLANG_WARN_COMMA = YES;
533 | CLANG_WARN_CONSTANT_CONVERSION = YES;
534 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
535 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
536 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
537 | CLANG_WARN_EMPTY_BODY = YES;
538 | CLANG_WARN_ENUM_CONVERSION = YES;
539 | CLANG_WARN_INFINITE_RECURSION = YES;
540 | CLANG_WARN_INT_CONVERSION = YES;
541 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
542 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
543 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
544 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
545 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
546 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
547 | CLANG_WARN_STRICT_PROTOTYPES = YES;
548 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
549 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
550 | CLANG_WARN_UNREACHABLE_CODE = YES;
551 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
552 | COPY_PHASE_STRIP = NO;
553 | DEAD_CODE_STRIPPING = YES;
554 | DEBUG_INFORMATION_FORMAT = dwarf;
555 | ENABLE_STRICT_OBJC_MSGSEND = YES;
556 | ENABLE_TESTABILITY = YES;
557 | GCC_C_LANGUAGE_STANDARD = gnu11;
558 | GCC_DYNAMIC_NO_PIC = NO;
559 | GCC_NO_COMMON_BLOCKS = YES;
560 | GCC_OPTIMIZATION_LEVEL = 0;
561 | GCC_PREPROCESSOR_DEFINITIONS = (
562 | "DEBUG=1",
563 | "$(inherited)",
564 | );
565 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
566 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
567 | GCC_WARN_UNDECLARED_SELECTOR = YES;
568 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
569 | GCC_WARN_UNUSED_FUNCTION = YES;
570 | GCC_WARN_UNUSED_VARIABLE = YES;
571 | IPHONEOS_DEPLOYMENT_TARGET = 14.0;
572 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
573 | MTL_FAST_MATH = YES;
574 | ONLY_ACTIVE_ARCH = YES;
575 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
576 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
577 | };
578 | name = Debug;
579 | };
580 | 52925EC628549A62001B9190 /* Release */ = {
581 | isa = XCBuildConfiguration;
582 | buildSettings = {
583 | ALWAYS_SEARCH_USER_PATHS = NO;
584 | CLANG_ANALYZER_NONNULL = YES;
585 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
586 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
587 | CLANG_ENABLE_MODULES = YES;
588 | CLANG_ENABLE_OBJC_ARC = YES;
589 | CLANG_ENABLE_OBJC_WEAK = YES;
590 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
591 | CLANG_WARN_BOOL_CONVERSION = YES;
592 | CLANG_WARN_COMMA = YES;
593 | CLANG_WARN_CONSTANT_CONVERSION = YES;
594 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
595 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
596 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
597 | CLANG_WARN_EMPTY_BODY = YES;
598 | CLANG_WARN_ENUM_CONVERSION = YES;
599 | CLANG_WARN_INFINITE_RECURSION = YES;
600 | CLANG_WARN_INT_CONVERSION = YES;
601 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
602 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
603 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
604 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
605 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
606 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
607 | CLANG_WARN_STRICT_PROTOTYPES = YES;
608 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
609 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
610 | CLANG_WARN_UNREACHABLE_CODE = YES;
611 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
612 | COPY_PHASE_STRIP = NO;
613 | DEAD_CODE_STRIPPING = YES;
614 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
615 | ENABLE_NS_ASSERTIONS = NO;
616 | ENABLE_STRICT_OBJC_MSGSEND = YES;
617 | GCC_C_LANGUAGE_STANDARD = gnu11;
618 | GCC_NO_COMMON_BLOCKS = YES;
619 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
620 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
621 | GCC_WARN_UNDECLARED_SELECTOR = YES;
622 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
623 | GCC_WARN_UNUSED_FUNCTION = YES;
624 | GCC_WARN_UNUSED_VARIABLE = YES;
625 | IPHONEOS_DEPLOYMENT_TARGET = 14.0;
626 | MTL_ENABLE_DEBUG_INFO = NO;
627 | MTL_FAST_MATH = YES;
628 | SWIFT_COMPILATION_MODE = wholemodule;
629 | SWIFT_OPTIMIZATION_LEVEL = "-O";
630 | };
631 | name = Release;
632 | };
633 | 52925EC828549A62001B9190 /* Debug */ = {
634 | isa = XCBuildConfiguration;
635 | buildSettings = {
636 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
637 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
638 | CODE_SIGN_STYLE = Automatic;
639 | CURRENT_PROJECT_VERSION = 1;
640 | DEVELOPMENT_TEAM = "";
641 | ENABLE_PREVIEWS = YES;
642 | GENERATE_INFOPLIST_FILE = YES;
643 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
644 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
645 | INFOPLIST_KEY_UILaunchScreen_Generation = YES;
646 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
647 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
648 | IPHONEOS_DEPLOYMENT_TARGET = 14.0;
649 | LD_RUNPATH_SEARCH_PATHS = (
650 | "$(inherited)",
651 | "@executable_path/Frameworks",
652 | );
653 | MARKETING_VERSION = 1.0;
654 | PRODUCT_BUNDLE_IDENTIFIER = co.uk.johnpatrickmorgan.NavigationBackportApp;
655 | PRODUCT_NAME = NavigationBackportApp;
656 | SDKROOT = iphoneos;
657 | SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator";
658 | SUPPORTS_MACCATALYST = NO;
659 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;
660 | SWIFT_EMIT_LOC_STRINGS = YES;
661 | SWIFT_VERSION = 5.0;
662 | TARGETED_DEVICE_FAMILY = "1,2,3";
663 | TVOS_DEPLOYMENT_TARGET = 14.0;
664 | };
665 | name = Debug;
666 | };
667 | 52925EC928549A62001B9190 /* Release */ = {
668 | isa = XCBuildConfiguration;
669 | buildSettings = {
670 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
671 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
672 | CODE_SIGN_STYLE = Automatic;
673 | CURRENT_PROJECT_VERSION = 1;
674 | DEVELOPMENT_TEAM = "";
675 | ENABLE_PREVIEWS = YES;
676 | GENERATE_INFOPLIST_FILE = YES;
677 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
678 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
679 | INFOPLIST_KEY_UILaunchScreen_Generation = YES;
680 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
681 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
682 | IPHONEOS_DEPLOYMENT_TARGET = 14.0;
683 | LD_RUNPATH_SEARCH_PATHS = (
684 | "$(inherited)",
685 | "@executable_path/Frameworks",
686 | );
687 | MARKETING_VERSION = 1.0;
688 | PRODUCT_BUNDLE_IDENTIFIER = co.uk.johnpatrickmorgan.NavigationBackportApp;
689 | PRODUCT_NAME = NavigationBackportApp;
690 | SDKROOT = iphoneos;
691 | SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator";
692 | SUPPORTS_MACCATALYST = NO;
693 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;
694 | SWIFT_EMIT_LOC_STRINGS = YES;
695 | SWIFT_VERSION = 5.0;
696 | TARGETED_DEVICE_FAMILY = "1,2,3";
697 | TVOS_DEPLOYMENT_TARGET = 14.0;
698 | VALIDATE_PRODUCT = YES;
699 | };
700 | name = Release;
701 | };
702 | 52925ECB28549A62001B9190 /* Debug */ = {
703 | isa = XCBuildConfiguration;
704 | buildSettings = {
705 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
706 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
707 | CODE_SIGN_ENTITLEMENTS = macOS/macOS.entitlements;
708 | CODE_SIGN_IDENTITY = "-";
709 | "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
710 | CODE_SIGN_STYLE = Automatic;
711 | COMBINE_HIDPI_IMAGES = YES;
712 | CURRENT_PROJECT_VERSION = 1;
713 | DEAD_CODE_STRIPPING = YES;
714 | DEVELOPMENT_TEAM = "";
715 | ENABLE_HARDENED_RUNTIME = YES;
716 | ENABLE_PREVIEWS = YES;
717 | GENERATE_INFOPLIST_FILE = YES;
718 | INFOPLIST_KEY_NSHumanReadableCopyright = "";
719 | LD_RUNPATH_SEARCH_PATHS = (
720 | "$(inherited)",
721 | "@executable_path/../Frameworks",
722 | );
723 | MACOSX_DEPLOYMENT_TARGET = 12.3;
724 | MARKETING_VERSION = 1.0;
725 | PRODUCT_BUNDLE_IDENTIFIER = co.uk.johnpatrickmorgan.NavigationBackportApp;
726 | PRODUCT_NAME = NavigationBackportApp;
727 | SDKROOT = macosx;
728 | SWIFT_EMIT_LOC_STRINGS = YES;
729 | SWIFT_VERSION = 5.0;
730 | };
731 | name = Debug;
732 | };
733 | 52925ECC28549A62001B9190 /* Release */ = {
734 | isa = XCBuildConfiguration;
735 | buildSettings = {
736 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
737 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
738 | CODE_SIGN_ENTITLEMENTS = macOS/macOS.entitlements;
739 | CODE_SIGN_IDENTITY = "-";
740 | "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-";
741 | CODE_SIGN_STYLE = Automatic;
742 | COMBINE_HIDPI_IMAGES = YES;
743 | CURRENT_PROJECT_VERSION = 1;
744 | DEAD_CODE_STRIPPING = YES;
745 | DEVELOPMENT_TEAM = "";
746 | ENABLE_HARDENED_RUNTIME = YES;
747 | ENABLE_PREVIEWS = YES;
748 | GENERATE_INFOPLIST_FILE = YES;
749 | INFOPLIST_KEY_NSHumanReadableCopyright = "";
750 | LD_RUNPATH_SEARCH_PATHS = (
751 | "$(inherited)",
752 | "@executable_path/../Frameworks",
753 | );
754 | MACOSX_DEPLOYMENT_TARGET = 12.3;
755 | MARKETING_VERSION = 1.0;
756 | PRODUCT_BUNDLE_IDENTIFIER = co.uk.johnpatrickmorgan.NavigationBackportApp;
757 | PRODUCT_NAME = NavigationBackportApp;
758 | SDKROOT = macosx;
759 | SWIFT_EMIT_LOC_STRINGS = YES;
760 | SWIFT_VERSION = 5.0;
761 | };
762 | name = Release;
763 | };
764 | 529B5DCC2A40F340006C6779 /* Debug */ = {
765 | isa = XCBuildConfiguration;
766 | buildSettings = {
767 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
768 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
769 | CODE_SIGN_STYLE = Automatic;
770 | CURRENT_PROJECT_VERSION = 1;
771 | INFOPLIST_KEY_CFBundleDisplayName = NavigationBackportWatchApp;
772 | MARKETING_VERSION = 1.0;
773 | PRODUCT_BUNDLE_IDENTIFIER = co.uk.johnpatrickmorgan.NavigationBackportWatchApp;
774 | PRODUCT_NAME = "$(TARGET_NAME)";
775 | SDKROOT = iphoneos;
776 | SWIFT_VERSION = 5.0;
777 | };
778 | name = Debug;
779 | };
780 | 529B5DCD2A40F340006C6779 /* Release */ = {
781 | isa = XCBuildConfiguration;
782 | buildSettings = {
783 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
784 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
785 | CODE_SIGN_STYLE = Automatic;
786 | CURRENT_PROJECT_VERSION = 1;
787 | INFOPLIST_KEY_CFBundleDisplayName = NavigationBackportWatchApp;
788 | MARKETING_VERSION = 1.0;
789 | PRODUCT_BUNDLE_IDENTIFIER = co.uk.johnpatrickmorgan.NavigationBackportWatchApp;
790 | PRODUCT_NAME = "$(TARGET_NAME)";
791 | SDKROOT = iphoneos;
792 | SWIFT_VERSION = 5.0;
793 | VALIDATE_PRODUCT = YES;
794 | };
795 | name = Release;
796 | };
797 | 529B5DCF2A40F340006C6779 /* Debug */ = {
798 | isa = XCBuildConfiguration;
799 | buildSettings = {
800 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
801 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
802 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
803 | CODE_SIGN_STYLE = Automatic;
804 | CURRENT_PROJECT_VERSION = 1;
805 | DEVELOPMENT_ASSET_PATHS = "\"NavigationBackportWatchApp Watch App/Preview Content\"";
806 | ENABLE_PREVIEWS = YES;
807 | GENERATE_INFOPLIST_FILE = YES;
808 | INFOPLIST_KEY_CFBundleDisplayName = NavigationBackportWatchApp;
809 | INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
810 | INFOPLIST_KEY_WKWatchOnly = YES;
811 | LD_RUNPATH_SEARCH_PATHS = (
812 | "$(inherited)",
813 | "@executable_path/Frameworks",
814 | );
815 | MARKETING_VERSION = 1.0;
816 | PRODUCT_BUNDLE_IDENTIFIER = co.uk.johnpatrickmorgan.NavigationBackportWatchApp.watchkitapp;
817 | PRODUCT_NAME = "$(TARGET_NAME)";
818 | SDKROOT = watchos;
819 | SKIP_INSTALL = YES;
820 | SWIFT_EMIT_LOC_STRINGS = YES;
821 | SWIFT_VERSION = 5.0;
822 | TARGETED_DEVICE_FAMILY = 4;
823 | WATCHOS_DEPLOYMENT_TARGET = 9.4;
824 | };
825 | name = Debug;
826 | };
827 | 529B5DD02A40F340006C6779 /* Release */ = {
828 | isa = XCBuildConfiguration;
829 | buildSettings = {
830 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
831 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
832 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
833 | CODE_SIGN_STYLE = Automatic;
834 | CURRENT_PROJECT_VERSION = 1;
835 | DEVELOPMENT_ASSET_PATHS = "\"NavigationBackportWatchApp Watch App/Preview Content\"";
836 | ENABLE_PREVIEWS = YES;
837 | GENERATE_INFOPLIST_FILE = YES;
838 | INFOPLIST_KEY_CFBundleDisplayName = NavigationBackportWatchApp;
839 | INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
840 | INFOPLIST_KEY_WKWatchOnly = YES;
841 | LD_RUNPATH_SEARCH_PATHS = (
842 | "$(inherited)",
843 | "@executable_path/Frameworks",
844 | );
845 | MARKETING_VERSION = 1.0;
846 | PRODUCT_BUNDLE_IDENTIFIER = co.uk.johnpatrickmorgan.NavigationBackportWatchApp.watchkitapp;
847 | PRODUCT_NAME = "$(TARGET_NAME)";
848 | SDKROOT = watchos;
849 | SKIP_INSTALL = YES;
850 | SWIFT_EMIT_LOC_STRINGS = YES;
851 | SWIFT_VERSION = 5.0;
852 | TARGETED_DEVICE_FAMILY = 4;
853 | VALIDATE_PRODUCT = YES;
854 | WATCHOS_DEPLOYMENT_TARGET = 9.4;
855 | };
856 | name = Release;
857 | };
858 | 52B3F1672A01BE4200EC5EA9 /* Debug */ = {
859 | isa = XCBuildConfiguration;
860 | buildSettings = {
861 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
862 | CODE_SIGN_STYLE = Automatic;
863 | CURRENT_PROJECT_VERSION = 1;
864 | GENERATE_INFOPLIST_FILE = YES;
865 | IPHONEOS_DEPLOYMENT_TARGET = 14.0;
866 | MARKETING_VERSION = 1.0;
867 | PRODUCT_BUNDLE_IDENTIFIER = co.uk.johnpatrickmorgan.NavigationBackportUITests;
868 | PRODUCT_NAME = "$(TARGET_NAME)";
869 | SDKROOT = iphoneos;
870 | SWIFT_EMIT_LOC_STRINGS = NO;
871 | SWIFT_VERSION = 5.0;
872 | TARGETED_DEVICE_FAMILY = "1,2";
873 | TEST_TARGET_NAME = "NavigationBackportApp (iOS)";
874 | };
875 | name = Debug;
876 | };
877 | 52B3F1682A01BE4200EC5EA9 /* Release */ = {
878 | isa = XCBuildConfiguration;
879 | buildSettings = {
880 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
881 | CODE_SIGN_STYLE = Automatic;
882 | CURRENT_PROJECT_VERSION = 1;
883 | GENERATE_INFOPLIST_FILE = YES;
884 | IPHONEOS_DEPLOYMENT_TARGET = 14.0;
885 | MARKETING_VERSION = 1.0;
886 | PRODUCT_BUNDLE_IDENTIFIER = co.uk.johnpatrickmorgan.NavigationBackportUITests;
887 | PRODUCT_NAME = "$(TARGET_NAME)";
888 | SDKROOT = iphoneos;
889 | SWIFT_EMIT_LOC_STRINGS = NO;
890 | SWIFT_VERSION = 5.0;
891 | TARGETED_DEVICE_FAMILY = "1,2";
892 | TEST_TARGET_NAME = "NavigationBackportApp (iOS)";
893 | VALIDATE_PRODUCT = YES;
894 | };
895 | name = Release;
896 | };
897 | /* End XCBuildConfiguration section */
898 |
899 | /* Begin XCConfigurationList section */
900 | 52925EAD28549A60001B9190 /* Build configuration list for PBXProject "NavigationBackportApp" */ = {
901 | isa = XCConfigurationList;
902 | buildConfigurations = (
903 | 52925EC528549A62001B9190 /* Debug */,
904 | 52925EC628549A62001B9190 /* Release */,
905 | );
906 | defaultConfigurationIsVisible = 0;
907 | defaultConfigurationName = Release;
908 | };
909 | 52925EC728549A62001B9190 /* Build configuration list for PBXNativeTarget "NavigationBackportApp (iOS)" */ = {
910 | isa = XCConfigurationList;
911 | buildConfigurations = (
912 | 52925EC828549A62001B9190 /* Debug */,
913 | 52925EC928549A62001B9190 /* Release */,
914 | );
915 | defaultConfigurationIsVisible = 0;
916 | defaultConfigurationName = Release;
917 | };
918 | 52925ECA28549A62001B9190 /* Build configuration list for PBXNativeTarget "NavigationBackportApp (macOS)" */ = {
919 | isa = XCConfigurationList;
920 | buildConfigurations = (
921 | 52925ECB28549A62001B9190 /* Debug */,
922 | 52925ECC28549A62001B9190 /* Release */,
923 | );
924 | defaultConfigurationIsVisible = 0;
925 | defaultConfigurationName = Release;
926 | };
927 | 529B5DD12A40F340006C6779 /* Build configuration list for PBXNativeTarget "NavigationBackportWatchApp Watch App" */ = {
928 | isa = XCConfigurationList;
929 | buildConfigurations = (
930 | 529B5DCF2A40F340006C6779 /* Debug */,
931 | 529B5DD02A40F340006C6779 /* Release */,
932 | );
933 | defaultConfigurationIsVisible = 0;
934 | defaultConfigurationName = Release;
935 | };
936 | 529B5DD22A40F340006C6779 /* Build configuration list for PBXNativeTarget "NavigationBackportWatchApp" */ = {
937 | isa = XCConfigurationList;
938 | buildConfigurations = (
939 | 529B5DCC2A40F340006C6779 /* Debug */,
940 | 529B5DCD2A40F340006C6779 /* Release */,
941 | );
942 | defaultConfigurationIsVisible = 0;
943 | defaultConfigurationName = Release;
944 | };
945 | 52B3F1692A01BE4200EC5EA9 /* Build configuration list for PBXNativeTarget "NavigationBackportUITests" */ = {
946 | isa = XCConfigurationList;
947 | buildConfigurations = (
948 | 52B3F1672A01BE4200EC5EA9 /* Debug */,
949 | 52B3F1682A01BE4200EC5EA9 /* Release */,
950 | );
951 | defaultConfigurationIsVisible = 0;
952 | defaultConfigurationName = Release;
953 | };
954 | /* End XCConfigurationList section */
955 |
956 | /* Begin XCSwiftPackageProductDependency section */
957 | 5252ED4F2C510686004CB25A /* NavigationBackport */ = {
958 | isa = XCSwiftPackageProductDependency;
959 | productName = NavigationBackport;
960 | };
961 | 529B5DD82A40F3BD006C6779 /* NavigationBackport */ = {
962 | isa = XCSwiftPackageProductDependency;
963 | productName = NavigationBackport;
964 | };
965 | 52DD85F72895C984004B5344 /* NavigationBackport */ = {
966 | isa = XCSwiftPackageProductDependency;
967 | productName = NavigationBackport;
968 | };
969 | /* End XCSwiftPackageProductDependency section */
970 | };
971 | rootObject = 52925EAA28549A60001B9190 /* Project object */;
972 | }
973 |
--------------------------------------------------------------------------------
/NavigationBackportApp/NavigationBackportApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/NavigationBackportApp/NavigationBackportApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/NavigationBackportApp/NavigationBackportApp.xcodeproj/xcshareddata/xcschemes/NavigationBackportApp (iOS).xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
33 |
39 |
40 |
41 |
44 |
50 |
51 |
52 |
53 |
54 |
64 |
66 |
72 |
73 |
74 |
75 |
78 |
79 |
82 |
83 |
84 |
85 |
91 |
93 |
99 |
100 |
101 |
102 |
104 |
105 |
108 |
109 |
110 |
--------------------------------------------------------------------------------
/NavigationBackportApp/NavigationBackportUITests/InitialisationUITests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 |
3 | final class InitialisationUITests: XCTestCase {
4 | override func setUpWithError() throws {
5 | continueAfterFailure = false
6 | }
7 |
8 | // MARK: With NavigationStack
9 |
10 | func testInitialisationViaPathWithNavigationStack() {
11 | launchAndRunInitialisationTests(tabTitle: "NBNavigationPath", useNavigationStack: true, app: XCUIApplication())
12 | }
13 |
14 | func testInitialisationViaArrayWithNavigationStack() {
15 | launchAndRunInitialisationTests(tabTitle: "ArrayBinding", useNavigationStack: true, app: XCUIApplication())
16 | }
17 |
18 | // MARK: Without NavigationStack
19 |
20 | func testInitialisationViaPathWithoutNavigationStack() {
21 | launchAndRunInitialisationTests(tabTitle: "NBNavigationPath", useNavigationStack: false, app: XCUIApplication())
22 | }
23 |
24 | func testInitialisationViaArrayWithoutNavigationStack() {
25 | launchAndRunInitialisationTests(tabTitle: "ArrayBinding", useNavigationStack: false, app: XCUIApplication())
26 | }
27 |
28 | func launchAndRunInitialisationTests(tabTitle: String, useNavigationStack: Bool, app: XCUIApplication) {
29 | if #available(iOS 16.0, *, macOS 13.0, *, watchOS 9.0, *, tvOS 16.0, *) {
30 | // Can test with and without NavigationStack
31 | if #available(iOS 18.0, *) {
32 | // Can only test with NavigationStack
33 | return
34 | }
35 | } else if useNavigationStack {
36 | // Can only test without NavigationStack
37 | return
38 | }
39 |
40 | app.launchArguments = ["NON_EMPTY_AT_LAUNCH"]
41 | if useNavigationStack {
42 | app.launchArguments.append("USE_NAVIGATIONSTACK")
43 | }
44 | app.launch()
45 |
46 | XCTAssertTrue(app.tabBars.buttons[tabTitle].waitForExistence(timeout: 3))
47 | app.tabBars.buttons[tabTitle].tap()
48 |
49 | if #available(iOS 16.0, *, macOS 13.0, *, watchOS 9.0, *, tvOS 16.0, *), useNavigationStack {
50 | XCTAssertTrue(app.navigationBars["4"].waitForExistence(timeout: navigationTimeout))
51 | } else {
52 | // When not using a NavigationStack, each screen is pushed one by one.
53 | XCTAssertTrue(app.navigationBars["4"].waitForExistence(timeout: navigationTimeout * 4))
54 | }
55 |
56 | app.navigationBars.buttons.element(boundBy: 0).tap()
57 | XCTAssertTrue(app.navigationBars["3"].waitForExistence(timeout: navigationTimeout))
58 |
59 | app.navigationBars.buttons.element(boundBy: 0).tap()
60 | XCTAssertTrue(app.navigationBars["2"].waitForExistence(timeout: navigationTimeout))
61 |
62 | app.navigationBars.buttons.element(boundBy: 0).tap()
63 | XCTAssertTrue(app.navigationBars["1"].waitForExistence(timeout: navigationTimeout))
64 |
65 | app.navigationBars.buttons.element(boundBy: 0).tap()
66 | XCTAssertTrue(app.navigationBars["Home"].waitForExistence(timeout: navigationTimeout))
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/NavigationBackportApp/NavigationBackportUITests/LeakUITests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 |
3 | final class LeakUITests: XCTestCase {
4 | override func setUpWithError() throws {
5 | continueAfterFailure = false
6 | }
7 |
8 | // MARK: Without NavigationStack
9 |
10 | // Note: NavigationStack itself does not immediately free up elements that are removed from its path,
11 | // so we only test when used without NavigationStack.
12 | func testLeakWithoutNavigationStack() {
13 | let app = XCUIApplication()
14 |
15 | app.launch()
16 |
17 | XCTAssertTrue(app.tabBars.buttons["LeakTest"].waitForExistence(timeout: 3))
18 | app.tabBars.buttons["LeakTest"].tap()
19 |
20 | XCTAssertTrue(app.buttons["Main"].exists)
21 |
22 | for _ in 0 ..< 3 {
23 | app.buttons["Main"].tap()
24 | XCTAssertTrue(app.navigationBars["Main"].waitForExistence(timeout: navigationTimeout))
25 | XCTAssertTrue(app.staticTexts["Count: 1"].exists)
26 | app.navigationBars.buttons.element(boundBy: 0).tap()
27 | XCTAssertTrue(app.buttons["Main"].waitForExistence(timeout: navigationTimeout))
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/NavigationBackportApp/NavigationBackportUITests/LocalDestinationUITests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 |
3 | final class LocalDestinationUITests: XCTestCase {
4 | override func setUpWithError() throws {
5 | continueAfterFailure = false
6 | }
7 |
8 | // MARK: Without NavigationStack
9 |
10 | func testLocalDestinationViaPathWithoutNavigationStack() {
11 | launchAndRunLocalDestinationTests(tabTitle: "NBNavigationPath", useNavigationStack: false, app: XCUIApplication())
12 | }
13 |
14 | func testLocalDestinationViaArrayWithoutNavigationStack() {
15 | launchAndRunLocalDestinationTests(tabTitle: "ArrayBinding", useNavigationStack: false, app: XCUIApplication())
16 | }
17 |
18 | func testLocalDestinationViaNoneWithoutNavigationStack() {
19 | launchAndRunLocalDestinationTests(tabTitle: "NoBinding", useNavigationStack: false, app: XCUIApplication())
20 | }
21 |
22 | // MARK: With NavigationStack
23 |
24 | func testLocalDestinationViaPathWithNavigationStack() {
25 | launchAndRunLocalDestinationTests(tabTitle: "NBNavigationPath", useNavigationStack: true, app: XCUIApplication())
26 | }
27 |
28 | func testLocalDestinationViaArrayWithNavigationStack() {
29 | launchAndRunLocalDestinationTests(tabTitle: "ArrayBinding", useNavigationStack: true, app: XCUIApplication())
30 | }
31 |
32 | func testLocalDestinationViaNoneWithNavigationStack() {
33 | launchAndRunLocalDestinationTests(tabTitle: "NoBinding", useNavigationStack: true, app: XCUIApplication())
34 | }
35 |
36 | func launchAndRunLocalDestinationTests(tabTitle: String, useNavigationStack: Bool, app: XCUIApplication) {
37 | if #available(iOS 16.0, *, macOS 13.0, *, watchOS 9.0, *, tvOS 16.0, *) {
38 | // Can test with and without NavigationStack
39 | } else if useNavigationStack {
40 | // Can only test without NavigationStack
41 | return
42 | }
43 |
44 | if useNavigationStack {
45 | app.launchArguments.append("USE_NAVIGATIONSTACK")
46 | }
47 | app.launch()
48 |
49 | XCTAssertTrue(app.tabBars.buttons[tabTitle].waitForExistence(timeout: 3))
50 | app.tabBars.buttons[tabTitle].tap()
51 |
52 | XCTAssertTrue(app.navigationBars["Home"].waitForExistence(timeout: navigationTimeout))
53 |
54 | app.buttons["Push traffic lights"].tap()
55 |
56 | XCTAssertTrue(app.staticTexts["red"].waitForExistence(timeout: 1))
57 | app.staticTexts["red"].tap()
58 |
59 | XCTAssertTrue(app.staticTexts["amber"].waitForExistence(timeout: 1))
60 | app.staticTexts["amber"].tap()
61 |
62 | XCTAssertTrue(app.staticTexts["green"].waitForExistence(timeout: 1))
63 | app.staticTexts["green"].tap()
64 |
65 | XCTAssertTrue(app.navigationBars["Home"].waitForExistence(timeout: navigationTimeout))
66 |
67 | app.buttons["Push local destination"].tap()
68 | XCTAssertTrue(app.staticTexts["Local destination"].waitForExistence(timeout: navigationTimeout))
69 |
70 | app.navigationBars.buttons.element(boundBy: 0).tap()
71 | XCTAssertTrue(app.navigationBars["Home"].waitForExistence(timeout: navigationTimeout))
72 | XCTAssertTrue(app.buttons["Push local destination"].isEnabled)
73 |
74 | app.buttons["Push local destination"].tap()
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/NavigationBackportApp/NavigationBackportUITests/NavigationUITests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 |
3 | let navigationTimeout = 0.8
4 |
5 | final class NavigationBackportUITests: XCTestCase {
6 | override func setUpWithError() throws {
7 | continueAfterFailure = false
8 | }
9 |
10 | // MARK: Without NavigationStack
11 |
12 | func testNavigationViaPathWithoutNavigationStack() {
13 | launchAndRunNavigationTests(tabTitle: "NBNavigationPath", useNavigationStack: false, app: XCUIApplication())
14 | }
15 |
16 | func testNavigationViaArrayWithoutNavigationStack() {
17 | launchAndRunNavigationTests(tabTitle: "ArrayBinding", useNavigationStack: false, app: XCUIApplication())
18 | }
19 |
20 | func testNavigationViaNoneWithoutNavigationStack() {
21 | launchAndRunNavigationTests(tabTitle: "NoBinding", useNavigationStack: false, app: XCUIApplication())
22 | }
23 |
24 | // MARK: With NavigationStack
25 |
26 | func testNavigationViaPathWithNavigationStack() {
27 | launchAndRunNavigationTests(tabTitle: "NBNavigationPath", useNavigationStack: true, app: XCUIApplication())
28 | }
29 |
30 | func testNavigationViaArrayWithNavigationStack() {
31 | launchAndRunNavigationTests(tabTitle: "ArrayBinding", useNavigationStack: true, app: XCUIApplication())
32 | }
33 |
34 | func testNavigationViaNoneWithNavigationStack() {
35 | launchAndRunNavigationTests(tabTitle: "NoBinding", useNavigationStack: true, app: XCUIApplication())
36 | }
37 |
38 | func launchAndRunNavigationTests(tabTitle: String, useNavigationStack: Bool, app: XCUIApplication) {
39 | if useNavigationStack {
40 | app.launchArguments = ["USE_NAVIGATIONSTACK"]
41 | }
42 | app.launch()
43 |
44 | XCTAssertTrue(app.tabBars.buttons[tabTitle].waitForExistence(timeout: 3))
45 | app.tabBars.buttons[tabTitle].tap()
46 | XCTAssertTrue(app.navigationBars["Home"].waitForExistence(timeout: 2))
47 |
48 | app.buttons["Pick a number"].tap()
49 | XCTAssertTrue(app.navigationBars["List"].waitForExistence(timeout: navigationTimeout))
50 |
51 | app.navigationBars.buttons.element(boundBy: 0).tap()
52 | XCTAssertTrue(app.navigationBars["Home"].waitForExistence(timeout: navigationTimeout))
53 |
54 | app.buttons["99 Red balloons"].tap()
55 | XCTAssertTrue(app.navigationBars["Visualise 99"].waitForExistence(timeout: 2 * navigationTimeout))
56 |
57 | app.navigationBars.buttons.element(boundBy: 0).tap()
58 | app.navigationBars.buttons.element(boundBy: 0).tap()
59 | XCTAssertTrue(app.navigationBars["Home"].waitForExistence(timeout: navigationTimeout))
60 |
61 | app.buttons["Pick a number"].tap()
62 | XCTAssertTrue(app.navigationBars["List"].waitForExistence(timeout: navigationTimeout))
63 |
64 | app.buttons["1"].tap()
65 | XCTAssertTrue(app.navigationBars["1"].waitForExistence(timeout: navigationTimeout))
66 |
67 | app.buttons["Show next number"].tap()
68 | XCTAssertTrue(app.navigationBars["2"].waitForExistence(timeout: navigationTimeout))
69 |
70 | app.buttons["Show next number"].tap()
71 | XCTAssertTrue(app.navigationBars["3"].waitForExistence(timeout: navigationTimeout))
72 |
73 | app.buttons["Show next number"].tap()
74 | XCTAssertTrue(app.navigationBars["4"].waitForExistence(timeout: navigationTimeout))
75 |
76 | app.buttons["Go back to root"].tap()
77 | XCTAssertTrue(app.navigationBars["Home"].waitForExistence(timeout: navigationTimeout))
78 |
79 | if #available(iOS 15.0, *) {
80 | // This test fails on iOS 14, despite working in real use.
81 | app.buttons["Push local destination"].tap()
82 | XCTAssertTrue(app.staticTexts["Local destination"].waitForExistence(timeout: navigationTimeout * 2))
83 |
84 | app.navigationBars.buttons.element(boundBy: 0).tap()
85 | XCTAssertTrue(app.navigationBars["Home"].waitForExistence(timeout: navigationTimeout))
86 | XCTAssertTrue(app.buttons["Push local destination"].isEnabled)
87 | }
88 |
89 | if tabTitle != "ArrayBinding" {
90 | app.buttons["Show Class Destination"].tap()
91 | XCTAssertTrue(app.staticTexts["Sample data"].waitForExistence(timeout: navigationTimeout))
92 |
93 | app.navigationBars.buttons.element(boundBy: 0).tap()
94 | XCTAssertTrue(app.navigationBars["Home"].waitForExistence(timeout: navigationTimeout))
95 | }
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/NavigationBackportApp/NavigationBackportWatchApp Watch App/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/NavigationBackportApp/NavigationBackportWatchApp Watch App/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "platform" : "watchos",
6 | "size" : "1024x1024"
7 | }
8 | ],
9 | "info" : {
10 | "author" : "xcode",
11 | "version" : 1
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/NavigationBackportApp/NavigationBackportWatchApp Watch App/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/NavigationBackportApp/NavigationBackportWatchApp Watch App/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/NavigationBackportApp/Package.swift:
--------------------------------------------------------------------------------
1 | import PackageDescription
2 |
3 | let package = Package(
4 | name: "",
5 | products: [],
6 | dependencies: [],
7 | targets: []
8 | )
9 |
--------------------------------------------------------------------------------
/NavigationBackportApp/Shared/ArrayBindingView.swift:
--------------------------------------------------------------------------------
1 | import NavigationBackport
2 | import SwiftUI
3 |
4 | enum Screen: NBScreen {
5 | case number(Int)
6 | case numberList(NumberList)
7 | case visualisation(EmojiVisualisation)
8 | }
9 |
10 | struct ArrayBindingView: View {
11 | @State var savedPath: [Screen]?
12 | @State var path: [Screen] = ProcessArguments.nonEmptyAtLaunch ? [.number(1), .number(2), .number(3), .number(4)] : []
13 |
14 | var body: some View {
15 | VStack {
16 | HStack {
17 | Button("Save", action: savePath)
18 | .disabled(savedPath == path)
19 | Button("Restore", action: restorePath)
20 | .disabled(savedPath == nil)
21 | }
22 | NBNavigationStack(path: $path) {
23 | HomeView()
24 | .nbNavigationDestination(for: Screen.self, destination: { screen in
25 | switch screen {
26 | case let .numberList(numberList):
27 | NumberListView(numberList: numberList)
28 | case let .number(number):
29 | NumberView(number: number)
30 | case let .visualisation(visualisation):
31 | EmojiView(visualisation: visualisation)
32 | }
33 | })
34 | }
35 | }
36 | }
37 |
38 | func savePath() {
39 | savedPath = path
40 | }
41 |
42 | func restorePath() {
43 | guard let savedPath = savedPath else { return }
44 | path = savedPath
45 | }
46 | }
47 |
48 | private struct HomeView: View {
49 | @EnvironmentObject var navigator: Navigator
50 | @State var isPushing = false
51 | @State private var trafficLight: TrafficLight?
52 |
53 | var body: some View {
54 | ScrollView {
55 | VStack(spacing: 8) {
56 | // Push via NBNavigationLink
57 | NBNavigationLink(value: Screen.numberList(NumberList(range: 0 ..< 10)), label: { Text("Pick a number") })
58 | // Push via navigator
59 | Button("99 Red balloons", action: show99RedBalloons)
60 | // Push via Bool binding
61 | Button("Push local destination", action: { isPushing = true }).disabled(isPushing)
62 | // Push via Optional Hashable binding
63 | Button("Push traffic lights", action: {
64 | trafficLight = TrafficLight.allCases.first
65 | })
66 | }
67 | }
68 | .nbNavigationDestination(isPresented: $isPushing) {
69 | Text("Local destination")
70 | }
71 | .nbNavigationDestination(item: $trafficLight) { trafficLight in
72 | Text(String(describing: trafficLight))
73 | .foregroundColor(trafficLight.color)
74 | .onTapGesture { self.trafficLight = trafficLight.next }
75 | }
76 | .navigationTitle("Home")
77 | }
78 |
79 | func show99RedBalloons() {
80 | navigator.push(.number(99))
81 | navigator.push(.visualisation(EmojiVisualisation(emoji: "🎈", count: 99)))
82 | }
83 | }
84 |
85 | private struct NumberListView: View {
86 | let numberList: NumberList
87 | var body: some View {
88 | List {
89 | ForEach(numberList.range, id: \.self) { number in
90 | NBNavigationLink("\(number)", value: Screen.number(number))
91 | .buttonStyle(.navigationLinkRowStyle)
92 | }
93 | }
94 | .navigationTitle("List")
95 | }
96 | }
97 |
98 | private struct NumberView: View {
99 | @EnvironmentObject var navigator: Navigator
100 | @State var number: Int
101 |
102 | var body: some View {
103 | ScrollView {
104 | VStack(spacing: 8) {
105 | Text("\(number)").font(.title)
106 | #if os(tvOS)
107 | #else
108 | Stepper(
109 | label: { Text("\(number)") },
110 | onIncrement: { number += 1 },
111 | onDecrement: { number -= 1 }
112 | ).labelsHidden()
113 | #endif
114 | NBNavigationLink(
115 | value: Screen.number(number + 1),
116 | label: { Text("Show next number") }
117 | )
118 | NBNavigationLink(
119 | value: Screen.visualisation(.init(emoji: "🐑", count: number)),
120 | label: { Text("Visualise with sheep") }
121 | )
122 | Button("Go back to root", action: { navigator.popToRoot() })
123 | }
124 | }
125 | .navigationTitle("\(number)")
126 | }
127 | }
128 |
129 | private struct EmojiView: View {
130 | let visualisation: EmojiVisualisation
131 |
132 | var body: some View {
133 | Text(visualisation.text)
134 | .navigationTitle("Visualise \(visualisation.count)")
135 | }
136 | }
137 |
--------------------------------------------------------------------------------
/NavigationBackportApp/Shared/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/NavigationBackportApp/Shared/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "scale" : "2x",
6 | "size" : "20x20"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "scale" : "3x",
11 | "size" : "20x20"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "scale" : "2x",
16 | "size" : "29x29"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "scale" : "3x",
21 | "size" : "29x29"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "scale" : "2x",
26 | "size" : "40x40"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "scale" : "3x",
31 | "size" : "40x40"
32 | },
33 | {
34 | "idiom" : "iphone",
35 | "scale" : "2x",
36 | "size" : "60x60"
37 | },
38 | {
39 | "idiom" : "iphone",
40 | "scale" : "3x",
41 | "size" : "60x60"
42 | },
43 | {
44 | "idiom" : "ipad",
45 | "scale" : "1x",
46 | "size" : "20x20"
47 | },
48 | {
49 | "idiom" : "ipad",
50 | "scale" : "2x",
51 | "size" : "20x20"
52 | },
53 | {
54 | "idiom" : "ipad",
55 | "scale" : "1x",
56 | "size" : "29x29"
57 | },
58 | {
59 | "idiom" : "ipad",
60 | "scale" : "2x",
61 | "size" : "29x29"
62 | },
63 | {
64 | "idiom" : "ipad",
65 | "scale" : "1x",
66 | "size" : "40x40"
67 | },
68 | {
69 | "idiom" : "ipad",
70 | "scale" : "2x",
71 | "size" : "40x40"
72 | },
73 | {
74 | "idiom" : "ipad",
75 | "scale" : "2x",
76 | "size" : "76x76"
77 | },
78 | {
79 | "idiom" : "ipad",
80 | "scale" : "2x",
81 | "size" : "83.5x83.5"
82 | },
83 | {
84 | "idiom" : "ios-marketing",
85 | "scale" : "1x",
86 | "size" : "1024x1024"
87 | },
88 | {
89 | "idiom" : "mac",
90 | "scale" : "1x",
91 | "size" : "16x16"
92 | },
93 | {
94 | "idiom" : "mac",
95 | "scale" : "2x",
96 | "size" : "16x16"
97 | },
98 | {
99 | "idiom" : "mac",
100 | "scale" : "1x",
101 | "size" : "32x32"
102 | },
103 | {
104 | "idiom" : "mac",
105 | "scale" : "2x",
106 | "size" : "32x32"
107 | },
108 | {
109 | "idiom" : "mac",
110 | "scale" : "1x",
111 | "size" : "128x128"
112 | },
113 | {
114 | "idiom" : "mac",
115 | "scale" : "2x",
116 | "size" : "128x128"
117 | },
118 | {
119 | "idiom" : "mac",
120 | "scale" : "1x",
121 | "size" : "256x256"
122 | },
123 | {
124 | "idiom" : "mac",
125 | "scale" : "2x",
126 | "size" : "256x256"
127 | },
128 | {
129 | "idiom" : "mac",
130 | "scale" : "1x",
131 | "size" : "512x512"
132 | },
133 | {
134 | "idiom" : "mac",
135 | "scale" : "2x",
136 | "size" : "512x512"
137 | }
138 | ],
139 | "info" : {
140 | "author" : "xcode",
141 | "version" : 1
142 | }
143 | }
144 |
--------------------------------------------------------------------------------
/NavigationBackportApp/Shared/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/NavigationBackportApp/Shared/ContentView.swift:
--------------------------------------------------------------------------------
1 | import NavigationBackport
2 | import SwiftUI
3 |
4 | struct EmojiVisualisation: Hashable, Codable {
5 | let emoji: String
6 | let count: Int
7 |
8 | var text: String {
9 | Array(repeating: emoji, count: count).joined()
10 | }
11 | }
12 |
13 | struct NumberList: Hashable, Codable {
14 | let range: Range
15 | }
16 |
17 | class ClassDestination {
18 | let data: String
19 |
20 | init(data: String) {
21 | self.data = data
22 | }
23 | }
24 |
25 | extension ClassDestination: Hashable {
26 | static func == (lhs: ClassDestination, rhs: ClassDestination) -> Bool {
27 | lhs.data == rhs.data
28 | }
29 |
30 | func hash(into hasher: inout Hasher) {
31 | hasher.combine(data)
32 | }
33 | }
34 |
35 | class SampleClassDestination: ClassDestination {
36 | init() { super.init(data: "Sample data") }
37 | }
38 |
39 | struct ContentView: View {
40 | var body: some View {
41 | TabView {
42 | NoBindingView()
43 | .tabItem { Text("NoBinding") }
44 | NBNavigationPathView()
45 | .tabItem { Text("NBNavigationPath") }
46 | ArrayBindingView()
47 | .tabItem { Text("ArrayBinding") }
48 | LeakTestView()
49 | .tabItem { Text("LeakTest") }
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/NavigationBackportApp/Shared/LeakTest/LeakTestView.swift:
--------------------------------------------------------------------------------
1 | import NavigationBackport
2 | import SwiftUI
3 |
4 | struct LeakTestView: View {
5 | @State private var path: [Presentable] = []
6 |
7 | var body: some View {
8 | NBNavigationStack(path: $path) {
9 | root.nbNavigationDestination(for: Presentable.self) { presentable in
10 | presentable
11 | }
12 | }
13 | }
14 |
15 | private var root: some View {
16 | Button("Main") {
17 | path.append(Presentable(MainView(viewModel: MainViewModel())))
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/NavigationBackportApp/Shared/LeakTest/MainView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct MainView: View {
4 | @StateObject private var viewModel: MainViewModel
5 |
6 | /// When passing an instance of viewModel through the initializer, there is a problem with memory leaks.
7 | init(viewModel: MainViewModel) {
8 | _viewModel = StateObject(wrappedValue: viewModel)
9 | }
10 |
11 | var body: some View {
12 | VStack {
13 | Text("Main")
14 | Text("Count: \(MainViewModel.count)")
15 | }
16 | .navigationTitle("Main")
17 | }
18 | }
19 |
20 | final class MainViewModel: ObservableObject {
21 | static var count = 0
22 |
23 | init() {
24 | Self.count += 1
25 | }
26 |
27 | deinit {
28 | Self.count -= 1
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/NavigationBackportApp/Shared/LeakTest/Presentable.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct Presentable: View {
4 | let id = UUID()
5 | let content: AnyView
6 |
7 | init(_ content: some View) {
8 | self.content = AnyView(content)
9 | }
10 |
11 | var body: some View {
12 | content
13 | }
14 | }
15 |
16 | extension Presentable: Hashable {
17 | static func == (lhs: Presentable, rhs: Presentable) -> Bool {
18 | lhs.id == rhs.id
19 | }
20 |
21 | func hash(into hasher: inout Hasher) {
22 | hasher.combine(id)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/NavigationBackportApp/Shared/NBNavigationPathView.swift:
--------------------------------------------------------------------------------
1 | import NavigationBackport
2 | import SwiftUI
3 |
4 | struct NBNavigationPathView: View {
5 | @State var encodedPathData: Data?
6 | @State var path = NBNavigationPath(ProcessArguments.nonEmptyAtLaunch ? [1, 2, 3, 4] : [])
7 |
8 | var body: some View {
9 | VStack {
10 | HStack {
11 | Button("Encode", action: encodePath)
12 | .disabled(try! encodedPathData == JSONEncoder().encode(path.codable))
13 | Button("Decode", action: decodePath)
14 | .disabled(encodedPathData == nil)
15 | }
16 | NBNavigationStack(path: $path) {
17 | HomeView()
18 | .nbNavigationDestination(for: NumberList.self, destination: { numberList in
19 | NumberListView(numberList: numberList)
20 | })
21 | .nbNavigationDestination(for: Int.self, destination: { number in
22 | NumberView(number: number)
23 | })
24 | .nbNavigationDestination(for: EmojiVisualisation.self, destination: { visualisation in
25 | EmojiView(visualisation: visualisation)
26 | })
27 | .nbNavigationDestination(for: ClassDestination.self, destination: { destination in
28 | ClassDestinationView(destination: destination)
29 | })
30 | }
31 | }
32 | }
33 |
34 | func encodePath() {
35 | guard let codable = path.codable else {
36 | return
37 | }
38 | encodedPathData = try! JSONEncoder().encode(codable)
39 | }
40 |
41 | func decodePath() {
42 | guard let encodedPathData = encodedPathData else {
43 | return
44 | }
45 | let codable = try! JSONDecoder().decode(NBNavigationPath.CodableRepresentation.self, from: encodedPathData)
46 | path = NBNavigationPath(codable)
47 | }
48 | }
49 |
50 | private struct HomeView: View {
51 | @EnvironmentObject var navigator: PathNavigator
52 | @State var isPushing = false
53 | @State private var trafficLight: TrafficLight?
54 |
55 | var body: some View {
56 | ScrollView {
57 | VStack(spacing: 8) {
58 | // Push via link
59 | NBNavigationLink(value: NumberList(range: 0 ..< 10), label: { Text("Pick a number") })
60 | // Push via navigator
61 | Button("99 Red balloons", action: show99RedBalloons)
62 | // Push child class via navigator
63 | Button("Show Class Destination", action: showClassDestination)
64 | // Push via Bool binding
65 | Button("Push local destination", action: { isPushing = true }).disabled(isPushing)
66 | // Push via Optional Hashable binding
67 | Button("Push traffic lights", action: {
68 | trafficLight = TrafficLight.allCases.first
69 | })
70 | }
71 | }
72 | .nbNavigationDestination(isPresented: $isPushing) {
73 | Text("Local destination")
74 | }
75 | .nbNavigationDestination(item: $trafficLight) { trafficLight in
76 | Text(String(describing: trafficLight))
77 | .foregroundColor(trafficLight.color)
78 | .onTapGesture { self.trafficLight = trafficLight.next }
79 | }
80 | .navigationTitle("Home")
81 | }
82 |
83 | func show99RedBalloons() {
84 | navigator.push(99)
85 | navigator.push(EmojiVisualisation(emoji: "🎈", count: 99))
86 | }
87 |
88 | func showClassDestination() {
89 | navigator.push(SampleClassDestination())
90 | }
91 | }
92 |
93 | private struct NumberListView: View {
94 | let numberList: NumberList
95 | var body: some View {
96 | List {
97 | ForEach(numberList.range, id: \.self) { number in
98 | NBNavigationLink("\(number)", value: number)
99 | .buttonStyle(.navigationLinkRowStyle)
100 | }
101 | }.navigationTitle("List")
102 | }
103 | }
104 |
105 | private struct NumberView: View {
106 | @EnvironmentObject var navigator: PathNavigator
107 | @State var number: Int
108 |
109 | var body: some View {
110 | ScrollView {
111 | VStack(spacing: 8) {
112 | Text("\(number)").font(.title)
113 | #if os(tvOS)
114 | #else
115 | Stepper(
116 | label: { Text("\(number)") },
117 | onIncrement: { number += 1 },
118 | onDecrement: { number -= 1 }
119 | ).labelsHidden()
120 | #endif
121 | NBNavigationLink(
122 | value: number + 1,
123 | label: { Text("Show next number") }
124 | )
125 | NBNavigationLink(
126 | value: EmojiVisualisation(emoji: "🐑", count: number),
127 | label: { Text("Visualise with sheep") }
128 | )
129 | Button("Go back to root", action: { navigator.popToRoot() })
130 | }
131 | }
132 | .navigationTitle("\(number)")
133 | }
134 | }
135 |
136 | private struct EmojiView: View {
137 | let visualisation: EmojiVisualisation
138 |
139 | var body: some View {
140 | Text(visualisation.text)
141 | .navigationTitle("Visualise \(visualisation.count)")
142 | }
143 | }
144 |
145 | private struct ClassDestinationView: View {
146 | let destination: ClassDestination
147 |
148 | var body: some View {
149 | Text(destination.data)
150 | .navigationTitle("A ClassDestination")
151 | }
152 | }
153 |
--------------------------------------------------------------------------------
/NavigationBackportApp/Shared/NavigationBackportApp.swift:
--------------------------------------------------------------------------------
1 | import NavigationBackport
2 | import SwiftUI
3 |
4 | @main
5 | struct NavigationBackportApp: App {
6 | var body: some Scene {
7 | WindowGroup {
8 | ContentView()
9 | .nbUseNavigationStack(ProcessArguments.navigationStackPolicy)
10 | }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/NavigationBackportApp/Shared/NoBindingView.swift:
--------------------------------------------------------------------------------
1 | import NavigationBackport
2 | import SwiftUI
3 |
4 | struct NoBindingView: View {
5 | var body: some View {
6 | NBNavigationStack {
7 | HomeView()
8 | .nbNavigationDestination(for: NumberList.self, destination: { numberList in
9 | NumberListView(numberList: numberList)
10 | })
11 | .nbNavigationDestination(for: Int.self, destination: { number in
12 | NumberView(number: number)
13 | })
14 | .nbNavigationDestination(for: EmojiVisualisation.self, destination: { visualisation in
15 | EmojiView(visualisation: visualisation)
16 | })
17 | .nbNavigationDestination(for: ClassDestination.self, destination: { destination in
18 | ClassDestinationView(destination: destination)
19 | })
20 | }
21 | }
22 | }
23 |
24 | private struct HomeView: View {
25 | @EnvironmentObject var navigator: PathNavigator
26 | @State var isPushing = false
27 | @State private var trafficLight: TrafficLight?
28 |
29 | var body: some View {
30 | ScrollView {
31 | VStack(spacing: 8) {
32 | // Push via link
33 | NBNavigationLink(value: NumberList(range: 0 ..< 10), label: { Text("Pick a number") })
34 | // Push via navigator
35 | Button("99 Red balloons", action: show99RedBalloons)
36 | // Push child class via navigator
37 | Button("Show Class Destination", action: showClassDestination)
38 | // Push via Bool binding
39 | Button("Push local destination", action: { isPushing = true }).disabled(isPushing)
40 | // Push via Optional Hashable binding
41 | Button("Push traffic lights", action: {
42 | trafficLight = TrafficLight.allCases.first
43 | })
44 | }
45 | }
46 | .nbNavigationDestination(isPresented: $isPushing) {
47 | Text("Local destination")
48 | }
49 | .nbNavigationDestination(item: $trafficLight) { trafficLight in
50 | Text(String(describing: trafficLight))
51 | .foregroundColor(trafficLight.color)
52 | .onTapGesture { self.trafficLight = trafficLight.next }
53 | }
54 | .navigationTitle("Home")
55 | }
56 |
57 | func show99RedBalloons() {
58 | navigator.push(99)
59 | navigator.push(EmojiVisualisation(emoji: "🎈", count: 99))
60 | }
61 |
62 | func showClassDestination() {
63 | navigator.push(SampleClassDestination())
64 | }
65 | }
66 |
67 | private struct NumberListView: View {
68 | let numberList: NumberList
69 | var body: some View {
70 | List {
71 | ForEach(numberList.range, id: \.self) { number in
72 | NBNavigationLink("\(number)", value: number)
73 | .buttonStyle(.navigationLinkRowStyle)
74 | }
75 | }.navigationTitle("List")
76 | }
77 | }
78 |
79 | private struct NumberView: View {
80 | @EnvironmentObject var navigator: PathNavigator
81 | @State var number: Int
82 |
83 | var body: some View {
84 | ScrollView {
85 | VStack(spacing: 8) {
86 | Text("\(number)").font(.title)
87 | #if os(tvOS)
88 | #else
89 | Stepper(
90 | label: { Text("\(number)") },
91 | onIncrement: { number += 1 },
92 | onDecrement: { number -= 1 }
93 | ).labelsHidden()
94 | #endif
95 | NBNavigationLink(
96 | value: number + 1,
97 | label: { Text("Show next number") }
98 | )
99 | NBNavigationLink(
100 | value: EmojiVisualisation(emoji: "🐑", count: number),
101 | label: { Text("Visualise with sheep") }
102 | )
103 | Button("Go back to root") {
104 | navigator.popToRoot()
105 | }
106 | }
107 | }
108 | .navigationTitle("\(number)")
109 | }
110 | }
111 |
112 | private struct EmojiView: View {
113 | let visualisation: EmojiVisualisation
114 |
115 | var body: some View {
116 | Text(visualisation.text)
117 | .navigationTitle("Visualise \(visualisation.count)")
118 | }
119 | }
120 |
121 | private struct ClassDestinationView: View {
122 | let destination: ClassDestination
123 |
124 | var body: some View {
125 | Text(destination.data)
126 | .navigationTitle("A ClassDestination")
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/NavigationBackportApp/Shared/ProcessArguments.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import NavigationBackport
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 |
10 | static var nonEmptyAtLaunch: Bool {
11 | // Allows initial path to be set from UI tests.
12 | ProcessInfo.processInfo.arguments.contains("NON_EMPTY_AT_LAUNCH")
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/NavigationBackportApp/Shared/TrafficLight.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | enum TrafficLight: Int, CaseIterable {
4 | case red, amber, green
5 |
6 | var next: TrafficLight? {
7 | TrafficLight(rawValue: rawValue + 1)
8 | }
9 |
10 | var color: Color {
11 | switch self {
12 | case .red: return .red
13 | case .amber: return .orange
14 | case .green: return .green
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/NavigationBackportApp/macOS/macOS.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.app-sandbox
6 |
7 | com.apple.security.files.user-selected.read-only
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/NavigationBackportApp/run_ui_tests.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | xcodebuild \
4 | -project NavigationBackportApp.xcodeproj \
5 | -scheme "NavigationBackportApp (iOS)" \
6 | -sdk iphonesimulator \
7 | -destination 'platform=iOS Simulator,name=iPhone 12,OS=14.5' \
8 | test && \
9 | xcodebuild \
10 | -project NavigationBackportApp.xcodeproj \
11 | -scheme "NavigationBackportApp (iOS)" \
12 | -sdk iphonesimulator \
13 | -destination 'platform=iOS Simulator,name=iPhone 13,OS=15.5' \
14 | test && \
15 | xcodebuild \
16 | -project NavigationBackportApp.xcodeproj \
17 | -scheme "NavigationBackportApp (iOS)" \
18 | -sdk iphonesimulator \
19 | -destination 'platform=iOS Simulator,name=iPhone 14,OS=16.2' \
20 | test
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 5.6
2 |
3 | import PackageDescription
4 |
5 | let package = Package(
6 | name: "NavigationBackport",
7 | platforms: [
8 | .iOS(.v14), .watchOS(.v7), .macOS(.v11), .tvOS(.v14),
9 | ],
10 | products: [
11 | .library(
12 | name: "NavigationBackport",
13 | targets: ["NavigationBackport"]
14 | ),
15 | ],
16 | dependencies: [],
17 | targets: [
18 | .target(
19 | name: "NavigationBackport",
20 | dependencies: []
21 | ),
22 | .testTarget(
23 | name: "NavigationBackportTests",
24 | dependencies: ["NavigationBackport"]
25 | ),
26 | ]
27 | )
28 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Navigation Backport
2 |
3 | This package uses the navigation APIs available in older SwiftUI versions (such as `NavigationView` and `NavigationLink`) to recreate the new `NavigationStack` APIs introduced in WWDC22, so that you can start targeting those APIs on older versions of iOS, tvOS, macOS and watchOS. When running on an OS version that supports `NavigationStack`, `NavigationStack` will be used under the hood.
4 |
5 | ✅ `NavigationStack` -> `NBNavigationStack`
6 |
7 | ✅ `NavigationLink` -> `NBNavigationLink`
8 |
9 | ✅ `NavigationPath` -> `NBNavigationPath`
10 |
11 | ✅ `navigationDestination` -> `nbNavigationDestination`
12 |
13 | ✅ `NavigationPath.CodableRepresentation` -> `NBNavigationPath.CodableRepresentation`
14 |
15 |
16 | You can migrate to these APIs now, and when you eventually bump your deployment target, you can remove this library and easily migrate to its SwiftUI equivalent. `NavigationStack`'s full API is replicated, so you can initialise an `NBNavigationStack` with a binding to an `Array`, with a binding to an `NBNavigationPath` binding, or with no binding at all.
17 |
18 | ## Example
19 |
20 |
21 | Click to expand an example
22 |
23 | ```swift
24 | import NavigationBackport
25 | import SwiftUI
26 |
27 | struct ContentView: View {
28 | @State var path = NBNavigationPath()
29 |
30 | var body: some View {
31 | NBNavigationStack(path: $path) {
32 | HomeView()
33 | .nbNavigationDestination(for: NumberList.self, destination: { numberList in
34 | NumberListView(numberList: numberList)
35 | })
36 | .nbNavigationDestination(for: Int.self, destination: { number in
37 | NumberView(number: number)
38 | })
39 | .nbNavigationDestination(for: EmojiVisualisation.self, destination: { visualisation in
40 | EmojiView(visualisation: visualisation)
41 | })
42 | }
43 | }
44 | }
45 |
46 | struct HomeView: View {
47 | var body: some View {
48 | VStack(spacing: 8) {
49 | NBNavigationLink(value: NumberList(range: 0 ..< 100), label: { Text("Pick a number") })
50 | }.navigationTitle("Home")
51 | }
52 | }
53 |
54 | struct NumberList: Hashable {
55 | let range: Range
56 | }
57 |
58 | struct NumberListView: View {
59 | let numberList: NumberList
60 | var body: some View {
61 | List {
62 | ForEach(numberList.range, id: \.self) { number in
63 | NBNavigationLink("\(number)", value: number)
64 | }
65 | }.navigationTitle("List")
66 | }
67 | }
68 |
69 | struct NumberView: View {
70 | @EnvironmentObject var navigator: PathNavigator
71 |
72 | let number: Int
73 |
74 | var body: some View {
75 | VStack(spacing: 8) {
76 | Text("\(number)")
77 | NBNavigationLink(
78 | value: number + 1,
79 | label: { Text("Show next number") }
80 | )
81 | NBNavigationLink(
82 | value: EmojiVisualisation(emoji: "🐑", count: number),
83 | label: { Text("Visualise with sheep") }
84 | )
85 | Button("Go back to root", action: { navigator.popToRoot() })
86 | }.navigationTitle("\(number)")
87 | }
88 | }
89 |
90 | struct EmojiVisualisation: Hashable {
91 | let emoji: String
92 | let count: Int
93 |
94 | var text: String {
95 | Array(repeating: emoji, count: count).joined()
96 | }
97 | }
98 |
99 | struct EmojiView: View {
100 | let visualisation: EmojiVisualisation
101 |
102 | var body: some View {
103 | Text(visualisation.text)
104 | .navigationTitle("Visualise \(visualisation.count)")
105 | }
106 | }
107 | ```
108 |
109 |
110 |
111 | ## Additional features
112 |
113 | As well as replicating the standard features of the new `NavigationStack` APIs, some helpful utilities have also been added.
114 |
115 | ### Navigator
116 |
117 | A `Navigator` object is available through the environment, giving access to the current navigation path. The navigator can be accessed via the environment, e.g. for a NBNavigationPath-backed stack:
118 |
119 | ```swift
120 | @EnvironmentObject var navigator: PathNavigator
121 | ```
122 |
123 | Or for a stack backed by an Array, e.g. `[ScreenType]`:
124 |
125 | ```swift
126 | @EnvironmentObject var navigator: Navigator
127 | ```
128 |
129 | As well as allowing you to inspect the path elements, the navigator can be used to push new screens, pop, pop to a specific screen or pop to the root.
130 |
131 | ### Navigation functions
132 |
133 | Whether interacting with an `Array`, an `NBNavigationPath`, or a `Navigator`, a number of utility functions are available for easier navigation, such as:
134 |
135 | ```swift
136 | path.push(Profile(name: "John"))
137 |
138 | path.pop()
139 |
140 | path.popToRoot()
141 |
142 | path.popTo(Profile.self)
143 | ```
144 |
145 | Note that, if you want to use these methods on an `Array`, ensure the `Array`'s `Element` conforms to `NBScreen`, a protocol that inherits from Hashable without adding any additional requirements. This avoids polluting all arrays with APIs specific to navigation.
146 |
147 | ## Deep-linking
148 |
149 | Before `NavigationStack`, SwiftUI did not support pushing more than one screen in a single state update, e.g. when deep-linking to a screen multiple layers deep in a navigation hierarchy. `NavigationBackport` works around this limitation: you can make any such path changes, and the library will, behind the scenes, break down the larger update into a series of smaller updates that SwiftUI supports if necessary, with delays in between. For example, the following code that pushes three screens in one state update will push the screens one by one if needed:
150 |
151 | ```swift
152 | path.append(Screen.orders)
153 | path.append(Screen.editOrder(id: id))
154 | path.append(Screen.confirmChanges(orderId: id))
155 | ```
156 |
157 | This only happens when necessary: on versions of SwiftUI that support `NavigationStack`, all three screens will be pushed successfully in one update.
158 |
159 | ## Support for iOS/tvOS 13
160 |
161 | This library targets iOS/tvOS versions 14 and above, since it uses `StateObject`, which is unavailable on iOS/tvOS 13. However, there is an `ios13` branch, which uses [SwiftUIBackports](https://github.com/shaps80/SwiftUIBackports)' backported StateObject, so that it works on iOS/tvOS 13 too.
162 |
163 | ## FlowStacks
164 |
165 | Want to further upgrade your navigation APIs? [FlowStacks](https://github.com/johnpatrickmorgan/FlowStacks) enhances these familiar APIs to allow you to additionally drive sheet and full-screen cover navigation from a single unified interface.
166 |
167 | -------
168 |
169 | [](https://ko-fi.com/T6T114GWOT)
170 |
--------------------------------------------------------------------------------
/Sources/NavigationBackport/Array+utilities.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// Various utilities for pushing and popping.
4 | public extension Array where Element: NBScreen {
5 | /// Pushes a new screen.
6 | /// - Parameter screen: The screen to push.
7 | mutating func push(_ screen: Element) {
8 | append(screen)
9 | }
10 |
11 | /// Pops a given number of screens off the stack.
12 | /// - Parameter count: The number of screens to pop. Defaults to 1.
13 | mutating func pop(_ count: Int = 1) {
14 | assert(
15 | self.count - count >= 0,
16 | "Can't pop\(count == 1 ? "" : " \(count) screens") - the screen count is \(self.count)"
17 | )
18 | assert(
19 | count >= 0,
20 | "Can't pop \(count) screens - count must be positive"
21 | )
22 | guard self.count - count >= 0, count >= 0 else { return }
23 | removeLast(count)
24 | }
25 |
26 | /// Pops to a given index in the array of screens. The resulting screen count
27 | /// will be index.
28 | /// - Parameter index: The index that should become the Array's endIndex.
29 | mutating func popTo(index: Int) {
30 | let popCount = count - 1 - index
31 | pop(popCount)
32 | }
33 |
34 | /// Pops to the root screen. The resulting screen count will be 0.
35 | mutating func popToRoot() {
36 | // Popping to index -1 ensures the resulting array is empty.
37 | popTo(index: -1)
38 | }
39 |
40 | /// Pops to the topmost (most recently pushed) screen in the stack
41 | /// that satisfies the given condition. If no screens satisfy the condition,
42 | /// the screens array will be unchanged.
43 | /// - Parameter condition: The predicate indicating which screen to pop to.
44 | /// - Returns: A `Bool` indicating whether a screen was found.
45 | @discardableResult
46 | mutating func popTo(where condition: (Element) -> Bool) -> Bool {
47 | guard let index = lastIndex(where: condition) else {
48 | return false
49 | }
50 | popTo(index: index)
51 | return true
52 | }
53 | }
54 |
55 | public extension Array where Element: NBScreen & Equatable {
56 | /// Pops to the topmost (most recently pushed) screen in the stack
57 | /// equal to the given screen. If no screens are found,
58 | /// the screens array will be unchanged.
59 | /// - Parameter screen: The predicate indicating which screen to go back to.
60 | /// - Returns: A `Bool` indicating whether a matching screen was found.
61 | @discardableResult
62 | mutating func popTo(_ screen: Element) -> Bool {
63 | popTo(where: { $0 == screen })
64 | }
65 | }
66 |
67 | public extension Array where Element: NBScreen & Identifiable {
68 | /// Pops to the topmost (most recently pushed) identifiable screen in the stack
69 | /// with the given ID. If no screens are found, the screens array will be unchanged.
70 | /// - Parameter id: The id of the screen to goBack to.
71 | /// - Returns: A `Bool` indicating whether a matching screen was found.
72 | @discardableResult
73 | mutating func popTo(id: Element.ID) -> Bool {
74 | popTo(where: { $0.id == id })
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/Sources/NavigationBackport/Binding+withDelaysIfUnsupported.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import SwiftUI
3 |
4 | /// The time it takes for a push or pop operation, during which time another push or pop operation would be problematic.
5 | let navigationDelay = 0.65
6 |
7 | public extension Binding where Value: Collection {
8 | /// Any changes can be made to the screens array passed to the transform closure. If those
9 | /// changes are not supported within a single update by SwiftUI, the changes will be
10 | /// applied in stages.
11 | @_disfavoredOverload
12 | @MainActor
13 | func withDelaysIfUnsupported(_ transform: (inout [Screen]) -> Void, onCompletion: (() -> Void)? = nil) where Value == [Screen] {
14 | let start = wrappedValue
15 | let end = apply(transform, to: start)
16 |
17 | let didUpdateSynchronously = synchronouslyUpdateIfSupported(from: start, to: end)
18 | guard !didUpdateSynchronously else { return }
19 |
20 | Task { @MainActor in
21 | await withDelaysIfUnsupported(from: start, to: end, keyPath: \.self)
22 | onCompletion?()
23 | }
24 | }
25 |
26 | /// Any changes can be made to the screens array passed to the transform closure. If those
27 | /// changes are not supported within a single update by SwiftUI, the changes will be
28 | /// applied in stages.
29 | @MainActor
30 | func withDelaysIfUnsupported(_ transform: (inout [Screen]) -> Void) async where Value == [Screen] {
31 | let start = wrappedValue
32 | let end = apply(transform, to: start)
33 |
34 | let didUpdateSynchronously = synchronouslyUpdateIfSupported(from: start, to: end)
35 | guard !didUpdateSynchronously else { return }
36 |
37 | await withDelaysIfUnsupported(from: start, to: end, keyPath: \.self)
38 | }
39 |
40 | fileprivate func synchronouslyUpdateIfSupported(from start: [Screen], to end: [Screen]) -> Bool where Value == [Screen] {
41 | guard NavigationBackport.canSynchronouslyUpdate(from: start, to: end) else {
42 | return false
43 | }
44 | wrappedValue = end
45 | return true
46 | }
47 | }
48 |
49 | public extension Binding where Value == NBNavigationPath {
50 | /// Any changes can be made to the screens array passed to the transform closure. If those
51 | /// changes are not supported within a single update by SwiftUI, the changes will be
52 | /// applied in stages.
53 | @_disfavoredOverload
54 | @MainActor
55 | func withDelaysIfUnsupported(_ transform: (inout NBNavigationPath) -> Void, onCompletion: (() -> Void)? = nil) {
56 | let start = wrappedValue
57 | let end = apply(transform, to: start)
58 |
59 | let didUpdateSynchronously = synchronouslyUpdateIfSupported(from: start.elements, to: end.elements)
60 | guard !didUpdateSynchronously else { return }
61 |
62 | Task { @MainActor in
63 | await withDelaysIfUnsupported(from: start.elements, to: end.elements, keyPath: \.elements)
64 | onCompletion?()
65 | }
66 | }
67 |
68 | /// Any changes can be made to the screens array passed to the transform closure. If those
69 | /// changes are not supported within a single update by SwiftUI, the changes will be
70 | /// applied in stages.
71 | @MainActor
72 | func withDelaysIfUnsupported(_ transform: (inout Value) -> Void) async {
73 | let start = wrappedValue
74 | let end = apply(transform, to: start)
75 |
76 | let didUpdateSynchronously = synchronouslyUpdateIfSupported(from: start.elements, to: end.elements)
77 | guard !didUpdateSynchronously else { return }
78 |
79 | await withDelaysIfUnsupported(from: start.elements, to: end.elements, keyPath: \.elements)
80 | }
81 |
82 | fileprivate func synchronouslyUpdateIfSupported(from start: [AnyHashable], to end: [AnyHashable]) -> Bool {
83 | guard NavigationBackport.canSynchronouslyUpdate(from: start, to: end) else {
84 | return false
85 | }
86 | wrappedValue.elements = end
87 | return true
88 | }
89 | }
90 |
91 | extension Binding {
92 | @MainActor
93 | func withDelaysIfUnsupported(from start: [Screen], to end: [Screen], keyPath: WritableKeyPath) async {
94 | let steps = NavigationBackport.calculateSteps(from: start, to: end)
95 |
96 | wrappedValue[keyPath: keyPath] = steps.first!
97 | await scheduleRemainingSteps(steps: Array(steps.dropFirst()), keyPath: keyPath)
98 | }
99 |
100 | @MainActor
101 | func scheduleRemainingSteps(steps: [[Screen]], keyPath: WritableKeyPath) async {
102 | guard let firstStep = steps.first else {
103 | return
104 | }
105 | wrappedValue[keyPath: keyPath] = firstStep
106 | do {
107 | try await Task.sleep(nanoseconds: UInt64(navigationDelay * 1_000_000_000))
108 | await scheduleRemainingSteps(steps: Array(steps.dropFirst()), keyPath: keyPath)
109 | } catch {}
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/Sources/NavigationBackport/CodableRepresentation.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public extension NBNavigationPath {
4 | /// A codable representation of a navigation path.
5 | struct CodableRepresentation {
6 | static let encoder = JSONEncoder()
7 | static let decoder = JSONDecoder()
8 |
9 | var elements: [Codable]
10 | }
11 |
12 | var codable: CodableRepresentation? {
13 | let codableElements = elements.compactMap { $0 as? Codable }
14 | guard codableElements.count == elements.count else {
15 | return nil
16 | }
17 | return CodableRepresentation(elements: codableElements)
18 | }
19 |
20 | init(_ codable: CodableRepresentation) {
21 | // NOTE: Casting to Any first prevents the compiler from flagging the cast to AnyHashable as one that
22 | // always fails (which it isn't, thanks to the compiler magic around AnyHashable).
23 | self.init(codable.elements.map { ($0 as Any) as! AnyHashable })
24 | }
25 | }
26 |
27 | extension NBNavigationPath.CodableRepresentation: Encodable {
28 | fileprivate func generalEncodingError(_ description: String) -> EncodingError {
29 | let context = EncodingError.Context(codingPath: [], debugDescription: description)
30 | return EncodingError.invalidValue(elements, context)
31 | }
32 |
33 | fileprivate static func encodeExistential(_ element: Encodable) throws -> Data {
34 | func encodeOpened(_ element: A) throws -> Data {
35 | try NBNavigationPath.CodableRepresentation.encoder.encode(element)
36 | }
37 | return try _openExistential(element, do: encodeOpened(_:))
38 | }
39 |
40 | /// Encodes the representation into the encoder's unkeyed container.
41 | /// - Parameter encoder: The encoder to use.
42 | public func encode(to encoder: Encoder) throws {
43 | var container = encoder.unkeyedContainer()
44 | for element in elements.reversed() {
45 | guard let typeName = _mangledTypeName(type(of: element)) else {
46 | throw generalEncodingError(
47 | "Unable to create '_mangledTypeName' from \(String(describing: type(of: element)))"
48 | )
49 | }
50 | try container.encode(typeName)
51 | #if swift(<5.7)
52 | let data = try Self.encodeExistential(element)
53 | let string = String(decoding: data, as: UTF8.self)
54 | try container.encode(string)
55 | #else
56 | let string = try String(decoding: Self.encoder.encode(element), as: UTF8.self)
57 | try container.encode(string)
58 | #endif
59 | }
60 | }
61 | }
62 |
63 | extension NBNavigationPath.CodableRepresentation: Decodable {
64 | public init(from decoder: Decoder) throws {
65 | var container = try decoder.unkeyedContainer()
66 | elements = []
67 | while !container.isAtEnd {
68 | let typeName = try container.decode(String.self)
69 | guard let type = _typeByName(typeName) else {
70 | throw DecodingError.dataCorruptedError(
71 | in: container,
72 | debugDescription: "Cannot instantiate type from name '\(typeName)'."
73 | )
74 | }
75 | guard let codableType = type as? Codable.Type else {
76 | throw DecodingError.dataCorruptedError(
77 | in: container,
78 | debugDescription: "\(typeName) does not conform to Codable."
79 | )
80 | }
81 | let encodedValue = try container.decode(String.self)
82 | let data = Data(encodedValue.utf8)
83 | #if swift(<5.7)
84 | func decodeExistential(type: Codable.Type) throws -> Codable {
85 | func decodeOpened(type _: A.Type) throws -> A {
86 | try NBNavigationPath.CodableRepresentation.decoder.decode(A.self, from: data)
87 | }
88 | return try _openExistential(type, do: decodeOpened)
89 | }
90 | let value = try decodeExistential(type: codableType)
91 | #else
92 | let value = try Self.decoder.decode(codableType, from: data)
93 | #endif
94 | elements.insert(value, at: 0)
95 | }
96 | }
97 | }
98 |
99 | extension NBNavigationPath.CodableRepresentation: Equatable {
100 | public static func == (lhs: Self, rhs: Self) -> Bool {
101 | do {
102 | let encodedLhs = try encodeExistential(lhs)
103 | let encodedRhs = try encodeExistential(rhs)
104 | return encodedLhs == encodedRhs
105 | } catch {
106 | return false
107 | }
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/Sources/NavigationBackport/ConditionalViewBuilder.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | /// Builds a view given optional data and a function for transforming the data into a view.
4 | struct ConditionalViewBuilder: View {
5 | @Binding var data: Data?
6 | var buildView: (Data) -> DestinationView
7 |
8 | var body: some View {
9 | if let data {
10 | buildView(data)
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/Sources/NavigationBackport/DestinationBuilderHolder.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import SwiftUI
3 |
4 | /// Keeps hold of the destination builder closures for a given type or local destination ID.
5 | class DestinationBuilderHolder: ObservableObject {
6 | static func identifier(for type: Any.Type) -> String {
7 | String(reflecting: type)
8 | }
9 |
10 | var builders: [String: (Any) -> AnyView?] = [:]
11 |
12 | init() {
13 | builders = [:]
14 | }
15 |
16 | func appendBuilder(_ builder: @escaping (T) -> AnyView) {
17 | let key = Self.identifier(for: T.self)
18 | builders[key] = { data in
19 | if let typedData = data as? T {
20 | return builder(typedData)
21 | } else {
22 | return nil
23 | }
24 | }
25 | }
26 |
27 | func appendLocalBuilder(identifier: LocalDestinationID, _ builder: @escaping () -> AnyView) {
28 | let key = identifier.rawValue.uuidString
29 | builders[key] = { _ in builder() }
30 | }
31 |
32 | func removeLocalBuilder(identifier: LocalDestinationID) {
33 | let key = identifier.rawValue.uuidString
34 | builders[key] = nil
35 | }
36 |
37 | func build(_ typedData: T) -> AnyView {
38 | let base = (typedData as? AnyHashable)?.base
39 | if let identifier = (base ?? typedData) as? LocalDestinationID {
40 | let key = identifier.rawValue.uuidString
41 | if let builder = builders[key], let output = builder(typedData) {
42 | return output
43 | }
44 | assertionFailure("No view builder found for type \(key)")
45 | } else {
46 | var possibleMirror: Mirror? = Mirror(reflecting: base ?? typedData)
47 | while let mirror = possibleMirror {
48 | let key = Self.identifier(for: mirror.subjectType)
49 |
50 | if let builder = builders[key], let output = builder(typedData) {
51 | return output
52 | }
53 | possibleMirror = mirror.superclassMirror
54 | }
55 | assertionFailure("No view builder found for type \(type(of: base ?? typedData))")
56 | }
57 | return AnyView(Image(systemName: "exclamationmark.triangle"))
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/Sources/NavigationBackport/DestinationBuilderModifier.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import SwiftUI
3 |
4 | /// Modifier for appending a new destination builder.
5 | struct DestinationBuilderModifier: ViewModifier {
6 | let typedDestinationBuilder: DestinationBuilder
7 |
8 | @EnvironmentObject var destinationBuilder: DestinationBuilderHolder
9 |
10 | func body(content: Content) -> some View {
11 | destinationBuilder.appendBuilder(typedDestinationBuilder)
12 |
13 | return content
14 | .environmentObject(destinationBuilder)
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/Sources/NavigationBackport/DestinationBuilderView.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import SwiftUI
3 |
4 | /// Builds a view from the given Data, using the destination builder environment object.
5 | struct DestinationBuilderView: View {
6 | let data: Data
7 |
8 | @EnvironmentObject var destinationBuilder: DestinationBuilderHolder
9 |
10 | var body: some View {
11 | DataDependentView(data: data, content: { destinationBuilder.build(data) }).equatable()
12 | }
13 | }
14 |
15 | struct DataDependentView: View, Equatable {
16 | static func ==(lhs: DataDependentView, rhs: DataDependentView) -> Bool {
17 | return lhs.data == rhs.data
18 | }
19 |
20 | let data: AnyHashable
21 | let content: () -> Content
22 |
23 | var body: some View {
24 | content()
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/Sources/NavigationBackport/EnvironmentValues+navigationStack.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct UseNavigationStackPolicyKey: EnvironmentKey {
4 | static let defaultValue = UseNavigationStackPolicy.whenAvailable
5 | }
6 |
7 | struct IsWithinNavigationStackKey: EnvironmentKey {
8 | static let defaultValue = false
9 | }
10 |
11 | extension EnvironmentValues {
12 | var useNavigationStack: UseNavigationStackPolicy {
13 | get { self[UseNavigationStackPolicyKey.self] }
14 | set { self[UseNavigationStackPolicyKey.self] = newValue }
15 | }
16 |
17 | var isWithinNavigationStack: Bool {
18 | get { self[IsWithinNavigationStackKey.self] }
19 | set { self[IsWithinNavigationStackKey.self] = newValue }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Sources/NavigationBackport/LocalDestinationBuilderModifier.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import SwiftUI
3 |
4 | /// Uniquely identifies an instance of a local destination builder.
5 | struct LocalDestinationID: RawRepresentable, Hashable {
6 | let rawValue: UUID
7 | }
8 |
9 | /// Persistent object to hold the local destination ID and remove it when the destination builder is removed.
10 | class LocalDestinationIDHolder: ObservableObject {
11 | let id = LocalDestinationID(rawValue: UUID())
12 | weak var destinationBuilder: DestinationBuilderHolder?
13 |
14 | func setDestinationBuilder(_ builder: DestinationBuilderHolder) {
15 | destinationBuilder = builder
16 | }
17 |
18 | deinit {
19 | // On iOS 15, there are some extraneous re-renders after LocalDestinationBuilderModifier is removed from
20 | // the view tree. Dispatching async allows those re-renders to succeed before removing the local builder.
21 | DispatchQueue.main.async { [destinationBuilder, id] in
22 | destinationBuilder?.removeLocalBuilder(identifier: id)
23 | }
24 | }
25 | }
26 |
27 | /// Modifier that appends a local destination builder and ensures the Bool binding is observed and updated.
28 | struct LocalDestinationBuilderModifier: ViewModifier {
29 | let isPresented: Binding
30 | let builder: () -> AnyView
31 |
32 | @StateObject var destinationID = LocalDestinationIDHolder()
33 | @EnvironmentObject var destinationBuilder: DestinationBuilderHolder
34 | @EnvironmentObject var pathHolder: NavigationPathHolder
35 | @Environment(\.isWithinNavigationStack) var isWithinNavigationStack
36 |
37 | func body(content: Content) -> some View {
38 | if isWithinNavigationStack {
39 | if #available(iOS 16.0, *, macOS 13.0, *, watchOS 9.0, *, tvOS 16.0, *) {
40 | content.navigationDestination(isPresented: isPresented, destination: builder)
41 | } else {
42 | fatalError("isWithinNavigationStack shouldn't ever be true on platforms that don't support it")
43 | }
44 | } else {
45 | let _ = destinationBuilder.appendLocalBuilder(identifier: destinationID.id, builder)
46 | let _ = destinationID.setDestinationBuilder(destinationBuilder)
47 |
48 | content
49 | .environmentObject(destinationBuilder)
50 | .onChange(of: pathHolder.path) { _ in
51 | if isPresented.wrappedValue {
52 | if !pathHolder.path.contains(where: { ($0 as? LocalDestinationID) == destinationID.id }) {
53 | isPresented.wrappedValue = false
54 | }
55 | }
56 | }
57 | .onChange(of: isPresented.wrappedValue) { isPresented in
58 | if isPresented {
59 | pathHolder.path.append(destinationID.id)
60 | } else {
61 | let index = pathHolder.path.lastIndex(where: { ($0 as? LocalDestinationID) == destinationID.id })
62 | if let index {
63 | pathHolder.path.remove(at: index)
64 | }
65 | }
66 | }
67 | }
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/Sources/NavigationBackport/NBNavigationLink.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import SwiftUI
3 |
4 | @available(iOS, deprecated: 16.0, message: "Use SwiftUI's Navigation API beyond iOS 15")
5 | /// When value is non-nil, shows the destination associated with its type.
6 | public struct NBNavigationLink: View {
7 | var value: P?
8 | var label: Label
9 |
10 | @EnvironmentObject var pathHolder: Unobserved
11 |
12 | public init(value: P?, @ViewBuilder label: () -> Label) {
13 | self.value = value
14 | self.label = label()
15 | }
16 |
17 | public var body: some View {
18 | // TODO: Ensure this button is styled more like a NavigationLink within a List.
19 | // See: https://gist.github.com/tgrapperon/034069d6116ff69b6240265132fd9ef7
20 | Button(
21 | action: {
22 | guard let value = value else { return }
23 | pathHolder.object.path.append(value)
24 | },
25 | label: { label }
26 | )
27 | }
28 | }
29 |
30 | public extension NBNavigationLink where Label == Text {
31 | init(_ titleKey: LocalizedStringKey, value: P?) {
32 | self.init(value: value) { Text(titleKey) }
33 | }
34 |
35 | init(_ title: S, value: P?) where S: StringProtocol {
36 | self.init(value: value) { Text(title) }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Sources/NavigationBackport/NBNavigationPath+utilities.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public extension NBNavigationPath {
4 | /// Pushes a new screen via a push navigation.
5 | /// - Parameter screen: The screen to push.
6 | mutating func push(_ screen: AnyHashable) {
7 | elements.push(screen)
8 | }
9 |
10 | /// Pops a given number of screens off the stack.
11 | /// - Parameter count: The number of screens to go back. Defaults to 1.
12 | mutating func pop(_ count: Int = 1) {
13 | elements.pop(count)
14 | }
15 |
16 | /// Pops to a given index in the array of screens. The resulting screen count
17 | /// will be index.
18 | /// - Parameter index: The index that should become top of the stack.
19 | mutating func popTo(index: Int) {
20 | elements.popTo(index: index)
21 | }
22 |
23 | /// Pops to the root screen (index 0). The resulting screen count
24 | /// will be 1.
25 | mutating func popToRoot() {
26 | elements.popToRoot()
27 | }
28 |
29 | /// Pops to the topmost (most recently pushed) screen in the stack
30 | /// that satisfies the given condition. If no screens satisfy the condition,
31 | /// the screens array will be unchanged.
32 | /// - Parameter condition: The predicate indicating which screen to pop to.
33 | /// - Returns: A `Bool` indicating whether a screen was found.
34 | @discardableResult
35 | mutating func popTo(where condition: (AnyHashable) -> Bool) -> Bool {
36 | elements.popTo(where: condition)
37 | }
38 | }
39 |
40 | public extension NBNavigationPath {
41 | /// Pops to the topmost (most recently pushed) screen in the stack
42 | /// equal to the given screen. If no screens are found,
43 | /// the screens array will be unchanged.
44 | /// - Parameter screen: The predicate indicating which screen to go back to.
45 | /// - Returns: A `Bool` indicating whether a matching screen was found.
46 | @discardableResult
47 | mutating func popTo(_ screen: AnyHashable) -> Bool {
48 | return elements.popTo(screen)
49 | }
50 |
51 | /// Pops to the topmost (most recently pushed) screen in the stack
52 | /// equal to the given screen. If no screens are found,
53 | /// the screens array will be unchanged.
54 | /// - Parameter screen: The predicate indicating which screen to go back to.
55 | /// - Returns: A `Bool` indicating whether a matching screen was found.
56 | @discardableResult
57 | mutating func popTo(_: T.Type) -> Bool {
58 | return popTo(where: { $0 is T })
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/Sources/NavigationBackport/NBNavigationPath.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import SwiftUI
3 |
4 | @available(iOS, deprecated: 16.0, message: "Use SwiftUI's Navigation API beyond iOS 15")
5 | /// A type-erased wrapper for an Array of any Hashable types, to be displayed in a `NBNavigationStack`.
6 | public struct NBNavigationPath: Equatable {
7 | var elements: [AnyHashable]
8 |
9 | /// The number of screens in the path.
10 | public var count: Int { elements.count }
11 |
12 | /// WHether the path is empty.
13 | public var isEmpty: Bool { elements.isEmpty }
14 |
15 | public init(_ elements: [AnyHashable] = []) {
16 | self.elements = elements
17 | }
18 |
19 | public init(_ elements: S) where S.Element: Hashable {
20 | self.init(elements.map { $0 as AnyHashable })
21 | }
22 |
23 | public mutating func append(_ value: V) {
24 | elements.append(value)
25 | }
26 |
27 | public mutating func removeLast(_ k: Int = 1) {
28 | elements.removeLast(k)
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/Sources/NavigationBackport/NBNavigationStack.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import SwiftUI
3 |
4 | @available(iOS, deprecated: 16.0, message: "Use SwiftUI's Navigation API beyond iOS 15")
5 | /// A replacement for SwiftUI's `NavigationStack` that's available on older OS versions.
6 | public struct NBNavigationStack: View {
7 | @Binding var externalTypedPath: [Data]
8 | @State var internalTypedPath: [Data] = []
9 | @StateObject var path: NavigationPathHolder
10 | @StateObject var destinationBuilder = DestinationBuilderHolder()
11 | @StateObject var navigator: Navigator = .init(.constant([]))
12 | @Environment(\.useNavigationStack) var useNavigationStack
13 | // NOTE: Using `Environment(\.scenePhase)` doesn't work if the app uses UIKIt lifecycle events (via AppDelegate/SceneDelegate).
14 | // We do not need to re-render the view when appIsActive changes, and doing so can cause animation glitches, so it is wrapped
15 | // in `NonReactiveState`.
16 | @State var appIsActive = NonReactiveState(value: true)
17 | var root: Root
18 | var useInternalTypedPath: Bool
19 |
20 | var isUsingNavigationView: Bool {
21 | if #available(iOS 16.0, *, macOS 13.0, *, watchOS 9.0, *, tvOS 16.0, *), useNavigationStack == .whenAvailable {
22 | return false
23 | } else {
24 | return true
25 | }
26 | }
27 |
28 | @ViewBuilder
29 | var content: some View {
30 | if #available(iOS 16.0, *, macOS 13.0, *, watchOS 9.0, *, tvOS 16.0, *), useNavigationStack == .whenAvailable {
31 | NavigationStack(path: useInternalTypedPath ? $internalTypedPath : $externalTypedPath) {
32 | root
33 | .navigationDestination(for: LocalDestinationID.self, destination: { DestinationBuilderView(data: $0) })
34 | .navigationDestination(for: Data.self, destination: { DestinationBuilderView(data: $0) })
35 | .anyHashableNavigationDestination(for: Data.self, destination: { DestinationBuilderView(data: $0) })
36 | }
37 | .environment(\.isWithinNavigationStack, true)
38 | } else {
39 | NavigationView {
40 | Router(rootView: root, screens: $path.path)
41 | }
42 | .navigationViewStyle(supportedNavigationViewStyle)
43 | .environment(\.isWithinNavigationStack, false)
44 | }
45 | }
46 |
47 | public var body: some View {
48 | content
49 | .environmentObject(path)
50 | .environmentObject(Unobserved(object: path))
51 | .environmentObject(destinationBuilder)
52 | .environmentObject(navigator)
53 | .onFirstAppear {
54 | if useInternalTypedPath {
55 | // We can only access the StateObject once the view has been added to the view tree.
56 | navigator.pathBinding = $internalTypedPath
57 | }
58 | }
59 | .onFirstAppear {
60 | guard isUsingNavigationView else {
61 | return
62 | }
63 | // For NavigationView, only initialising with one pushed screen is supported.
64 | // Any others will be pushed one after another with delays.
65 | path.path = Array(path.path.prefix(1))
66 | path.withDelaysIfUnsupported(\.path) {
67 | $0 = externalTypedPath
68 | }
69 | }
70 | .onChange(of: externalTypedPath) { externalTypedPath in
71 | guard isUsingNavigationView else {
72 | return
73 | }
74 | guard path.path != externalTypedPath.map({ $0 }) else { return }
75 | guard appIsActive.value else { return }
76 | path.withDelaysIfUnsupported(\.path) {
77 | $0 = externalTypedPath
78 | }
79 | }
80 | .onChange(of: internalTypedPath) { internalTypedPath in
81 | guard isUsingNavigationView else {
82 | return
83 | }
84 | guard path.path != internalTypedPath.map({ $0 }) else { return }
85 | guard appIsActive.value else { return }
86 | path.withDelaysIfUnsupported(\.path) {
87 | $0 = internalTypedPath
88 | }
89 | }
90 | .onChange(of: path.path) { path in
91 | if useInternalTypedPath {
92 | guard path != internalTypedPath.map({ $0 }) else { return }
93 | internalTypedPath = path.compactMap { anyHashable in
94 | if let data = anyHashable.base as? Data {
95 | return data
96 | } else if anyHashable.base is LocalDestinationID {
97 | return nil
98 | }
99 | fatalError("Cannot add \(type(of: anyHashable.base)) to stack of \(Data.self)")
100 | }
101 | } else {
102 | guard path != externalTypedPath.map({ $0 }) else { return }
103 | externalTypedPath = path.compactMap { anyHashable in
104 | if let data = anyHashable.base as? Data {
105 | return data
106 | } else if anyHashable.base is LocalDestinationID {
107 | return nil
108 | }
109 | fatalError("Cannot add \(type(of: anyHashable.base)) to stack of \(Data.self)")
110 | }
111 | }
112 | }
113 | #if os(iOS)
114 | .onReceive(NotificationCenter.default.publisher(for: didBecomeActive)) { _ in
115 | appIsActive.value = true
116 | guard isUsingNavigationView else { return }
117 | path.withDelaysIfUnsupported(\.path) {
118 | $0 = useInternalTypedPath ? internalTypedPath : externalTypedPath
119 | }
120 | }
121 | .onReceive(NotificationCenter.default.publisher(for: willResignActive)) { _ in
122 | appIsActive.value = false
123 | }
124 | #elseif os(tvOS)
125 | .onReceive(NotificationCenter.default.publisher(for: didBecomeActive)) { _ in
126 | appIsActive.value = true
127 | guard isUsingNavigationView else { return }
128 | path.withDelaysIfUnsupported(\.path) {
129 | $0 = useInternalTypedPath ? internalTypedPath : externalTypedPath
130 | }
131 | }
132 | .onReceive(NotificationCenter.default.publisher(for: willResignActive)) { _ in
133 | appIsActive.value = false
134 | }
135 | #endif
136 | }
137 |
138 | public init(path: Binding<[Data]>?, @ViewBuilder root: () -> Root) {
139 | _externalTypedPath = path ?? .constant([])
140 | self.root = root()
141 | _path = StateObject(wrappedValue: NavigationPathHolder(path: path?.wrappedValue ?? []))
142 | useInternalTypedPath = path == nil
143 |
144 | let navigator = useInternalTypedPath ? Navigator(.constant([])) : Navigator($externalTypedPath)
145 | _navigator = StateObject(wrappedValue: navigator)
146 | }
147 | }
148 |
149 | public extension NBNavigationStack where Data == AnyHashable {
150 | init(@ViewBuilder root: () -> Root) {
151 | self.init(path: nil, root: root)
152 | }
153 | }
154 |
155 | public extension NBNavigationStack where Data == AnyHashable {
156 | init(path: Binding, @ViewBuilder root: () -> Root) {
157 | let path = Binding(
158 | get: { path.wrappedValue.elements },
159 | set: { path.transaction($1).wrappedValue.elements = $0 }
160 | )
161 | self.init(path: path, root: root)
162 | }
163 | }
164 |
165 | var supportedNavigationViewStyle: some NavigationViewStyle {
166 | #if os(macOS)
167 | .automatic
168 | #else
169 | .stack
170 | #endif
171 | }
172 |
173 | #if os(iOS)
174 | private let didBecomeActive = UIApplication.didBecomeActiveNotification
175 | private let willResignActive = UIApplication.willResignActiveNotification
176 | #elseif os(tvOS)
177 | private let didBecomeActive = UIApplication.didBecomeActiveNotification
178 | private let willResignActive = UIApplication.willResignActiveNotification
179 | #endif
180 |
181 | @available(iOS 16.0, macOS 13.0, watchOS 9.0, *, tvOS 16.0, *)
182 | extension View {
183 | @ViewBuilder
184 | func anyHashableNavigationDestination(
185 | for data: D.Type,
186 | @ViewBuilder destination: @escaping (D) -> C
187 | ) -> some View where D: Hashable, C: View {
188 | if ObjectIdentifier(D.self) == ObjectIdentifier(AnyHashable.self) {
189 | // No need to add AnyHashable navigation destination as it's already been added as the Data
190 | // navigation destination.
191 | self
192 | } else {
193 | // Including this ensures that `PathNavigator` can always be used.
194 | navigationDestination(for: AnyHashable.self, destination: { DestinationBuilderView(data: $0) })
195 | }
196 | }
197 | }
198 |
--------------------------------------------------------------------------------
/Sources/NavigationBackport/NBScreen.swift:
--------------------------------------------------------------------------------
1 | /// An empty protocol extending Hashable. Allows access to utility extensions on Array where the element
2 | /// conforms to NBScreen, rather than polluting all Arrays with navigation APIs.
3 | public protocol NBScreen: Hashable {}
4 |
5 | extension AnyHashable: NBScreen {}
6 |
--------------------------------------------------------------------------------
/Sources/NavigationBackport/NavigationBackport.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import SwiftUI
3 |
4 | typealias DestinationBuilder = (T) -> AnyView
5 |
6 | enum NavigationBackport {
7 | /// Calculates the minimal number of steps to update from one stack of screens to another, within the constraints of SwiftUI.
8 | /// - Parameters:
9 | /// - start: The initial state.
10 | /// - end: The goal state.
11 | /// - Returns: A series of state updates from the start to end.
12 | public static func calculateSteps(from start: [Screen], to end: [Screen]) -> [[Screen]] {
13 | let replacableScreens = end.prefix(start.count)
14 | let remainingScreens = start.count < end.count ? end.suffix(from: start.count) : []
15 |
16 | var steps = [Array(replacableScreens)]
17 | var lastStep: [Screen] { steps.last! }
18 |
19 | for screen in remainingScreens {
20 | steps.append(lastStep + [screen])
21 | }
22 |
23 | return steps
24 | }
25 |
26 | static func canSynchronouslyUpdate(from start: [Screen], to end: [Screen]) -> Bool {
27 | // If there are less than 3 steps, the transformation can be applied in one update.
28 | let steps = calculateSteps(from: start, to: end)
29 | return steps.count < 3
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/Sources/NavigationBackport/NavigationLinkRowStyle.swift:
--------------------------------------------------------------------------------
1 | // Adapted from: https://gist.github.com/tgrapperon/e92d7699b2a6ca8093bdf2cb1abb3376
2 | import SwiftUI
3 |
4 | public extension ButtonStyle where Self == NavigationLinkRowStyle {
5 | /// Experimental approach to mimic the appearance of a `NavigationLink` within a table.
6 | static var navigationLinkRowStyle: NavigationLinkRowStyle { .init() }
7 | }
8 |
9 | /// Mimics the appearance of a `NavigationLink` within a table.
10 | public struct NavigationLinkRowStyle: ButtonStyle {
11 | public func makeBody(configuration: Configuration) -> some View {
12 | NavigationLink {} label: { configuration.label }
13 | // HACK: Adding a 'clear' background ensures the tappable area fills the entire space.
14 | // There may be a better way of achieving this.
15 | .background(Color.white.opacity(0.0001))
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/Sources/NavigationBackport/NavigationPathHolder.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import SwiftUI
3 |
4 | /// An object that publishes changes to the path Array it holds.
5 | class NavigationPathHolder: ObservableObject {
6 | @Published var path: [AnyHashable]
7 |
8 | init(path: [AnyHashable] = []) {
9 | self.path = path
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/Sources/NavigationBackport/Navigator+utilities.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public extension Navigator where Screen: NBScreen {
4 | /// Pushes a new screen via a push navigation.
5 | /// - Parameter screen: The screen to push.
6 | func push(_ screen: Screen) {
7 | path.push(screen)
8 | }
9 |
10 | /// Pops a given number of screens off the stack.
11 | /// - Parameter count: The number of screens to go back. Defaults to 1.
12 | func pop(_ count: Int = 1) {
13 | path.pop(count)
14 | }
15 |
16 | /// Pops to a given index in the array of screens. The resulting screen count
17 | /// will be index.
18 | /// - Parameter index: The index that should become top of the stack.
19 | func popTo(index: Int) {
20 | path.popTo(index: index)
21 | }
22 |
23 | /// Pops to the root screen (index 0). The resulting screen count
24 | /// will be 1.
25 | func popToRoot() {
26 | path.popToRoot()
27 | }
28 |
29 | /// Pops to the topmost (most recently pushed) screen in the stack
30 | /// that satisfies the given condition. If no screens satisfy the condition,
31 | /// the screens array will be unchanged.
32 | /// - Parameter condition: The predicate indicating which screen to pop to.
33 | /// - Returns: A `Bool` indicating whether a screen was found.
34 | @discardableResult
35 | func popTo(where condition: (Screen) -> Bool) -> Bool {
36 | path.popTo(where: condition)
37 | }
38 | }
39 |
40 | public extension Navigator where Screen: NBScreen & Equatable {
41 | /// Pops to the topmost (most recently pushed) screen in the stack
42 | /// equal to the given screen. If no screens are found,
43 | /// the screens array will be unchanged.
44 | /// - Parameter screen: The predicate indicating which screen to go back to.
45 | /// - Returns: A `Bool` indicating whether a matching screen was found.
46 | @discardableResult
47 | func popTo(_ screen: Screen) -> Bool {
48 | return path.popTo(screen)
49 | }
50 | }
51 |
52 | public extension Navigator where Screen: NBScreen & Identifiable {
53 | /// Pops to the topmost (most recently pushed) identifiable screen in the stack
54 | /// with the given ID. If no screens are found, the screens array will be unchanged.
55 | /// - Parameter id: The id of the screen to goBack to.
56 | /// - Returns: A `Bool` indicating whether a matching screen was found.
57 | @discardableResult
58 | func popTo(id: Screen.ID) -> Bool {
59 | path.popTo(id: id)
60 | }
61 | }
62 |
63 | public extension Navigator where Screen == AnyHashable {
64 | /// Pops to the topmost (most recently pushed) identifiable screen in the stack
65 | /// with the given ID. If no screens are found, the screens array will be unchanged.
66 | /// - Parameter id: The id of the screen to goBack to.
67 | /// - Returns: A `Bool` indicating whether a matching screen was found.
68 | @discardableResult
69 | func popTo(_: T.Type) -> Bool {
70 | popTo(where: { $0 is T })
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/Sources/NavigationBackport/Navigator+withDelaysIfUnsupported.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public extension Navigator {
4 | /// Any changes can be made to the screens array passed to the transform closure. If those
5 | /// changes are not supported within a single update by SwiftUI, the changes will be
6 | /// applied in stages.
7 | @_disfavoredOverload
8 | @MainActor
9 | func withDelaysIfUnsupported(transform: (inout [Screen]) -> Void, onCompletion: (() -> Void)? = nil) {
10 | let start = path
11 | let end = apply(transform, to: start)
12 |
13 | let didUpdateSynchronously = synchronouslyUpdateIfSupported(from: start, to: end)
14 | guard !didUpdateSynchronously else { return }
15 |
16 | Task { @MainActor in
17 | await pathBinding.withDelaysIfUnsupported(from: start, to: end, keyPath: \.self)
18 | onCompletion?()
19 | }
20 | }
21 |
22 | /// Any changes can be made to the screens array passed to the transform closure. If those
23 | /// changes are not supported within a single update by SwiftUI, the changes will be
24 | /// applied in stages.
25 | @MainActor
26 | func withDelaysIfUnsupported(transform: (inout [Screen]) -> Void) async {
27 | let start = path
28 | let end = apply(transform, to: start)
29 |
30 | let didUpdateSynchronously = synchronouslyUpdateIfSupported(from: start, to: end)
31 | guard !didUpdateSynchronously else { return }
32 |
33 | await pathBinding.withDelaysIfUnsupported(transform)
34 | }
35 |
36 | fileprivate func synchronouslyUpdateIfSupported(from start: [Screen], to end: [Screen]) -> Bool {
37 | guard NavigationBackport.canSynchronouslyUpdate(from: start, to: end) else {
38 | return false
39 | }
40 | path = end
41 | return true
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/Sources/NavigationBackport/Navigator.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | /// A navigator to use when the `NBNavigationStack` is initialized with a `NBNavigationPath` binding or no binding.`
4 | public typealias PathNavigator = Navigator
5 |
6 | /// An object available via the environment that gives access to the current path.
7 | /// Supports push and pop operations when `Screen` conforms to `NBScreen`.
8 | @MainActor
9 | public class Navigator: ObservableObject {
10 | var pathBinding: Binding<[Screen]>
11 |
12 | /// The current navigation path.
13 | public var path: [Screen] {
14 | get { pathBinding.wrappedValue }
15 | set { pathBinding.wrappedValue = newValue }
16 | }
17 |
18 | init(_ pathBinding: Binding<[Screen]>) {
19 | self.pathBinding = pathBinding
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Sources/NavigationBackport/Node.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import SwiftUI
3 |
4 | struct Node: View {
5 | @Binding var allScreens: [Screen]
6 | let truncateToIndex: (Int) -> Void
7 | let index: Int
8 | let screen: Screen?
9 |
10 | @State var isAppeared = false
11 |
12 | init(allScreens: Binding<[Screen]>, truncateToIndex: @escaping (Int) -> Void, index: Int) {
13 | _allScreens = allScreens
14 | self.truncateToIndex = truncateToIndex
15 | self.index = index
16 | screen = allScreens.wrappedValue[safe: index]
17 | }
18 |
19 | private var isActiveBinding: Binding {
20 | return Binding(
21 | get: { allScreens.count > index + 1 },
22 | set: { isShowing in
23 | guard !isShowing else { return }
24 | guard allScreens.count > index + 1 else { return }
25 | guard isAppeared else { return }
26 | truncateToIndex(index + 1)
27 | }
28 | )
29 | }
30 |
31 | var next: some View {
32 | Node(allScreens: $allScreens, truncateToIndex: truncateToIndex, index: index + 1)
33 | }
34 |
35 | var body: some View {
36 | if let screen = allScreens[safe: index] ?? screen {
37 | DestinationBuilderView(data: screen)
38 | ._navigationDestination(isActive: isActiveBinding, destination: next)
39 | .onAppear { isAppeared = true }
40 | .onDisappear { isAppeared = false }
41 | }
42 | }
43 | }
44 |
45 | extension Collection {
46 | /// Returns the element at the specified index if it is within bounds, otherwise nil.
47 | subscript(safe index: Index) -> Element? {
48 | return indices.contains(index) ? self[index] : nil
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/Sources/NavigationBackport/NonReactiveState.swift:
--------------------------------------------------------------------------------
1 | /// This provides a mechanism to store state attached to a SwiftUI view's lifecycle, without causing the view to re-render when the value changes.
2 | class NonReactiveState {
3 | var value: T
4 |
5 | init(value: T) {
6 | self.value = value
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/Sources/NavigationBackport/ObservableObject+withDelaysIfUnsupported.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import SwiftUI
3 |
4 | public extension ObservableObject {
5 | /// Any changes can be made to the screens array passed to the transform closure. If those
6 | /// changes are not supported within a single update by SwiftUI, the changes will be
7 | /// applied in stages. An async version of this function is also available.
8 | @_disfavoredOverload
9 | @MainActor
10 | func withDelaysIfUnsupported(_ keyPath: WritableKeyPath, transform: (inout [Screen]) -> Void, onCompletion: (() -> Void)? = nil) {
11 | let start = self[keyPath: keyPath]
12 | let end = apply(transform, to: start)
13 |
14 | let didUpdateSynchronously = synchronouslyUpdateIfSupported(keyPath, from: start, to: end)
15 | guard !didUpdateSynchronously else { return }
16 |
17 | Task { @MainActor in
18 | await withDelaysIfUnsupported(keyPath, from: start, to: end)
19 | onCompletion?()
20 | }
21 | }
22 |
23 | /// Any changes can be made to the screens array passed to the transform closure. If those
24 | /// changes are not supported within a single update by SwiftUI, the changes will be
25 | /// applied in stages.
26 | @MainActor
27 | func withDelaysIfUnsupported(_ keyPath: WritableKeyPath, transform: (inout [Screen]) -> Void) async {
28 | let start = self[keyPath: keyPath]
29 | let end = apply(transform, to: start)
30 |
31 | let didUpdateSynchronously = synchronouslyUpdateIfSupported(keyPath, from: start, to: end)
32 | guard !didUpdateSynchronously else { return }
33 |
34 | await withDelaysIfUnsupported(keyPath, from: start, to: end)
35 | }
36 |
37 | /// Any changes can be made to the screens array passed to the transform closure. If those
38 | /// changes are not supported within a single update by SwiftUI, the changes will be
39 | /// applied in stages. An async version of this function is also available.
40 | @_disfavoredOverload
41 | @MainActor
42 | func withDelaysIfUnsupported(_ keyPath: WritableKeyPath, transform: (inout NBNavigationPath) -> Void, onCompletion: (() -> Void)? = nil) {
43 | let start = self[keyPath: keyPath]
44 | let end = apply(transform, to: start)
45 |
46 | let didUpdateSynchronously = synchronouslyUpdateIfSupported(keyPath.appending(path: \.elements), from: start.elements, to: end.elements)
47 | guard !didUpdateSynchronously else { return }
48 |
49 | Task { @MainActor in
50 | await withDelaysIfUnsupported(keyPath.appending(path: \.elements), from: start.elements, to: end.elements)
51 | onCompletion?()
52 | }
53 | }
54 |
55 | /// Any changes can be made to the screens array passed to the transform closure. If those
56 | /// changes are not supported within a single update by SwiftUI, the changes will be
57 | /// applied in stages.
58 | @MainActor
59 | func withDelaysIfUnsupported(_ keyPath: WritableKeyPath, transform: (inout NBNavigationPath) -> Void) async {
60 | let start = self[keyPath: keyPath]
61 | let end = apply(transform, to: start)
62 |
63 | let didUpdateSynchronously = synchronouslyUpdateIfSupported(keyPath.appending(path: \.elements), from: start.elements, to: end.elements)
64 | guard !didUpdateSynchronously else { return }
65 |
66 | await withDelaysIfUnsupported(keyPath.appending(path: \.elements), from: start.elements, to: end.elements)
67 | }
68 |
69 | @MainActor
70 | fileprivate func withDelaysIfUnsupported(_ keyPath: WritableKeyPath, from start: [Screen], to end: [Screen]) async {
71 | let binding = Binding(
72 | get: { [weak self] in self?[keyPath: keyPath] ?? [] },
73 | set: { [weak self] in self?[keyPath: keyPath] = $0 }
74 | )
75 | await binding.withDelaysIfUnsupported(from: start, to: end, keyPath: \.self)
76 | }
77 |
78 | fileprivate func synchronouslyUpdateIfSupported(_ keyPath: WritableKeyPath, from start: [Screen], to end: [Screen]) -> Bool {
79 | guard NavigationBackport.canSynchronouslyUpdate(from: start, to: end) else {
80 | return false
81 | }
82 | // Even though self is known to be a class, the compiler complains that self is immutable
83 | // without this indirection.
84 | var copy = self
85 | copy[keyPath: keyPath] = end
86 | return true
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/Sources/NavigationBackport/Router.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import SwiftUI
3 |
4 | struct Router: View {
5 | let rootView: RootView
6 |
7 | @Binding var screens: [Screen]
8 |
9 | init(rootView: RootView, screens: Binding<[Screen]>) {
10 | self.rootView = rootView
11 | _screens = screens
12 | }
13 |
14 | var pushedScreens: some View {
15 | Node(allScreens: $screens, truncateToIndex: { screens = Array(screens.prefix($0)) }, index: 0)
16 | }
17 |
18 | private var isActiveBinding: Binding {
19 | Binding(
20 | get: { !screens.isEmpty },
21 | set: { isShowing in
22 | guard !isShowing else { return }
23 | guard !screens.isEmpty else { return }
24 | screens = []
25 | }
26 | )
27 | }
28 |
29 | var body: some View {
30 | rootView
31 | ._navigationDestination(isActive: isActiveBinding, destination: pushedScreens)
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/Sources/NavigationBackport/Unobserved.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | /// A wrapper that allows access to an observable object without publishing its changes.
4 | class Unobserved: ObservableObject {
5 | let object: Object
6 |
7 | init(object: Object) {
8 | self.object = object
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/Sources/NavigationBackport/UseNavigationStackPolicy.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | public enum UseNavigationStackPolicy {
4 | case whenAvailable
5 | case never
6 | }
7 |
--------------------------------------------------------------------------------
/Sources/NavigationBackport/View+_navigationDestination.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct NavigationLinkModifier: ViewModifier {
4 | @Binding var isActiveBinding: Bool
5 | var destination: Destination
6 | @Environment(\.isWithinNavigationStack) var isWithinNavigationStack
7 |
8 | func body(content: Content) -> some View {
9 | if #available(iOS 16.0, *, macOS 13.0, *, watchOS 9.0, *, tvOS 16.0, *), isWithinNavigationStack {
10 | AnyView(
11 | content
12 | .navigationDestination(isPresented: $isActiveBinding, destination: { destination })
13 | )
14 | } else {
15 | AnyView(
16 | content
17 | .background(
18 | NavigationLink(destination: destination, isActive: $isActiveBinding, label: EmptyView.init)
19 | .hidden()
20 | )
21 | )
22 | }
23 | }
24 | }
25 |
26 | extension View {
27 | func _navigationDestination(isActive: Binding, destination: Destination) -> some View {
28 | return modifier(NavigationLinkModifier(isActiveBinding: isActive, destination: destination))
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/Sources/NavigationBackport/View+nbNavigationDestination.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import SwiftUI
3 |
4 | public extension View {
5 | @available(iOS, deprecated: 16.0, message: "Use SwiftUI's Navigation API beyond iOS 15")
6 | func nbNavigationDestination(for pathElementType: D.Type, @ViewBuilder destination builder: @escaping (D) -> C) -> some View {
7 | return modifier(DestinationBuilderModifier(typedDestinationBuilder: { AnyView(builder($0)) }))
8 | }
9 | }
10 |
11 | public extension View {
12 | @available(iOS, deprecated: 16.0, message: "Use SwiftUI's Navigation API beyond iOS 15")
13 | /// Associates a destination view with a binding that can be used to push
14 | /// the view onto a ``NBNavigationStack``.
15 | ///
16 | /// In general, favor binding a path to a navigation stack for programmatic
17 | /// navigation. Add this view modifer to a view inside a ``NBNavigationStack``
18 | /// to programmatically push a single view onto the stack. This is useful
19 | /// for building components that can push an associated view. For example,
20 | /// you can present a `ColorDetail` view for a particular color:
21 | ///
22 | /// @State private var showDetails = false
23 | /// var favoriteColor: Color
24 | ///
25 | /// NBNavigationStack {
26 | /// VStack {
27 | /// Circle()
28 | /// .fill(favoriteColor)
29 | /// Button("Show details") {
30 | /// showDetails = true
31 | /// }
32 | /// }
33 | /// .nbNavigationDestination(isPresented: $showDetails) {
34 | /// ColorDetail(color: favoriteColor)
35 | /// }
36 | /// .nbNavigationTitle("My Favorite Color")
37 | /// }
38 | ///
39 | /// Do not put a navigation destination modifier inside a "lazy" container,
40 | /// like ``List`` or ``LazyVStack``. These containers create child views
41 | /// only when needed to render on screen. Add the navigation destination
42 | /// modifier outside these containers so that the navigation stack can
43 | /// always see the destination.
44 | ///
45 | /// - Parameters:
46 | /// - isPresented: A binding to a Boolean value that indicates whether
47 | /// `destination` is currently presented.
48 | /// - destination: A view to present.
49 | func nbNavigationDestination(isPresented: Binding, @ViewBuilder destination: () -> V) -> some View where V: View {
50 | let builtDestination = AnyView(destination())
51 | return modifier(
52 | LocalDestinationBuilderModifier(
53 | isPresented: isPresented,
54 | builder: { builtDestination }
55 | )
56 | )
57 | }
58 |
59 | @available(iOS, deprecated: 16.0, message: "Use SwiftUI's Navigation API beyond iOS 15")
60 | /// Associates a destination view with a bound value for use within a
61 | /// navigation stack.
62 | ///
63 | /// Add this view modifer to a view inside an ``NBNavigationStack`` to describe
64 | /// the view that the stack displays when presenting a particular kind of data. Programmatically
65 | /// update the binding to display or remove the view. For example:
66 | ///
67 | /// @State private var colorShown: Color?
68 | ///
69 | /// NBNavigationView {
70 | /// List {
71 | /// Button("Mint") { colorShown = .mint }
72 | /// Button("Pink") { colorShown = .pink }
73 | /// Button("Teal") { colorShown = .teal }
74 | /// }
75 | /// .nbNavigationDestination(item: $colorShown) { color in
76 | /// ColorDetail(color: color)
77 | /// }
78 | /// }
79 | ///
80 | /// When the person using the app taps on the Mint button, the mint color
81 | /// is pushed onto the navigation stack. You can pop the view
82 | /// by setting `colorShown` back to `nil`.
83 | ///
84 | /// You can add more than one navigation destination modifier to the stack
85 | /// if it needs to present more than one kind of data.
86 | ///
87 | /// Do not put a navigation destination modifier inside a "lazy" container,
88 | /// like ``List`` or ``LazyVStack``. These containers create child views
89 | /// only when needed to render on screen. Add the navigation destination
90 | /// modifier outside these containers so that the navigation view can
91 | /// always see the destination.
92 | ///
93 | /// - Parameters:
94 | /// - item: A binding to the data presented, or `nil` if nothing is
95 | /// currently presented.
96 | /// - destination: A view builder that defines a view to display
97 | /// when `item` is not `nil`.
98 | func nbNavigationDestination(item: Binding, @ViewBuilder destination: @escaping (D) -> C) -> some View {
99 | nbNavigationDestination(
100 | isPresented: Binding(
101 | get: { item.wrappedValue != nil },
102 | set: { isActive, transaction in
103 | if !isActive {
104 | item.transaction(transaction).wrappedValue = nil
105 | }
106 | }
107 | ),
108 | destination: { ConditionalViewBuilder(data: item, buildView: destination) }
109 | )
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/Sources/NavigationBackport/View+nbUseNavigationStack.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | public extension View {
4 | @available(iOS, deprecated: 16.0, message: "Use SwiftUI's Navigation API beyond iOS 15")
5 | /// Sets the policy for whether to use SwiftUI's built-in `NavigationStack` when available (i.e. when the SwiftUI
6 | /// version includes it). The default behaviour is to never use `NavigationStack` - instead `NavigationView`
7 | /// will be used on all versions, even when the API is available.
8 | /// - Parameter policy: The policy to use
9 | /// - Returns: A view with the policy set for all child views via a private environment value.
10 | func nbUseNavigationStack(_ policy: UseNavigationStackPolicy) -> some View {
11 | environment(\.useNavigationStack, policy)
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/Sources/NavigationBackport/View+onFirstAppear.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | private struct OnFirstAppear: ViewModifier {
4 | let action: (() -> Void)?
5 |
6 | @State private var hasAppeared = false
7 |
8 | func body(content: Content) -> some View {
9 | content.onAppear {
10 | if !hasAppeared {
11 | hasAppeared = true
12 | action?()
13 | }
14 | }
15 | }
16 | }
17 |
18 | extension View {
19 | func onFirstAppear(perform action: (() -> Void)? = nil) -> some View {
20 | modifier(OnFirstAppear(action: action))
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/Sources/NavigationBackport/apply.swift:
--------------------------------------------------------------------------------
1 | /// Utilty for applying a transform to a value.
2 | /// - Parameters:
3 | /// - transform: The transform to apply.
4 | /// - input: The value to be transformed.
5 | /// - Returns: The transformed value.
6 | func apply(_ transform: (inout T) -> Void, to input: T) -> T {
7 | var transformed = input
8 | transform(&transformed)
9 | return transformed
10 | }
11 |
--------------------------------------------------------------------------------
/Tests/NavigationBackportTests/NavigationBackportTests.swift:
--------------------------------------------------------------------------------
1 | @testable import NavigationBackport
2 | import XCTest
3 |
4 | final class NavigationBackportTests: XCTestCase {
5 | func testPushOneAtATime() {
6 | let start = [1]
7 | let end = [-1, -2, -3, -4]
8 |
9 | let steps = NavigationBackport.calculateSteps(from: start, to: end)
10 |
11 | let expectedSteps = [
12 | [-1],
13 | [-1, -2],
14 | [-1, -2, -3],
15 | end,
16 | ]
17 | XCTAssertEqual(steps, expectedSteps)
18 | }
19 |
20 | func testPopAllInOne() {
21 | let start = [1, 2, 3, 4]
22 | let end = [-1]
23 |
24 | let steps = NavigationBackport.calculateSteps(from: start, to: end)
25 |
26 | let expectedSteps = [end]
27 | XCTAssertEqual(steps, expectedSteps)
28 | }
29 |
30 | func testEquatableEquals() {
31 | let path = [1, 2, 3]
32 | let lhs = NBNavigationPath(path)
33 | let rhs = NBNavigationPath(path)
34 |
35 | XCTAssertEqual(lhs, rhs)
36 | }
37 |
38 | func testEquatableNotEquals() {
39 | let lhs = NBNavigationPath([1, 2, 3])
40 | let rhs = NBNavigationPath([1, 2])
41 |
42 | XCTAssertNotEqual(lhs, rhs)
43 | }
44 | }
45 |
--------------------------------------------------------------------------------