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" } }, { 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/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..aa4f14f --- /dev/null +++ b/Sources/OpenSwiftUI/View/Modifier/AppearanceActionModifier.swift @@ -0,0 +1,159 @@ +// +// AppearanceActionModifier.swift +// OpenSwiftUI +// +// Audited for RELEASE_2021 +// Status: Blocked by _makeViewList +// 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 canImport(Darwin) + if node.attribute == nil { + node.attribute = .current + } + + if phase.seed != resetSeed { + resetSeed = phase.seed + disappeared() + } + lastValue = modifier + appeared() + #else + fatalError("See #39") + #endif + } +} + +#if canImport(Darwin) // See #39 + +// 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) + } +} +#endif + +// 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)) + } +} 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