Backported SwiftUI navigation APIs introduced in WWDC22

Overview

Navigation Backport

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 and watchOS.

NavigationStack -> NBNavigationStack

NavigationLink -> NBNavigationLink

NavigationPath -> NBNavigationPath

navigationDestination -> nbNavigationDestination

You can migrate to these APIs now, and when you eventually bump your deployment target to iOS 16, you can remove this library and easily migrate to its SwiftUI equivalent. You can initialise NBNavigationStack with a binding to an Array, a NBNavigationPath binding, or neither.

Example

import NavigationBackport
import SwiftUI

struct ContentView: View {
  @State var path = NBNavigationPath()

  var body: some View {
    NBNavigationStack(path: $path) {
      HomeView()
        .nbNavigationDestination(for: NumberList.self, destination: { numberList in
          NumberListView(numberList: numberList)
        })
        .nbNavigationDestination(for: Int.self, destination: { number in
          NumberView(number: number, goBackToRoot: { path.removeLast(path.count) })
        })
        .nbNavigationDestination(for: EmojiVisualisation.self, destination: { visualisation in
          EmojiView(visualisation: visualisation)
        })
    }
  }
}

struct HomeView: View {
  var body: some View {
    VStack(spacing: 8) {
      NBNavigationLink(value: NumberList(range: 0 ..< 100), label: { Text("Pick a number") })
    }.navigationTitle("Home")
  }
}

struct NumberList: Hashable {
  let range: Range<Int>
}

struct NumberListView: View {
  let numberList: NumberList
  var body: some View {
    List {
      ForEach(numberList.range, id: \.self) { number in
        NBNavigationLink("\(number)", value: number)
      }
    }.navigationTitle("List")
  }
}

struct NumberView: View {
  let number: Int
  let goBackToRoot: () -> Void

  var body: some View {
    VStack(spacing: 8) {
      Text("\(number)")
      NBNavigationLink(
        value: number + 1,
        label: { Text("Show next number") }
      )
      NBNavigationLink(
        value: EmojiVisualisation(emoji: "🐑", count: number),
        label: { Text("Visualise with sheep") }
      )
      Button("Go back to root", action: goBackToRoot)
    }.navigationTitle("\(number)")
  }
}

struct EmojiVisualisation: Hashable {
  let emoji: Character
  let count: Int
}

struct EmojiView: View {
  let visualisation: EmojiVisualisation

  var body: some View {
    Text(String(Array(repeating: visualisation.emoji, count: visualisation.count)))
      .navigationTitle("Visualise \(visualisation.count)")
  }
}

Deep-linking

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 provides an API to work around this limitation: you can wrap such path changes within a call to withDelaysIfUnsupported, and the library will, if necessary, break down the larger update into a series of smaller updates that SwiftUI supports, with delays in between. For example, the following code that tries to push three screens in one update will not work:

  path.append(Screen.orders)
  path.append(Screen.editOrder(id: id))
  path.append(Screen.confirmChanges(orderId: id))

However, the following code will successfully push all three screens, one after another:

$path.withDelaysIfUnsupported {
  $0.append(Screen.orders)
  $0.append(Screen.editOrder(id: id))
  $0.append(Screen.confirmChanges(orderId: id))
}

You can make any changes to the path passed into the withDelaysIfUnsupported closure, and the library will calculate the minimal number of state updates required to successfully update the UI.

To do

The package is not yet fully complete. Here are some outstanding tasks:

  • Codable support for NavigationPath
  • Codable support for NavigationLink
  • Backport NavigationSplitView
  • Conditionally use SwiftUI Navigation API if available?
