| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129 |
- /// 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<Detail: View>: 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<NavigationPath>
- /// 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<NavigationPath>,
- @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<D: Codable, C: View>(
- for data: D.Type,
- @ViewBuilder destination: @escaping (D) -> C
- ) -> NavigationStack<EitherView<Detail, C>> {
- // 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<EitherView<Detail, C>>(
- previous: self,
- destination: destination
- )
- }
- /// Add a destination for a specific path element (by adding another layer of ``EitherView``).
- private init<PreviousDetail: View, NewDetail: View, Component: Codable>(
- previous: NavigationStack<PreviousDetail>,
- destination: @escaping (Component) -> NewDetail?
- ) where Detail == EitherView<PreviousDetail, NewDetail> {
- 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
- }
- }
|