NavigationStack.swift 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129
  1. /// Type to indicate the root of the NavigationStack. This is internal to prevent root accidentally showing instead
  2. /// of a detail view.
  3. struct NavigationStackRootPath: Codable {}
  4. /// A view that displays a root view and enables you to present additional views
  5. /// over the root view.
  6. ///
  7. /// Use ``navigationDestination(for:destination:)`` on this view instead of its
  8. /// children, unlike Apple's SwiftUI API.
  9. public struct NavigationStack<Detail: View>: View {
  10. public var body: some View {
  11. if let element = elements.last {
  12. if let content = child(element) {
  13. content
  14. } else {
  15. fatalError(
  16. "Failed to find detail view for \"\(element)\", make sure you have called .navigationDestination for this type."
  17. )
  18. }
  19. } else {
  20. Text("Empty navigation path")
  21. }
  22. }
  23. /// A binding to the current navigation path.
  24. var path: Binding<NavigationPath>
  25. /// The types handled by each destination (in the same order as their
  26. /// corresponding views in the stack).
  27. var destinationTypes: [any Codable.Type]
  28. /// Gets a recursive ``EitherView`` structure which will have a single view
  29. /// visible suitable for displaying the given path element (based on its
  30. /// type).
  31. ///
  32. /// It's implemented as a recursive structure because that's the best way to keep this
  33. /// typesafe without introducing some crazy generated pseudo-variadic storage types of
  34. /// some sort. This way we can easily have unlimited navigation destinations and there's
  35. /// just a single simple method for adding a navigation destination.
  36. var child: (any Codable) -> Detail?
  37. /// The elements of the navigation path. The result can depend on
  38. /// ``NavigationStack/destinationTypes`` which determines how the keys are
  39. /// decoded if they haven't yet been decoded (this happens if they're loaded
  40. /// from disk for persistence).
  41. var elements: [any Codable] {
  42. let resolvedPath = path.wrappedValue.path(
  43. destinationTypes: destinationTypes
  44. )
  45. return [NavigationStackRootPath()] + resolvedPath
  46. }
  47. /// Creates a navigation stack with heterogeneous navigation state that you
  48. /// can control.
  49. ///
  50. /// - Parameters:
  51. /// - path: A ``Binding`` to the navigation state for this stack.
  52. /// - root: The view to display when the stack is empty.
  53. public init(
  54. path: Binding<NavigationPath>,
  55. @ViewBuilder _ root: @escaping () -> Detail
  56. ) {
  57. self.path = path
  58. destinationTypes = []
  59. child = { element in
  60. if element is NavigationStackRootPath {
  61. return root()
  62. } else {
  63. return nil
  64. }
  65. }
  66. }
  67. /// Associates a destination view with a presented data type for use within
  68. /// a navigation stack.
  69. ///
  70. /// Add this view modifer to describe the view that the stack displays when
  71. /// presenting a particular kind of data. Use a ``NavigationLink`` to
  72. /// present the data. You can add more than one navigation destination
  73. /// modifier to the stack if it needs to present more than one kind of data.
  74. ///
  75. /// - Parameters:
  76. /// - data: The type of data that this destination matches.
  77. /// - destination: A view builder that defines a view to display when the
  78. /// stack's navigation state contains a value of type data. The closure
  79. /// takes one argument, which is the value of the data to present.
  80. public func navigationDestination<D: Codable, C: View>(
  81. for data: D.Type,
  82. @ViewBuilder destination: @escaping (D) -> C
  83. ) -> NavigationStack<EitherView<Detail, C>> {
  84. // Adds another detail view by adding to the recursive structure of either views created
  85. // to display details in a type-safe manner. See NavigationStack.child for details.
  86. return NavigationStack<EitherView<Detail, C>>(
  87. previous: self,
  88. destination: destination
  89. )
  90. }
  91. /// Add a destination for a specific path element (by adding another layer of ``EitherView``).
  92. private init<PreviousDetail: View, NewDetail: View, Component: Codable>(
  93. previous: NavigationStack<PreviousDetail>,
  94. destination: @escaping (Component) -> NewDetail?
  95. ) where Detail == EitherView<PreviousDetail, NewDetail> {
  96. path = previous.path
  97. destinationTypes = previous.destinationTypes + [Component.self]
  98. child = {
  99. if let previous = previous.child($0) {
  100. // Either root or previously defined destination returned a view
  101. return EitherView(previous)
  102. } else if let component = $0 as? Component, let new = destination(component) {
  103. // This destination returned a detail view for the current element
  104. return EitherView(new)
  105. } else {
  106. // Possibly a future .navigationDestination will handle this path element
  107. return nil
  108. }
  109. }
  110. }
  111. /// Attempts to compute the detail view for the given element (the type of
  112. /// the element decides which detail is shown). Crashes if no suitable detail
  113. /// view is found.
  114. func childOrCrash(for element: some Codable) -> Detail {
  115. guard let child = child(element) else {
  116. fatalError(
  117. "Failed to find detail view for \"\(element)\", make sure you have called .navigationDestination for this type."
  118. )
  119. }
  120. return child
  121. }
  122. }