Comments
  • Adds Navigator and navigation utilities

    Adds Navigator and navigation utilities

    This PR:

    • Adds a Navigator object accessible via the environment, which gives access to the current navigation path.
    • Adds utilities for pushing and popping via the navigator, or directly with an Array or NBNavigationPath.

    Usage:

    The navigator can be accessed via the environment, e.g. for a NBNavigationPath-backed stack:

    @EnvironmentObject var navigator: PathNavigator

    Or for an Array-backed stack:

    @EnvironmentObject var navigator: Navigator<Screen>

    The navigator can then be used in a variety of ways:

    navigator.push(Profile(name: "John"))
    
    navigator.pop()
    
    navigator.popToRoot()
    
    navigator.popTo(Profile.self)
    
    opened by johnpatrickmorgan 6
  • public NavigationPathHolder for usage in cases where Custom UI is needed

    public NavigationPathHolder for usage in cases where Custom UI is needed

    When using a SwiftUI Button or any other programmatic way, it might be convinient to be able to access the NavigationPathHolder EnvironmentObject to manipulate the path directly in sub views.

    opened by muellnes 6
  • Any chance of some backport love for the `isPresented` overload of `navigationDestination`?

    Any chance of some backport love for the `isPresented` overload of `navigationDestination`?

    From the SwiftUI interface (documentation link):

        /// Associates a destination view with a binding that can be used to push
        /// the view onto a ``NavigationStack``.
        ///
        /// In general, favor binding a path to a navigation stack for programmatic
        /// navigation. Add this view modifer to a view inside a ``NavigationStack``
        /// to programmatically push a single view onto the stack. This is useful
        /// for building components that can push an associated view. For example,
        /// you can present a `ColorDetail` view for a particular color:
        ///
        ///     @State private var showDetails = false
        ///     var favoriteColor: Color
        ///
        ///     NavigationStack {
        ///         VStack {
        ///             Circle()
        ///                 .fill(favoriteColor)
        ///             Button("Show details") {
        ///                 showDetails = true
        ///             }
        ///         }
        ///         .navigationDestination(isPresented: $showDetails) {
        ///             ColorDetail(color: favoriteColor)
        ///         }
        ///         .navigationTitle("My Favorite Color")
        ///     }
        ///
        /// Do not put a navigation destination modifier inside a "lazy" container,
        /// like ``List`` or ``LazyVStack``. These containers create child views
        /// only when needed to render on screen. Add the navigation destination
        /// modifier outside these containers so that the navigation stack can
        /// always see the destination.
        ///
        /// - Parameters:
        ///   - isPresented: A binding to a Boolean value that indicates whether
        ///     `destination` is currently presented.
        ///   - destination: A view to present.
        public func navigationDestination<V>(isPresented: Binding<Bool>, @ViewBuilder destination: () -> V) -> some View where V : View
    
    opened by kielgillard 6
  • NavigationPath Codable

    NavigationPath Codable

    Hey so I have been messing around with trying to make NavigationPath Codable in the meantime. Curious if you have any ideas on how maybe it is getting done? Wanted to try and implement something rudimentary in the time being.

    opened by NicholasBellucci 6
  • Introduce NBNavigationSplitView as a backport of NavigationSplitView

    Introduce NBNavigationSplitView as a backport of NavigationSplitView

    Apple introduced NavigationSplitView in WWDC22, this is a backport implementation of it with limitation:

    • only support double colunm layout (sidebar + detail)
    • can not nested in NBNavigationStack

    Pease see the demo for usage

    opened by strangeliu 5
  • Runtime issue relating to NavigationLink usage

    Runtime issue relating to NavigationLink usage

    I'm not sure if it's possible to fix this, but at runtime I get the following issue:

    NavigationLink presenting a value must appear inside a NavigationContent-based NavigationView. Link will be disabled.

    I'm not 100% confident of the root cause of this, but I know it happens when I tap an NBNavigationLink.

    I see 2 places in the project that use NavigationLink: https://github.com/johnpatrickmorgan/NavigationBackport/blob/79fed7bfd6323850634881632365c7f4318442f0/Sources/NavigationBackport/Router.swift#L36 and https://github.com/johnpatrickmorgan/NavigationBackport/blob/79fed7bfd6323850634881632365c7f4318442f0/Sources/NavigationBackport/Node.swift#L38. I tried – naively – to wrap these in NavigationViews, but I guess the NavigationView then handles/overrides the links being tapping the link then does nothing.

    Maybe this is something we'll just have to live with, but it would be nice to not have the runtime issue if possible!

    I'm using Xcode 14 beta 2.

    opened by JosephDuffy 5
  • Crash on iOS 14 devices

    Crash on iOS 14 devices

    I have an issue where the navigation works flawlessly on iOS 15 devices, but immediately crashes the app upon pressing a link on an iOS 14 device. I had a look at DestinationBuilderModifier and the content there is printed out differently.

    on iOS 15 devices (Including simulator) SwiftUI._ViewModifier_Content<NavigationBackport.DestinationBuilderModifier<MyProject.Event>>()

    on iOS 14 devices (Including simulator) (SwiftUI._ViewModifier_Content<NavigationBackport.DestinationBuilderModifier<MyProjectt.Event>>) content = {}

    This is one of the places implemented that crashes the app. The navigation is done by NBNavigationDestination attached to a scroll view

    NBNavigationStack {
    	ScrollView {
    		NBNavigationLink(value: account) {
    			Text("CLICK ME")
    				.Shadow()
    		}
    		.buttonStyle(.plain)
    	}
    	.setBackground()
    	.nbNavigationDestination(for: Account.self) { account in
    		DestinationView(for: account)
    	}
    }
    

    Tested cases:

    • Navigation Stack with no path, does not navigate at all
    • Navigation Stack with path, crashes the app on second level
    • Navigation Stack that is added through a ViewModifier crashes the app, if generics is used

    The error printed out is 2022-07-06 14:34:52.810190+0200 MyProject[99017:5417116] Fatal error: No view builder found for key MyProject.Account: file NavigationBackport/DestinationBuilderHolder.swift, line 34

    Here is also another example where it works on iOS 15 and not on iOS 14

    struct DestinationView: View {
        let account: Account
    
        @Binding var path: NBNavigationPath
    
        var body: some View {
          VStack {
            Text(account.name)
    
            Button {
              $path.withDelaysIfUnsupported { $0.append(Account()) }
            } label: {
              Text("Try me?")
            }
    
            NBNavigationLink(value: Account()) {
              Text("Or... try me?")
            }
          }
          .nbNavigationDestination(for: Account.self, destination: { person in
            Text("CRASHES")
          })
        }
      }
    
    opened by GioPalusa 4
  • No ObservableObject of type DestinationBuilderHolder found.

    No ObservableObject of type DestinationBuilderHolder found.

    Screen Shot 2022-10-29 at 16 12 06

    Hello! I keep get this error when i try to navigate.

    Fatal error: No ObservableObject of type DestinationBuilderHolder found. A View.environmentObject(_:) for DestinationBuilderHolder may be missing as an ancestor of this view.

    Couldn't really figured out why it crashes since a default environment object is presented inside NBNavigationStack. Do you have any suggestion on how to solve this. Sorry if it's already too obvious 😅

    opened by raduncicen 3
  • Warning when using with @Published values

    Warning when using with @Published values

    When using this library with @Published values, I get the old familiar warning in Xcode:

    Publishing changes from background threads is not allowed; make sure to publish values from the main thread (via operators like receive(on:)) on model updates.

    Note that this warning actually stems from Binding.withDelaysIfUnsupported(from:to:keyPath:) in the NavigationBackport source code, and not from my app source code.

    Is this something that I should be worried about? Is it "just a warning"? Can we get rid of it by making some changes to the NavigationBackport library?

    Related to #20

    opened by denizdogan 3
  • Adds NBNavigationLinkViewModifier to allow programmatic triggering of the navigate action.

    Adds NBNavigationLinkViewModifier to allow programmatic triggering of the navigate action.

    Purpose

    In use cases where we would normally like to use the NBNavigationLink, but need to trigger the same navigation action programmatically, based on a changing state variable, instead of with a button press.

    E.g. I found this useful for a few cases where there is an HTML web view embedded in app, and I needed to intercept links being tapped, to trigger onward navigation.

    opened by gsp100677 3
  • Fatal error when using NBNavigationStack with a non empty path in a conditional

    Fatal error when using NBNavigationStack with a non empty path in a conditional

    Navigation Backport is a great library which enables me to use the much better iOS 16 API also on iOS 14. Thanks a lot for sharing it.

    Although, I found one major issue when using a NBNavigationStack with a non empty path in the else part of a conditional. It results in a fatal error and looks for the user like a crash.

    I can reproduce this behaviour with the following piece of code compiled with Xcode 13.4 and iOS 15.5 in the simulator:

    import NavigationBackport
    import SwiftUI
    
    enum Item {
        case one
        case two
    }
    
    struct ContentView: View {
        // App will crash on iOS 15.5 if showGreen is initially true and the path is not empty
        @State var showGreen = true
        @State var path: [Item] = [.one, .two]
        
        var body: some View {
            if showGreen {
                Color.green.onAppear {
                    showGreen = false
                }
            } else {
                NBNavigationStack(path: $path) {
                    VStack(spacing: 10) {
                        NBNavigationLink(value: Item.one) { Text("one").padding() }
                        NBNavigationLink("two", value: Item.two)
                    }
                    .nbNavigationDestination(for: Item.self) { item in
                        Text("Detail \(String(describing: item))").navigationTitle(String(describing: item))
                    }
                    .navigationTitle("Runnning")
                }
            }
        }
    }
    
    

    It produces the following error message:

    NavigationBackport/DestinationBuilderHolder.swift:34: Fatal error: No view builder found for key MyApp.Item
    2022-09-27 14:49:16.499575+0200 MyApp[36633:10718781] NavigationBackport/DestinationBuilderHolder.swift:34: Fatal error: No view builder found for key MyApp.Item
    
    

    A workaround is to always start with an empty path state and then assign the correct path in an .onAppear or .task() modifier. But this is not in all use case easy to accomplish.

    opened by roland-schmitz-ocu 3
  • Add NBNavigationSplitView

    Add NBNavigationSplitView

    This PR:

    • Adds NBNavigationSplitView, attempting to overcome some limitations of #9.

    Limitations

    Some APIs related to column customisation are not available as they are not possible to backport using SwiftUI's older navigation APIs: e.g., columnVisibility, navigationSplitViewColumnWidth and navigationSplitViewStyle. Additionally, while it's possible to nest an NBNavigationStack within a NBNavigationSplitView, it should only be nested within the detail pane of the split view. Otherwise, sidebar and content screens might leak into the next pane.

    opened by johnpatrickmorgan 0
  • Could you support NavigationSplitView ?

    Could you support NavigationSplitView ?

    Hi, I want to use your library but my app support iPhone and iPad so I need to depend on NavigationSplitView not NavigationStack

    Could you support it ?

    opened by X901 1
