portico/Sources/Portico/Navigation/NavigationPath.swift

124 lines
4.9 KiB
Swift

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<S: Sequence>(_ 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<V: Hashable>(_ 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<T: Decodable & Hashable>(
_ type: T.Type, from data: Data
) throws -> AnyHashable {
AnyHashable(try JSONDecoder().decode(T.self, from: data))
}