From 55314b5189ceab6ce0b0cfdf16b0e396b17ef5b0 Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 5 May 2024 23:58:40 +0800 Subject: [PATCH 1/4] Bump OpenGraph dependency version Fix Attribute.flag setter mutating issue --- Package.resolved | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Package.resolved b/Package.resolved index 30e6d48..22a45e9 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "807d4a48456544b2827b8d4f69dc5e39b01103e9105f11c138529cc3159d18d4", + "originHash" : "7c61cf2fa8336371c9cc17d001d4e08e34b0f8b92c637a0c44f1b2cd08cd55b2", "pins" : [ { "identity" : "opengraph", @@ -7,7 +7,7 @@ "location" : "https://github.com/OpenSwiftUIProject/OpenGraph", "state" : { "branch" : "main", - "revision" : "47a81fde4bfa4092577abd29122206c19ad0cf98" + "revision" : "48ad5323175fbfdfde2287bbf26c3e5a861ae2bb" } }, { From 833728bbbf800d4403bfccb5017ed42080f9184f Mon Sep 17 00:00:00 2001 From: Kyle Date: Tue, 7 May 2024 02:01:18 +0800 Subject: [PATCH 2/4] Add AppearanceActionModifier implementation --- .../Core/View/View/ViewInputs.swift | 6 + .../Modifier/AppearanceActionModifier.swift | 152 ++++++++++++++++++ 2 files changed, 158 insertions(+) create mode 100644 Sources/OpenSwiftUI/View/Modifier/AppearanceActionModifier.swift diff --git a/Sources/OpenSwiftUI/Core/View/View/ViewInputs.swift b/Sources/OpenSwiftUI/Core/View/View/ViewInputs.swift index 9a37454..61f41bb 100644 --- a/Sources/OpenSwiftUI/Core/View/View/ViewInputs.swift +++ b/Sources/OpenSwiftUI/Core/View/View/ViewInputs.swift @@ -116,6 +116,12 @@ public struct _ViewInputs { return newInputs } + // MARK: - base.phase + @inline(__always) + var phase: Attribute<_GraphInputs.Phase> { + base.phase + } + // MARK: - base.changedDebugProperties @inline(__always) diff --git a/Sources/OpenSwiftUI/View/Modifier/AppearanceActionModifier.swift b/Sources/OpenSwiftUI/View/Modifier/AppearanceActionModifier.swift new file mode 100644 index 0000000..65bc103 --- /dev/null +++ b/Sources/OpenSwiftUI/View/Modifier/AppearanceActionModifier.swift @@ -0,0 +1,152 @@ +// +// AppearanceActionModifier.swift +// OpenSwiftUI +// +// Audited for RELEASE_2021 +// Status: WIP +// ID: 8817D3B1C81ADA2B53E3500D727F785A + +// MARK: - AppearanceActionModifier + +internal import OpenGraphShims + +/// A modifier that triggers actions when its view appears and disappears. +@frozen +public struct _AppearanceActionModifier: PrimitiveViewModifier { + public var appear: (() -> Void)? + public var disappear: (() -> Void)? + + @inlinable + public init(appear: (() -> Void)? = nil, disappear: (() -> Void)? = nil) { + self.appear = appear + self.disappear = disappear + } + + public static func _makeView( + modifier: _GraphValue, + inputs: _ViewInputs, + body: @escaping (_Graph, _ViewInputs) -> _ViewOutputs + ) -> _ViewOutputs { + let effect = AppearanceEffect(modifier: modifier.value, phase: inputs.phase) + let attribute = Attribute(effect) + attribute.flags = [.active, .removable] + return body(_Graph(), inputs) + } + + public static func _makeViewList( + modifier: _GraphValue, + inputs: _ViewListInputs, + body: @escaping (_Graph, _ViewListInputs) -> _ViewListOutputs + ) -> _ViewListOutputs { + fatalError("TODO") + } +} + +// MARK: - AppearanceEffect + +private struct AppearanceEffect { + @Attribute + var modifier: _AppearanceActionModifier + @Attribute + var phase: _GraphInputs.Phase + var lastValue: _AppearanceActionModifier? + var isVisible: Bool = false + var resetSeed: UInt32 = 0 + var node: AnyOptionalAttribute = AnyOptionalAttribute() + + mutating func appeared() { + guard !isVisible else { return } + defer { isVisible = true } + guard let lastValue, + let appear = lastValue.appear + else { return } + Update.enqueueAction(appear) + } + + mutating func disappeared() { + guard isVisible else { return } + defer { isVisible = false } + guard let lastValue, + let disappear = lastValue.disappear + else { return } + Update.enqueueAction(disappear) + } +} + +// MARK: AppearanceEffect + StatefulRule + +extension AppearanceEffect: StatefulRule { + typealias Value = Void + + mutating func updateValue() { + if node.attribute == nil { + node.attribute = .current + } + + if phase.seed != resetSeed { + resetSeed = phase.seed + disappeared() + } + lastValue = modifier + appeared() + } +} + +// MARK: AppearanceEffect + RemovableAttribute + +extension AppearanceEffect: RemovableAttribute { + static func willRemove(attribute: OGAttribute) { + let appearancePointer = UnsafeMutableRawPointer(mutating: attribute.info.body) + .assumingMemoryBound(to: AppearanceEffect.self) + guard appearancePointer.pointee.lastValue != nil else { + return + } + appearancePointer.pointee.disappeared() + } + + static func didReinsert(attribute: OGAttribute) { + 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) + } +} + +// 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 + 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 + public func onDisappear(perform action: (() -> Void)? = nil) -> some View { + modifier(_AppearanceActionModifier(appear: nil, disappear: action)) + } +} From bb0761ee1a49badc45f6ae21ad027cd7f4c92b62 Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 2 Jun 2024 17:50:14 +0800 Subject: [PATCH 3/4] Add appear test case --- Sources/OpenSwiftUI/Core/Update/Update.swift | 23 ++++++++++--- .../AppearanceActionModifierTests.swift | 33 +++++++++++++++++++ 2 files changed, 52 insertions(+), 4 deletions(-) create mode 100644 Tests/OpenSwiftUICompatibilityTests/View/Modifier/AppearanceActionModifierTests.swift diff --git a/Sources/OpenSwiftUI/Core/Update/Update.swift b/Sources/OpenSwiftUI/Core/Update/Update.swift index 8efc631..8df72ee 100644 --- a/Sources/OpenSwiftUI/Core/Update/Update.swift +++ b/Sources/OpenSwiftUI/Core/Update/Update.swift @@ -71,9 +71,24 @@ enum Update { @inline(__always) static func dispatchActions() { - // FIXME - for action in actions { - action() + guard !actions.isEmpty else { + return + } + + let actions = Update.actions + Update.actions = [] + performOnMainThread { + // TODO: Signpost.postUpdateActions + begin() + for action in actions { + let oldDepth = depth + action() + let newDepth = depth + if newDepth != oldDepth { + fatalError("Action caused unbalanced updates.") + } + } + end() } } @@ -107,5 +122,5 @@ extension Update { // FIXME: migrate to use @_extern(c, "xx") in Swift 6 extension MovableLock { @_silgen_name("_MovableLockSyncMain") - static func syncMain(lock: MovableLock ,body: @escaping () -> Void) + static func syncMain(lock: MovableLock, body: @escaping () -> Void) } diff --git a/Tests/OpenSwiftUICompatibilityTests/View/Modifier/AppearanceActionModifierTests.swift b/Tests/OpenSwiftUICompatibilityTests/View/Modifier/AppearanceActionModifierTests.swift new file mode 100644 index 0000000..2f3fc2b --- /dev/null +++ b/Tests/OpenSwiftUICompatibilityTests/View/Modifier/AppearanceActionModifierTests.swift @@ -0,0 +1,33 @@ +// +// AppearanceActionModifierTests.swift +// OpenSwiftUICompatibilityTests + +import Testing + +#if canImport(Darwin) +struct AppearanceActionModifierTests { + @Test + func appear() async throws { + struct ContentView: View { + var confirmation: Confirmation + + var body: some View { + AnyView(EmptyView()) + .onAppear { + confirmation() + } + } + } + + #if os(iOS) + await confirmation { @MainActor confirmation in + let vc = UIHostingController(rootView: ContentView(confirmation: confirmation)) + vc.triggerLayout() + workaroundIssue87(vc) + } + #endif + } + + // TODO: Add disappear support and test case +} +#endif From d906b6b7e4c4bffd7bdea9d9fe1244907c1f4256 Mon Sep 17 00:00:00 2001 From: Kyle Date: Sun, 2 Jun 2024 18:24:54 +0800 Subject: [PATCH 4/4] Fix non-Darwin platform build issue --- .../View/Modifier/AppearanceActionModifier.swift | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Sources/OpenSwiftUI/View/Modifier/AppearanceActionModifier.swift b/Sources/OpenSwiftUI/View/Modifier/AppearanceActionModifier.swift index 65bc103..aa4f14f 100644 --- a/Sources/OpenSwiftUI/View/Modifier/AppearanceActionModifier.swift +++ b/Sources/OpenSwiftUI/View/Modifier/AppearanceActionModifier.swift @@ -3,7 +3,7 @@ // OpenSwiftUI // // Audited for RELEASE_2021 -// Status: WIP +// Status: Blocked by _makeViewList // ID: 8817D3B1C81ADA2B53E3500D727F785A // MARK: - AppearanceActionModifier @@ -79,6 +79,7 @@ extension AppearanceEffect: StatefulRule { typealias Value = Void mutating func updateValue() { + #if canImport(Darwin) if node.attribute == nil { node.attribute = .current } @@ -89,9 +90,14 @@ extension AppearanceEffect: StatefulRule { } lastValue = modifier appeared() + #else + fatalError("See #39") + #endif } } +#if canImport(Darwin) // See #39 + // MARK: AppearanceEffect + RemovableAttribute extension AppearanceEffect: RemovableAttribute { @@ -114,6 +120,7 @@ extension AppearanceEffect: RemovableAttribute { nodeAttribute.graph.graphHost().graphInvalidation(from: nil) } } +#endif // MARK: - View Extension