Releases(0.7.0)
Owner
John Patrick Morgan
Swift Developer in London
John Patrick Morgan
A drop-in universal library helps you to manage the navigation bar styles and makes transition animations smooth between different navigation bar styles

A drop-in universal library helps you to manage the navigation bar styles and makes transition animations smooth between different navigation bar styles while pushing or popping a view controller for all orientations. And you don't need to write any line of code for it, it all happens automatically.

Zhouqi Mo 3.3k Dec 21, 2022
sRouting - The lightweight navigation framework for SwiftUI.

sRouting The lightweight navigation framework for SwiftUI. Overview sRouting using the native navigation mechanism in SwiftUI. It's easy to handle nav

Shiro 8 Aug 15, 2022
FlowStacks allows you to hoist SwiftUI navigation and presentation state into a Coordinator

FlowStacks allow you to manage complex SwiftUI navigation and presentation flows with a single piece of state. This makes it easy to hoist that state into a high-level coordinator view. Using this pattern, you can write isolated views that have zero knowledge of their context within the navigation flow of an app.

John Patrick Morgan 471 Jan 3, 2023
Change SwiftUI Navigation Bar Color for different View

SwiftUINavigationBarColor Change SwiftUI NavigationBar background color per screen. Usage For NavigationBarColor to work, you have to set the Navigati

