import Foundation // MARK: - NavigationPath nonisolated public struct NavigationPath: Equatable { /// The erased destination values, root-most first. Consumed by /// `NavigationView`'s path-driven initializer. @_spi(Portico) public private(set) var elements: [AnyHashable] /// Creates an empty path. public init() { self.elements = [] } /// Creates a path from a sequence of homogeneous destination values. public init(_ elements: S) where S.Element: Hashable { self.elements = elements.map { AnyHashable($0) } } /// Creates a path from already-erased values, used when writing a truncated /// stack back into the binding. @_spi(Portico) public init(erased elements: [AnyHashable]) { self.elements = elements } /// The number of destinations on the path. public var count: Int { elements.count } /// Whether the path presents no destinations. public var isEmpty: Bool { elements.isEmpty } /// Appends a destination value of any `Hashable` type. public mutating func append(_ value: V) { elements.append(AnyHashable(value)) } /// Removes the last `k` destinations, clamped to the path's length. /// /// Unlike SwiftUI's `NavigationPath.removeLast(_:)`, an out-of-range `k` /// clamps instead of trapping: a widget-initiated pop and a programmatic /// `removeLast` can observe the same path in either order, and a crash is /// the wrong outcome for that race. public mutating func removeLast(_ k: Int = 1) { elements.removeLast(min(max(k, 0), elements.count)) } } // MARK: - CodableRepresentation extension NavigationPath { /// A `Codable` snapshot of a path whose every element is `Codable`. /// /// Encodes as a flat unkeyed container of `2 * count` strings, root-most /// element first: each element contributes its mangled type name followed /// by its UTF-8 JSON encoding. Decoding resolves each type name through /// `_typeByName`, so a type that was renamed, moved module, or is absent /// from the decoding binary fails with `DecodingError.dataCorrupted`. public struct CodableRepresentation: Codable { let elements: [AnyHashable] init(elements: [AnyHashable]) { self.elements = elements } public init(from decoder: Decoder) throws { var container = try decoder.unkeyedContainer() var decoded: [AnyHashable] = [] while !container.isAtEnd { let name = try container.decode(String.self) let json = try container.decode(String.self) guard let resolved = _typeByName(name) as? any (Decodable & Hashable).Type else { throw DecodingError.dataCorrupted( .init(codingPath: container.codingPath, debugDescription: "NavigationPath: unknown element type \(name)") ) } decoded.append(try _decodeNavigationElement(resolved, from: Data(json.utf8))) } self.elements = decoded } public func encode(to encoder: Encoder) throws { var container = encoder.unkeyedContainer() for element in elements { guard let encodable = element.base as? any Encodable, let name = _mangledTypeName(type(of: element.base)) else { throw EncodingError.invalidValue( element.base, .init(codingPath: container.codingPath, debugDescription: "NavigationPath: element is not Encodable") ) } try container.encode(name) try container.encode(String(decoding: JSONEncoder().encode(encodable), as: UTF8.self)) } } } /// A `Codable` snapshot, or `nil` when any element's type is not `Codable` /// (matching SwiftUI, where one non-codable element makes the whole path /// unserializable rather than partially serializable). public var codable: CodableRepresentation? { for element in elements { guard element.base is any Codable, _mangledTypeName(type(of: element.base)) != nil else { return nil } } return CodableRepresentation(elements: elements) } /// Restores a path from a decoded snapshot. public init(_ codable: CodableRepresentation) { self.elements = codable.elements } } /// Decodes one erased element. A free generic function so that passing an /// `any (Decodable & Hashable).Type` opens the existential into `T` (SE-0352 /// implicit existential opening); the metatype cannot be handed to /// `JSONDecoder.decode` directly. private func _decodeNavigationElement( _ type: T.Type, from data: Data ) throws -> AnyHashable { AnyHashable(try JSONDecoder().decode(T.self, from: data)) }