NavigationPath.swift 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. import Foundation
  2. /// A type-erased list of data representing the content of a navigation stack.
  3. ///
  4. /// If you are persisting a path using the `Codable` implementation, you must
  5. /// not change type definitions in a non-backwards compatible way. Otherwise,
  6. /// the path may fail to decode.
  7. public struct NavigationPath {
  8. /// A storage class used so that we have control over exactly which changes
  9. /// are published (to avoid infinite loops).
  10. private class Storage {
  11. /// An entry that will be decoded next time it is used by a
  12. /// ``NavigationStack`` (we need to wait until we know what concrete
  13. /// entry types are available).
  14. struct EncodedEntry: Codable {
  15. var type: String
  16. var value: Data
  17. }
  18. /// The current path.
  19. ///
  20. /// If both this and ``encodedEntries`` are non-empty, the elements in
  21. /// `path` were added before the navigation path was even used to render
  22. /// a view. By design they come after the `encodedEntries` (because they
  23. /// can only be the result of appending and maybe popping).
  24. var path: [any Codable] = []
  25. /// Entries that will be decoded when this navigation path is first used
  26. /// by a ``NavigationStack``.
  27. ///
  28. /// It is not possible to decode the entries without first knowing what
  29. /// types the path can possibly contain (which only the
  30. /// ``NavigationStack`` will know).
  31. var encodedEntries: [EncodedEntry] = []
  32. }
  33. /// The path and any elements waiting to be decoded are stored in a class so
  34. /// that changes are triggered from within ``NavigationStack`` instead of
  35. /// ``NavigationPath`` when decoding the elements (which avoids an infinite
  36. /// loop of updates).
  37. private var storage = Storage()
  38. /// Indicates whether this path is empty.
  39. var isEmpty: Bool {
  40. storage.encodedEntries.isEmpty && storage.path.isEmpty
  41. }
  42. /// The number of elements in the path.
  43. var count: Int {
  44. storage.encodedEntries.count + storage.path.count
  45. }
  46. /// Creates an empty navigation path.
  47. public init() {}
  48. /// Appends a new value to the end of the path.
  49. ///
  50. /// - Parameter component: The component to append.
  51. public mutating func append(_ component: some Codable) {
  52. storage.path.append(component)
  53. }
  54. /// Removes values from the end of this path.
  55. ///
  56. /// - Precondition: `k >= 0`.
  57. ///
  58. /// - Parameter k: The number of elements to remove from the path.
  59. public mutating func removeLast(_ k: Int = 1) {
  60. precondition(k >= 0, "`k` must be greater than or equal to zero")
  61. if k < storage.path.count {
  62. storage.path.removeLast(k)
  63. } else if k < count {
  64. storage.encodedEntries.removeLast(k - storage.path.count)
  65. storage.path.removeAll()
  66. } else {
  67. removeAll()
  68. }
  69. }
  70. /// Removes all values from this path.
  71. public mutating func removeAll() {
  72. storage.path.removeAll()
  73. storage.encodedEntries.removeAll()
  74. }
  75. /// Gets the path's current entries.
  76. ///
  77. /// If the path was decoded from a stored representation and has not been
  78. /// used by a ``NavigationStack`` yet, the `destinationTypes` will be used
  79. /// to decode all elements in the path. Without knowing the
  80. /// `destinationTypes`, the entries cannot be decoded (after macOS 11 they
  81. /// can be decoded by using `_typeByName`, but we can't use that because of
  82. /// backwards compatibility).
  83. ///
  84. /// - Parameter destinationTypes: The types to use to decode the entries.
  85. /// - Returns: The decoded entries.
  86. func path(destinationTypes: [any Codable.Type]) -> [any Codable] {
  87. guard !storage.encodedEntries.isEmpty else {
  88. return storage.path
  89. }
  90. var decodedEntries: [Int: any Codable] = [:]
  91. for destinationType in destinationTypes {
  92. let type = String(reflecting: destinationType)
  93. for (i, entry) in storage.encodedEntries.enumerated() where entry.type == type {
  94. do {
  95. func decode<T: Codable>(_ type: T.Type, from data: Data) throws -> any Codable {
  96. return try JSONDecoder().decode(type, from: data)
  97. }
  98. let value = try decode(destinationType, from: entry.value)
  99. decodedEntries[i] = value
  100. } catch {
  101. let data = "<decoding error>"
  102. fatalError("Failed to decode item in encoded navigation path: '\(data)'")
  103. }
  104. }
  105. }
  106. var entries: [any Codable] = []
  107. for i in 0..<storage.encodedEntries.count {
  108. guard let entry = decodedEntries[i] else {
  109. // This should not be possible to reach
  110. fatalError("Failed to decode navigation path")
  111. }
  112. entries.append(entry)
  113. }
  114. storage.encodedEntries = []
  115. storage.path = entries + storage.path
  116. return storage.path
  117. }
  118. }
  119. extension NavigationPath: Codable {
  120. public init(from decoder: Decoder) throws {
  121. let container = try decoder.singleValueContainer()
  122. let entries = try container.decode([Storage.EncodedEntry].self)
  123. guard !entries.isEmpty else {
  124. return
  125. }
  126. storage.encodedEntries = entries
  127. }
  128. public func encode(to encoder: Encoder) throws {
  129. var container = encoder.singleValueContainer()
  130. // Combine any remaining encoded entries with the current decoded entries in the path.
  131. var entries = storage.encodedEntries
  132. entries += storage.path.map { entry in
  133. let type = String(reflecting: type(of: entry))
  134. let value: Data
  135. do {
  136. value = try JSONEncoder().encode(entry)
  137. } catch {
  138. fatalError("Failed to encode navigation path entry of type \(type)")
  139. }
  140. return Storage.EncodedEntry(
  141. type: type,
  142. value: value
  143. )
  144. }
  145. try container.encode(entries)
  146. }
  147. }