Hai Feng Kao 18 Jul 15, 2022
🧭 SwiftUI navigation done right

?? NavigationKit NavigationKit is a lightweight library which makes SwiftUI navigation super easy to use. ?? Installation ?? Swift Package Manager Usi

Alex Nagy 152 Dec 27, 2022
Navigation helpers for SwiftUI applications build with ComposableArchitecture

Swift Composable Presentation ?? Description Navigation helpers for SwiftUI applications build with ComposableArchitecture. More info about the concep

Dariusz Rybicki 52 Dec 14, 2022
Make SwiftUI Navigation be easy

VNavigator VNavigator is a clean and easy-to-use navigation in SwiftUI base on UINavigationController in UIKit Installation From CocoaPods CocoaPods i

Vu Vuong 10 Dec 6, 2022
Tools for making SwiftUI navigation simpler, more ergonomic and more precise.

SwiftUI Navigation Tools for making SwiftUI navigation simpler, more ergonomic and more precise. Motivation Tools Navigation overloads Navigation view

Point-Free 1.1k Jan 1, 2023
SwiftUINavigation provides UIKit-like navigation in SwiftUI

SwiftUINavigation About SwiftUINavigation provides UIKit-like navigation in Swif

Bhimsen Padalkar 1 Mar 28, 2022
A lightweight iOS mini framework that enables programmatic navigation with SwiftUI, by using UIKit under the hood.

