From 173647a4f32b00ebc4542d503cb5cddf27feef37 Mon Sep 17 00:00:00 2001 From: Kyle Date: Sat, 22 Feb 2025 19:56:41 +0800 Subject: [PATCH] Update AppearanceActionModifier --- .../AppearanceActionModifier.swift | 258 ++++++++++++------ 1 file changed, 181 insertions(+), 77 deletions(-) diff --git a/Sources/OpenSwiftUICore/Modifier/ViewModifier/AppearanceActionModifier.swift b/Sources/OpenSwiftUICore/Modifier/ViewModifier/AppearanceActionModifier.swift index 4be2f1092..2329fbea7 100644 --- a/Sources/OpenSwiftUICore/Modifier/ViewModifier/AppearanceActionModifier.swift +++ b/Sources/OpenSwiftUICore/Modifier/ViewModifier/AppearanceActionModifier.swift @@ -1,19 +1,21 @@ // // AppearanceActionModifier.swift -// OpenSwiftUI +// OpenSwiftUICore // -// Audited for iOS 15.5 -// Status: Blocked by _makeViewList -// ID: 8817D3B1C81ADA2B53E3500D727F785A +// Audited for iOS 18.0 +// Status: Complete +// ID: 8817D3B1C81ADA2B53E3500D727F785A (SwiftUI) +// ID: 3EDE22C3B37C9BBEF12EC9D1A4B340F3 (SwiftUICore) -// MARK: - AppearanceActionModifier +package import OpenGraphShims -import OpenGraphShims +// MARK: - _AppearanceActionModifier [WIP] /// A modifier that triggers actions when its view appears and disappears. @frozen -public struct _AppearanceActionModifier: PrimitiveViewModifier { +public struct _AppearanceActionModifier: ViewModifier, PrimitiveViewModifier { public var appear: (() -> Void)? + public var disappear: (() -> Void)? @inlinable @@ -38,64 +40,122 @@ public struct _AppearanceActionModifier: PrimitiveViewModifier { inputs: _ViewListInputs, body: @escaping (_Graph, _ViewListInputs) -> _ViewListOutputs ) -> _ViewListOutputs { - preconditionFailure("TODO") + let modifier = modifier.value + let attribute: Attribute + if isLinkedOnOrAfter(.v3) { + let callbacks = MergedCallbacks( + modifier: modifier, + phase: inputs.base.phase, + box: nil + ) + attribute = Attribute(callbacks) + } else { + attribute = modifier + } + var outputs = body(_Graph(), inputs) + outputs.multiModifier(_GraphValue(attribute), inputs: inputs) + return outputs + } +} + +@available(*, unavailable) +extension _AppearanceActionModifier: Sendable {} + +// MARK: - View Extension + +extension View { + /// Adds an action to perform before this view appears. + /// + /// The exact moment that OpenSwiftUI calls this method + /// depends on the specific view type that you apply it to, but + /// the `action` closure completes before the first + /// rendered frame appears. + /// + /// - Parameter action: The action to perform. If `action` is `nil`, the + /// call has no effect. + /// + /// - Returns: A view that triggers `action` before it appears. + @inlinable + nonisolated public func onAppear(perform action: (() -> Void)? = nil) -> some View { + modifier(_AppearanceActionModifier(appear: action, disappear: nil)) + } + + /// Adds an action to perform after this view disappears. + /// + /// The exact moment that OpenSwiftUI calls this method + /// depends on the specific view type that you apply it to, but + /// the `action` closure doesn't execute until the view + /// disappears from the interface. + /// + /// - Parameter action: The action to perform. If `action` is `nil`, the + /// call has no effect. + /// + /// - Returns: A view that triggers `action` after it disappears. + @inlinable + nonisolated public func onDisappear(perform action: (() -> Void)? = nil) -> some View { + modifier(_AppearanceActionModifier(appear: nil, disappear: action)) } } // MARK: - AppearanceEffect -private struct AppearanceEffect { - @Attribute - var modifier: _AppearanceActionModifier - @Attribute - var phase: _GraphInputs.Phase +package struct AppearanceEffect: StatefulRule, RemovableAttribute { + @Attribute var modifier: _AppearanceActionModifier + @Attribute var phase: _GraphInputs.Phase var lastValue: _AppearanceActionModifier? - var isVisible: Bool = false - var resetSeed: UInt32 = 0 - var node: AnyOptionalAttribute = AnyOptionalAttribute() + var isVisible: Bool + var resetSeed: UInt32 + var node: AnyOptionalAttribute + + package init(modifier: Attribute<_AppearanceActionModifier>, phase: Attribute) { + self._modifier = modifier + self._phase = phase + self.lastValue = nil + self.isVisible = false + self.resetSeed = 0 + self.node = AnyOptionalAttribute() + } mutating func appeared() { guard !isVisible else { return } - defer { isVisible = true } - guard let lastValue, - let appear = lastValue.appear - else { return } - Update.enqueueAction(appear) + if let lastValue, let appear = lastValue.appear { + Update.enqueueAction(appear) + } + isVisible = true + let host = GraphHost.currentHost + if !host.removedState.isEmpty, isLinkedOnOrAfter(.v6) { + let weak = AnyWeakAttribute(AnyAttribute.current!) + Update.enqueueAction { + guard let attribute = weak.attribute else { return } + Self.willRemove(attribute: attribute) + } + } } - + mutating func disappeared() { guard isVisible else { return } - defer { isVisible = false } - guard let lastValue, - let disappear = lastValue.disappear - else { return } - Update.enqueueAction(disappear) + if let lastValue, let disappear = lastValue.disappear { + Update.enqueueAction(disappear) + } + isVisible = false } -} -// MARK: AppearanceEffect + StatefulRule + package typealias Value = Void -extension AppearanceEffect: StatefulRule { - typealias Value = Void - - mutating func updateValue() { + package mutating func updateValue() { if node.attribute == nil { node.attribute = .current } - -// if phase.seed != resetSeed { -// resetSeed = phase.seed -// disappeared() -// } + let latestResetSeed = phase.resetSeed + if resetSeed != latestResetSeed { + resetSeed = latestResetSeed + disappeared() + } lastValue = modifier appeared() } -} - -// MARK: AppearanceEffect + RemovableAttribute -extension AppearanceEffect: RemovableAttribute { - static func willRemove(attribute: AnyAttribute) { + package static func willRemove(attribute: AnyAttribute) { let appearancePointer = UnsafeMutableRawPointer(mutating: attribute.info.body) .assumingMemoryBound(to: AppearanceEffect.self) guard appearancePointer.pointee.lastValue != nil else { @@ -103,50 +163,94 @@ extension AppearanceEffect: RemovableAttribute { } appearancePointer.pointee.disappeared() } - - static func didReinsert(attribute: AnyAttribute) { + + package static func didReinsert(attribute: AnyAttribute) { let appearancePointer = UnsafeMutableRawPointer(mutating: attribute.info.body) .assumingMemoryBound(to: AppearanceEffect.self) guard let nodeAttribute = appearancePointer.pointee.node.attribute else { return } nodeAttribute.invalidateValue() - nodeAttribute.graph.graphHost().graphInvalidation(from: nil) + let context = nodeAttribute.graph.graphHost() + context.graphInvalidation(from: nil) } } -// MARK: - View Extension +extension _AppearanceActionModifier { + // MARK: - MergedBox -extension View { - /// Adds an action to perform before this view appears. - /// - /// The exact moment that OpenSwiftUI calls this method - /// depends on the specific view type that you apply it to, but - /// the `action` closure completes before the first - /// rendered frame appears. - /// - /// - Parameter action: The action to perform. If `action` is `nil`, the - /// call has no effect. - /// - /// - Returns: A view that triggers `action` before it appears. - @inlinable - public func onAppear(perform action: (() -> Void)? = nil) -> some View { - modifier(_AppearanceActionModifier(appear: action, disappear: nil)) + private class MergedBox { + let resetSeed: UInt32 + var count: Int32 + var lastCount: Int32 + var base: _AppearanceActionModifier + var pendingUpdate: Bool + + init(resetSeed: UInt32, count: Int32 = 0, lastCount: Int32 = 0, base: _AppearanceActionModifier = .init(), pendingUpdate: Bool = false) { + self.resetSeed = resetSeed + self.count = count + self.lastCount = lastCount + self.base = base + self.pendingUpdate = pendingUpdate + } + + func appear() { + defer { count += 1 } + guard count == 0 else { return } + guard !pendingUpdate else { + count = 0 + return + } + pendingUpdate = true + update() + } + + func update() { + Update.enqueueAction { [self] in + pendingUpdate = false + let count = count + let lastCount = lastCount + self.lastCount = count + if lastCount <= 0, count >= 0, let appear = base.appear { + appear() + } else if lastCount > 0, count <= 0, let disappear = base.disappear { + disappear() + } + } + } } - - /// Adds an action to perform after this view disappears. - /// - /// The exact moment that OpenSwiftUI calls this method - /// depends on the specific view type that you apply it to, but - /// the `action` closure doesn't execute until the view - /// disappears from the interface. - /// - /// - Parameter action: The action to perform. If `action` is `nil`, the - /// call has no effect. - /// - /// - Returns: A view that triggers `action` after it disappears. - @inlinable - public func onDisappear(perform action: (() -> Void)? = nil) -> some View { - modifier(_AppearanceActionModifier(appear: nil, disappear: action)) + + // MARK: - MergedCallbacks + + private struct MergedCallbacks: StatefulRule { + @Attribute var modifier: _AppearanceActionModifier + @Attribute var phase: _GraphInputs.Phase + var box: MergedBox? + + typealias Value = _AppearanceActionModifier + + mutating func updateValue() { + let newBox: MergedBox + if let box, box.resetSeed == phase.resetSeed { + newBox = box + } else { + newBox = MergedBox(resetSeed: phase.resetSeed) + box = newBox + } + newBox.base = modifier + let box = box! + value = _AppearanceActionModifier( + appear: { + newBox.appear() + }, + disappear: { + box.count -= 1 + guard box.count == 0, !box.pendingUpdate else { + return + } + box.update() + } + ) + } } }