diff --git a/Sources/OpenSwiftUICore/View/IDView.swift b/Sources/OpenSwiftUICore/View/IDView.swift new file mode 100644 index 000000000..2765a4eaf --- /dev/null +++ b/Sources/OpenSwiftUICore/View/IDView.swift @@ -0,0 +1,155 @@ +// +// IDView.swift +// OpenSwiftUICore +// +// Audited for iOS 18.0 +// Status: Complete +// ID: D4C7BC89F06A89A4754FA9F578FD2C57 (SwiftUI) +// ID: ADF2FC9997986A8A2C672C0F3AA33367 (SwiftUICore) + +package import OpenGraphShims + +// MARK: - IDView + +@usableFromInline +@frozen +package struct IDView: View where Content: View, ID: Hashable { + @usableFromInline + var content: Content + + @usableFromInline + var id: ID + + @inlinable + package init(_ content: Content, id: ID) { + self.content = content + self.id = id + } + + @usableFromInline + package var body: Never { + bodyError() + } +} + +@available(*, unavailable) +extension IDView : Sendable {} + +// MARK: - IDView + View extension + +extension View { + /// Binds a view's identity to the given proxy value. + /// + /// When the proxy value specified by the `id` parameter changes, the + /// identity of the view — for example, its state — is reset. + @inlinable + nonisolated public func id(_ id: ID) -> some View where ID: Hashable { + return IDView(self, id: id) + } +} + +// MARK: - IDView + makeView implementation + +extension IDView { + @usableFromInline + package static func _makeView(view: _GraphValue, inputs: _ViewInputs) -> _ViewOutputs { + if _SemanticFeature_v2.isEnabled { + return makeImplicitRoot(view: view, inputs: inputs) + } else { + let id = view.value[offset:{ .of(&$0.id) }] + let phase = IDPhase(id: id, phase: inputs.viewPhase, lastID: nil, delta: 0) + var inputs = inputs + inputs.viewPhase = Attribute(phase) + return Content.makeDebuggableView(view: view[offset: { .of(&$0.content)}], inputs: inputs) + } + } +} + +// MARK: - IDPhase + +private struct IDPhase: StatefulRule, AsyncAttribute where ID: Hashable { + @Attribute var id: ID + + @Attribute var phase: _GraphInputs.Phase + + var lastID: ID? + + var delta: UInt32 + + init(id: Attribute, phase: Attribute<_GraphInputs.Phase>, lastID: ID?, delta: UInt32) { + self._id = id + self._phase = phase + self.lastID = lastID + self.delta = delta + } + + typealias Value = _GraphInputs.Phase + + mutating func updateValue() { + if lastID != id{ + if lastID != nil { + delta &+= 1 + } + lastID = id + } + var phase = phase + phase.resetSeed &+= delta + value = phase + } +} + +// MARK: - IDView + makeViewList implementation + +extension IDView { + @usableFromInline + package static func _makeViewList(view: _GraphValue, inputs: _ViewListInputs) -> _ViewListOutputs { + makeDynamicViewList(metadata: (), view: view, inputs: inputs) + } + + @usableFromInline + package static func _viewListCount(inputs: _ViewListCountInputs) -> Int? { + Content._viewListCount(inputs: inputs) + } +} + +// MARK: - IDView + DynamicView + +extension IDView: DynamicView { + package static var canTransition: Bool { true } + + package static var traitKeysDependOnView: Bool { false } + + package static func makeID() -> ID { preconditionFailure("") } + + package func childInfo(metadata: ()) -> (any Any.Type, ID?) { + (Content.self, id) + } + + package func makeChildView(metadata: (), view: Attribute, inputs: _ViewInputs) -> _ViewOutputs { + preconditionFailure("") + } + + package func makeChildViewList(metadata: (), view: Attribute, inputs: _ViewListInputs) -> _ViewListOutputs { + var inputs = inputs + inputs.base.pushStableID(id) + let view = _GraphValue(CachedView(view: view, id: id)) + return Content.makeDebuggableViewList(view: view, inputs: inputs) + } + + package typealias Metadata = () +} + +// MARK: - CachedView + +private struct CachedView: StatefulRule, AsyncAttribute where Content: View, ID: Hashable { + @Attribute var view: IDView + let id: ID + + typealias Value = Content + + func updateValue() { + if !hasValue || id == view.id { + value = view.content + } + } +} diff --git a/Sources/OpenSwiftUICore/View/Test/TestIDView.swift b/Sources/OpenSwiftUICore/View/Test/TestIDView.swift index 22cebae05..e55fd74f8 100644 --- a/Sources/OpenSwiftUICore/View/Test/TestIDView.swift +++ b/Sources/OpenSwiftUICore/View/Test/TestIDView.swift @@ -3,27 +3,30 @@ // OpenSwiftUICore // // Audited for iOS 18.0 -// Status: WIP +// Status: Complete // ID: CC151E1A36B4405FF56CDABA5D46BF1E import OpenGraphShims +// MARK: - TestIDView + View extension + @_spi(Testing) extension View { - nonisolated public func testID(_ id: ID) -> TestIDView where ID : Hashable { + nonisolated public func testID(_ id: ID) -> TestIDView where ID: Hashable { TestIDView(content: self, id: id) } } +// MARK: - TestIDView + @_spi(Testing) -@MainActor -@preconcurrency public struct TestIDView: PrimitiveView, UnaryView where Content: View, ID: Hashable { public var content: Content public var id: ID nonisolated public static func _makeView(view: _GraphValue, inputs: _ViewInputs) -> _ViewOutputs { - fatalError() + let view = _GraphValue(IdentifiedView(view: view.value, id: nil)) + return Content.makeDebuggableView(view: view, inputs: inputs) } public typealias Body = Never @@ -37,15 +40,15 @@ public struct TestIDView: PrimitiveView, UnaryView where Content: V self.id = id } - // TODO - typealias Value = TestIDView - + typealias Value = Content + mutating func updateValue() { - // TODO: id = view.id + id = view.id + value = view.content } func matchesIdentifier(_ identifier: I) -> Bool where I: Hashable { - compareValues(id, identifier as? ID) + (id as? I).map { $0 == identifier } == true } var description: String { diff --git a/Tests/OpenSwiftUICoreTests/View/IDViewTests.swift b/Tests/OpenSwiftUICoreTests/View/IDViewTests.swift new file mode 100644 index 000000000..83134cbcb --- /dev/null +++ b/Tests/OpenSwiftUICoreTests/View/IDViewTests.swift @@ -0,0 +1,18 @@ +// +// IDViewTests.swift +// OpenSwiftUICoreTests + +@testable import OpenSwiftUICore +import Testing +import Foundation + +@MainActor +struct IDViewTests { + @Test + func viewExtension() { + let empty = EmptyView() + _ = empty.id("1") + _ = empty.id(2) + _ = empty.id(UUID()) + } +}