RouteLinkKit A lightweight iOS mini framework that enables programmatic navigation with SwiftUI. RouteLinkKit is fully compatible with native Navigati

Αθανάσιος Κεφαλάς 4 Feb 8, 2022
SwiftUINavigator: a lightweight, flexible, and super easy library which makes SwiftUI navigation a trivial task

The logo is contributed with ❤️ by Mahmoud Hussein SwiftUINavigator is a lightwe

OpenBytes 22 Dec 21, 2022
Simple iOS app to showcase navigation with coordinators in SwiftUI + MVVM.

SimpleNavigation Simple iOS app to showcase the use of the Coordinator pattern using SwiftUI and MVVM. The implementation is as easy as calling a push

Erik Lopez 7 Dec 6, 2022
Custom navigation swiftui NavigationLink NavigationView

Custom navigation swiftui Experimenting with navigation link. if you find this idea interesting you can take and expend it into a more powerful soluti

Igor 5 Dec 2, 2022
Models UI navigation patterns using TCA

Composable Navigation The Composable Navigation is a Swift Package that builds on top of The Composable Architecture (TCA, for short). It models UI na

Michael Heinzl 41 Dec 14, 2022
An iOS view-controller navigation management. No inherit, using one line code to integrate.

KGNavigationBar Example An iOS view-controller navigation management. No inherit, using one line code to integrate. 一个 iOS 控制器导航管理库. 无需继承, 一行代码即可实现集成。

VanJay 5 Sep 6, 2021
Powerful navigation in the Composable Architecture via the coordinator pattern

TCACoordinators The coordinator pattern in the Composable Architecture TCACoordinators brings a flexible approach to navigation in SwiftUI using the C

John Patrick Morgan 231 Jan 7, 2023
A wrapper for NavigationView and NavigationLink that makes programmatic navigation a little friendlier.

NavigatorKit A wrapper for NavigationView and NavigationLink that makes programmatic navigation a little friendlier. NavigatorKit is an opinionated wr

Gyuri Grell 2 Jun 16, 2022
Cordova/Phonegap plugin for launching today's most popular navigation/ride apps to navigate to a destination.

Launch Navigator Cordova/Phonegap Plugin Cordova/Phonegap plugin for launching today's most popular navigation/ride apps to navigate to a destination.

null 0 Oct 25, 2021
Simple custom navigation bar by swift

YoNavBarView Example To run the example project, clone the repo, and run pod install from the Example directory first. Requirements Installation YoNav

null 1 Nov 23, 2021