Skip to content

Commit c128f45

Browse files
authored
Add onAppear and onDisappear support (#92)
* Bump OpenGraph dependency version Fix Attribute.flag setter mutating issue * Add AppearanceActionModifier implementation * Add appear test case * Fix non-Darwin platform build issue
1 parent 663b6ac commit c128f45

File tree

5 files changed

+219
-6
lines changed

5 files changed

+219
-6
lines changed

Package.resolved

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Sources/OpenSwiftUI/Core/Update/Update.swift

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,24 @@ enum Update {
7171

7272
@inline(__always)
7373
static func dispatchActions() {
74-
// FIXME
75-
for action in actions {
76-
action()
74+
guard !actions.isEmpty else {
75+
return
76+
}
77+
78+
let actions = Update.actions
79+
Update.actions = []
80+
performOnMainThread {
81+
// TODO: Signpost.postUpdateActions
82+
begin()
83+
for action in actions {
84+
let oldDepth = depth
85+
action()
86+
let newDepth = depth
87+
if newDepth != oldDepth {
88+
fatalError("Action caused unbalanced updates.")
89+
}
90+
}
91+
end()
7792
}
7893
}
7994

@@ -107,5 +122,5 @@ extension Update {
107122
// FIXME: migrate to use @_extern(c, "xx") in Swift 6
108123
extension MovableLock {
109124
@_silgen_name("_MovableLockSyncMain")
110-
static func syncMain(lock: MovableLock ,body: @escaping () -> Void)
125+
static func syncMain(lock: MovableLock, body: @escaping () -> Void)
111126
}

Sources/OpenSwiftUI/Core/View/View/ViewInputs.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,12 @@ public struct _ViewInputs {
116116
return newInputs
117117
}
118118

119+
// MARK: - base.phase
120+
@inline(__always)
121+
var phase: Attribute<_GraphInputs.Phase> {
122+
base.phase
123+
}
124+
119125
// MARK: - base.changedDebugProperties
120126

121127
@inline(__always)
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
//
2+
// AppearanceActionModifier.swift
3+
// OpenSwiftUI
4+
//
5+
// Audited for RELEASE_2021
6+
// Status: Blocked by _makeViewList
7+
// ID: 8817D3B1C81ADA2B53E3500D727F785A
8+
9+
// MARK: - AppearanceActionModifier
10+
11+
internal import OpenGraphShims
12+
13+
/// A modifier that triggers actions when its view appears and disappears.
14+
@frozen
15+
public struct _AppearanceActionModifier: PrimitiveViewModifier {
16+
public var appear: (() -> Void)?
17+
public var disappear: (() -> Void)?
18+
19+
@inlinable
20+
public init(appear: (() -> Void)? = nil, disappear: (() -> Void)? = nil) {
21+
self.appear = appear
22+
self.disappear = disappear
23+
}
24+
25+
public static func _makeView(
26+
modifier: _GraphValue<Self>,
27+
inputs: _ViewInputs,
28+
body: @escaping (_Graph, _ViewInputs) -> _ViewOutputs
29+
) -> _ViewOutputs {
30+
let effect = AppearanceEffect(modifier: modifier.value, phase: inputs.phase)
31+
let attribute = Attribute(effect)
32+
attribute.flags = [.active, .removable]
33+
return body(_Graph(), inputs)
34+
}
35+
36+
public static func _makeViewList(
37+
modifier: _GraphValue<Self>,
38+
inputs: _ViewListInputs,
39+
body: @escaping (_Graph, _ViewListInputs) -> _ViewListOutputs
40+
) -> _ViewListOutputs {
41+
fatalError("TODO")
42+
}
43+
}
44+
45+
// MARK: - AppearanceEffect
46+
47+
private struct AppearanceEffect {
48+
@Attribute
49+
var modifier: _AppearanceActionModifier
50+
@Attribute
51+
var phase: _GraphInputs.Phase
52+
var lastValue: _AppearanceActionModifier?
53+
var isVisible: Bool = false
54+
var resetSeed: UInt32 = 0
55+
var node: AnyOptionalAttribute = AnyOptionalAttribute()
56+
57+
mutating func appeared() {
58+
guard !isVisible else { return }
59+
defer { isVisible = true }
60+
guard let lastValue,
61+
let appear = lastValue.appear
62+
else { return }
63+
Update.enqueueAction(appear)
64+
}
65+
66+
mutating func disappeared() {
67+
guard isVisible else { return }
68+
defer { isVisible = false }
69+
guard let lastValue,
70+
let disappear = lastValue.disappear
71+
else { return }
72+
Update.enqueueAction(disappear)
73+
}
74+
}
75+
76+
// MARK: AppearanceEffect + StatefulRule
77+
78+
extension AppearanceEffect: StatefulRule {
79+
typealias Value = Void
80+
81+
mutating func updateValue() {
82+
#if canImport(Darwin)
83+
if node.attribute == nil {
84+
node.attribute = .current
85+
}
86+
87+
if phase.seed != resetSeed {
88+
resetSeed = phase.seed
89+
disappeared()
90+
}
91+
lastValue = modifier
92+
appeared()
93+
#else
94+
fatalError("See #39")
95+
#endif
96+
}
97+
}
98+
99+
#if canImport(Darwin) // See #39
100+
101+
// MARK: AppearanceEffect + RemovableAttribute
102+
103+
extension AppearanceEffect: RemovableAttribute {
104+
static func willRemove(attribute: OGAttribute) {
105+
let appearancePointer = UnsafeMutableRawPointer(mutating: attribute.info.body)
106+
.assumingMemoryBound(to: AppearanceEffect.self)
107+
guard appearancePointer.pointee.lastValue != nil else {
108+
return
109+
}
110+
appearancePointer.pointee.disappeared()
111+
}
112+
113+
static func didReinsert(attribute: OGAttribute) {
114+
let appearancePointer = UnsafeMutableRawPointer(mutating: attribute.info.body)
115+
.assumingMemoryBound(to: AppearanceEffect.self)
116+
guard let nodeAttribute = appearancePointer.pointee.node.attribute else {
117+
return
118+
}
119+
nodeAttribute.invalidateValue()
120+
nodeAttribute.graph.graphHost().graphInvalidation(from: nil)
121+
}
122+
}
123+
#endif
124+
125+
// MARK: - View Extension
126+
127+
extension View {
128+
/// Adds an action to perform before this view appears.
129+
///
130+
/// The exact moment that OpenSwiftUI calls this method
131+
/// depends on the specific view type that you apply it to, but
132+
/// the `action` closure completes before the first
133+
/// rendered frame appears.
134+
///
135+
/// - Parameter action: The action to perform. If `action` is `nil`, the
136+
/// call has no effect.
137+
///
138+
/// - Returns: A view that triggers `action` before it appears.
139+
@inlinable
140+
public func onAppear(perform action: (() -> Void)? = nil) -> some View {
141+
modifier(_AppearanceActionModifier(appear: action, disappear: nil))
142+
}
143+
144+
/// Adds an action to perform after this view disappears.
145+
///
146+
/// The exact moment that OpenSwiftUI calls this method
147+
/// depends on the specific view type that you apply it to, but
148+
/// the `action` closure doesn't execute until the view
149+
/// disappears from the interface.
150+
///
151+
/// - Parameter action: The action to perform. If `action` is `nil`, the
152+
/// call has no effect.
153+
///
154+
/// - Returns: A view that triggers `action` after it disappears.
155+
@inlinable
156+
public func onDisappear(perform action: (() -> Void)? = nil) -> some View {
157+
modifier(_AppearanceActionModifier(appear: nil, disappear: action))
158+
}
159+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
//
2+
// AppearanceActionModifierTests.swift
3+
// OpenSwiftUICompatibilityTests
4+
5+
import Testing
6+
7+
#if canImport(Darwin)
8+
struct AppearanceActionModifierTests {
9+
@Test
10+
func appear() async throws {
11+
struct ContentView: View {
12+
var confirmation: Confirmation
13+
14+
var body: some View {
15+
AnyView(EmptyView())
16+
.onAppear {
17+
confirmation()
18+
}
19+
}
20+
}
21+
22+
#if os(iOS)
23+
await confirmation { @MainActor confirmation in
24+
let vc = UIHostingController(rootView: ContentView(confirmation: confirmation))
25+
vc.triggerLayout()
26+
workaroundIssue87(vc)
27+
}
28+
#endif
29+
}
30+
31+
// TODO: Add disappear support and test case
32+
}
33+
#endif

0 commit comments

Comments
 (0)