├── .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 | [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](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 | --------------------------------------------------------------------------------