124 lines
4.9 KiB
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))
|
|
}
|