/// Type to indicate the root of the NavigationStack. This is internal to prevent root accidentally showing instead /// of a detail view. struct NavigationStackRootPath: Codable {} /// A view that displays a root view and enables you to present additional views /// over the root view. /// /// Use ``navigationDestination(for:destination:)`` on this view instead of its /// children, unlike Apple's SwiftUI API. public struct NavigationStack: View { public var body: some View { if let element = elements.last { if let content = child(element) { content } else { fatalError( "Failed to find detail view for \"\(element)\", make sure you have called .navigationDestination for this type." ) } } else { Text("Empty navigation path") } } /// A binding to the current navigation path. var path: Binding /// The types handled by each destination (in the same order as their /// corresponding views in the stack). var destinationTypes: [any Codable.Type] /// Gets a recursive ``EitherView`` structure which will have a single view /// visible suitable for displaying the given path element (based on its /// type). /// /// It's implemented as a recursive structure because that's the best way to keep this /// typesafe without introducing some crazy generated pseudo-variadic storage types of /// some sort. This way we can easily have unlimited navigation destinations and there's /// just a single simple method for adding a navigation destination. var child: (any Codable) -> Detail? /// The elements of the navigation path. The result can depend on /// ``NavigationStack/destinationTypes`` which determines how the keys are /// decoded if they haven't yet been decoded (this happens if they're loaded /// from disk for persistence). var elements: [any Codable] { let resolvedPath = path.wrappedValue.path( destinationTypes: destinationTypes ) return [NavigationStackRootPath()] + resolvedPath } /// Creates a navigation stack with heterogeneous navigation state that you /// can control. /// /// - Parameters: /// - path: A ``Binding`` to the navigation state for this stack. /// - root: The view to display when the stack is empty. public init( path: Binding, @ViewBuilder _ root: @escaping () -> Detail ) { self.path = path destinationTypes = [] child = { element in if element is NavigationStackRootPath { return root() } else { return nil } } } /// Associates a destination view with a presented data type for use within /// a navigation stack. /// /// Add this view modifer to describe the view that the stack displays when /// presenting a particular kind of data. Use a ``NavigationLink`` to /// present the data. You can add more than one navigation destination /// modifier to the stack if it needs to present more than one kind of data. /// /// - Parameters: /// - data: The type of data that this destination matches. /// - destination: A view builder that defines a view to display when the /// stack's navigation state contains a value of type data. The closure /// takes one argument, which is the value of the data to present. public func navigationDestination( for data: D.Type, @ViewBuilder destination: @escaping (D) -> C ) -> NavigationStack> { // Adds another detail view by adding to the recursive structure of either views created // to display details in a type-safe manner. See NavigationStack.child for details. return NavigationStack>( previous: self, destination: destination ) } /// Add a destination for a specific path element (by adding another layer of ``EitherView``). private init( previous: NavigationStack, destination: @escaping (Component) -> NewDetail? ) where Detail == EitherView { path = previous.path destinationTypes = previous.destinationTypes + [Component.self] child = { if let previous = previous.child($0) { // Either root or previously defined destination returned a view return EitherView(previous) } else if let component = $0 as? Component, let new = destination(component) { // This destination returned a detail view for the current element return EitherView(new) } else { // Possibly a future .navigationDestination will handle this path element return nil } } } /// Attempts to compute the detail view for the given element (the type of /// the element decides which detail is shown). Crashes if no suitable detail /// view is found. func childOrCrash(for element: some Codable) -> Detail { guard let child = child(element) else { fatalError( "Failed to find detail view for \"\(element)\", make sure you have called .navigationDestination for this type." ) } return child } }