Skip to content

Commit 9c02104

Browse files
authored
Update AppearanceActionModifier (#203)
1 parent c2e1f6b commit 9c02104

File tree

1 file changed

+181
-77
lines changed

1 file changed

+181
-77
lines changed
Lines changed: 181 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,21 @@
11
//
22
// AppearanceActionModifier.swift
3-
// OpenSwiftUI
3+
// OpenSwiftUICore
44
//
5-
// Audited for iOS 15.5
6-
// Status: Blocked by _makeViewList
7-
// ID: 8817D3B1C81ADA2B53E3500D727F785A
5+
// Audited for iOS 18.0
6+
// Status: Complete
7+
// ID: 8817D3B1C81ADA2B53E3500D727F785A (SwiftUI)
8+
// ID: 3EDE22C3B37C9BBEF12EC9D1A4B340F3 (SwiftUICore)
89

9-
// MARK: - AppearanceActionModifier
10+
package import OpenGraphShims
1011

11-
import OpenGraphShims
12+
// MARK: - _AppearanceActionModifier [WIP]
1213

1314
/// A modifier that triggers actions when its view appears and disappears.
1415
@frozen
15-
public struct _AppearanceActionModifier: PrimitiveViewModifier {
16+
public struct _AppearanceActionModifier: ViewModifier, PrimitiveViewModifier {
1617
public var appear: (() -> Void)?
18+
1719
public var disappear: (() -> Void)?
1820

1921
@inlinable
@@ -38,115 +40,217 @@ public struct _AppearanceActionModifier: PrimitiveViewModifier {
3840
inputs: _ViewListInputs,
3941
body: @escaping (_Graph, _ViewListInputs) -> _ViewListOutputs
4042
) -> _ViewListOutputs {
41-
preconditionFailure("TODO")
43+
let modifier = modifier.value
44+
let attribute: Attribute<Self>
45+
if isLinkedOnOrAfter(.v3) {
46+
let callbacks = MergedCallbacks(
47+
modifier: modifier,
48+
phase: inputs.base.phase,
49+
box: nil
50+
)
51+
attribute = Attribute(callbacks)
52+
} else {
53+
attribute = modifier
54+
}
55+
var outputs = body(_Graph(), inputs)
56+
outputs.multiModifier(_GraphValue(attribute), inputs: inputs)
57+
return outputs
58+
}
59+
}
60+
61+
@available(*, unavailable)
62+
extension _AppearanceActionModifier: Sendable {}
63+
64+
// MARK: - View Extension
65+
66+
extension View {
67+
/// Adds an action to perform before this view appears.
68+
///
69+
/// The exact moment that OpenSwiftUI calls this method
70+
/// depends on the specific view type that you apply it to, but
71+
/// the `action` closure completes before the first
72+
/// rendered frame appears.
73+
///
74+
/// - Parameter action: The action to perform. If `action` is `nil`, the
75+
/// call has no effect.
76+
///
77+
/// - Returns: A view that triggers `action` before it appears.
78+
@inlinable
79+
nonisolated public func onAppear(perform action: (() -> Void)? = nil) -> some View {
80+
modifier(_AppearanceActionModifier(appear: action, disappear: nil))
81+
}
82+
83+
/// Adds an action to perform after this view disappears.
84+
///
85+
/// The exact moment that OpenSwiftUI calls this method
86+
/// depends on the specific view type that you apply it to, but
87+
/// the `action` closure doesn't execute until the view
88+
/// disappears from the interface.
89+
///
90+
/// - Parameter action: The action to perform. If `action` is `nil`, the
91+
/// call has no effect.
92+
///
93+
/// - Returns: A view that triggers `action` after it disappears.
94+
@inlinable
95+
nonisolated public func onDisappear(perform action: (() -> Void)? = nil) -> some View {
96+
modifier(_AppearanceActionModifier(appear: nil, disappear: action))
4297
}
4398
}
4499

45100
// MARK: - AppearanceEffect
46101

47-
private struct AppearanceEffect {
48-
@Attribute
49-
var modifier: _AppearanceActionModifier
50-
@Attribute
51-
var phase: _GraphInputs.Phase
102+
package struct AppearanceEffect: StatefulRule, RemovableAttribute {
103+
@Attribute var modifier: _AppearanceActionModifier
104+
@Attribute var phase: _GraphInputs.Phase
52105
var lastValue: _AppearanceActionModifier?
53-
var isVisible: Bool = false
54-
var resetSeed: UInt32 = 0
55-
var node: AnyOptionalAttribute = AnyOptionalAttribute()
106+
var isVisible: Bool
107+
var resetSeed: UInt32
108+
var node: AnyOptionalAttribute
109+
110+
package init(modifier: Attribute<_AppearanceActionModifier>, phase: Attribute<ViewPhase>) {
111+
self._modifier = modifier
112+
self._phase = phase
113+
self.lastValue = nil
114+
self.isVisible = false
115+
self.resetSeed = 0
116+
self.node = AnyOptionalAttribute()
117+
}
56118

57119
mutating func appeared() {
58120
guard !isVisible else { return }
59-
defer { isVisible = true }
60-
guard let lastValue,
61-
let appear = lastValue.appear
62-
else { return }
63-
Update.enqueueAction(appear)
121+
if let lastValue, let appear = lastValue.appear {
122+
Update.enqueueAction(appear)
123+
}
124+
isVisible = true
125+
let host = GraphHost.currentHost
126+
if !host.removedState.isEmpty, isLinkedOnOrAfter(.v6) {
127+
let weak = AnyWeakAttribute(AnyAttribute.current!)
128+
Update.enqueueAction {
129+
guard let attribute = weak.attribute else { return }
130+
Self.willRemove(attribute: attribute)
131+
}
132+
}
64133
}
65-
134+
66135
mutating func disappeared() {
67136
guard isVisible else { return }
68-
defer { isVisible = false }
69-
guard let lastValue,
70-
let disappear = lastValue.disappear
71-
else { return }
72-
Update.enqueueAction(disappear)
137+
if let lastValue, let disappear = lastValue.disappear {
138+
Update.enqueueAction(disappear)
139+
}
140+
isVisible = false
73141
}
74-
}
75142

76-
// MARK: AppearanceEffect + StatefulRule
143+
package typealias Value = Void
77144

78-
extension AppearanceEffect: StatefulRule {
79-
typealias Value = Void
80-
81-
mutating func updateValue() {
145+
package mutating func updateValue() {
82146
if node.attribute == nil {
83147
node.attribute = .current
84148
}
85-
86-
// if phase.seed != resetSeed {
87-
// resetSeed = phase.seed
88-
// disappeared()
89-
// }
149+
let latestResetSeed = phase.resetSeed
150+
if resetSeed != latestResetSeed {
151+
resetSeed = latestResetSeed
152+
disappeared()
153+
}
90154
lastValue = modifier
91155
appeared()
92156
}
93-
}
94-
95-
// MARK: AppearanceEffect + RemovableAttribute
96157

97-
extension AppearanceEffect: RemovableAttribute {
98-
static func willRemove(attribute: AnyAttribute) {
158+
package static func willRemove(attribute: AnyAttribute) {
99159
let appearancePointer = UnsafeMutableRawPointer(mutating: attribute.info.body)
100160
.assumingMemoryBound(to: AppearanceEffect.self)
101161
guard appearancePointer.pointee.lastValue != nil else {
102162
return
103163
}
104164
appearancePointer.pointee.disappeared()
105165
}
106-
107-
static func didReinsert(attribute: AnyAttribute) {
166+
167+
package static func didReinsert(attribute: AnyAttribute) {
108168
let appearancePointer = UnsafeMutableRawPointer(mutating: attribute.info.body)
109169
.assumingMemoryBound(to: AppearanceEffect.self)
110170
guard let nodeAttribute = appearancePointer.pointee.node.attribute else {
111171
return
112172
}
113173
nodeAttribute.invalidateValue()
114-
nodeAttribute.graph.graphHost().graphInvalidation(from: nil)
174+
let context = nodeAttribute.graph.graphHost()
175+
context.graphInvalidation(from: nil)
115176
}
116177
}
117178

118-
// MARK: - View Extension
179+
extension _AppearanceActionModifier {
180+
// MARK: - MergedBox
119181

120-
extension View {
121-
/// Adds an action to perform before this view appears.
122-
///
123-
/// The exact moment that OpenSwiftUI calls this method
124-
/// depends on the specific view type that you apply it to, but
125-
/// the `action` closure completes before the first
126-
/// rendered frame appears.
127-
///
128-
/// - Parameter action: The action to perform. If `action` is `nil`, the
129-
/// call has no effect.
130-
///
131-
/// - Returns: A view that triggers `action` before it appears.
132-
@inlinable
133-
public func onAppear(perform action: (() -> Void)? = nil) -> some View {
134-
modifier(_AppearanceActionModifier(appear: action, disappear: nil))
182+
private class MergedBox {
183+
let resetSeed: UInt32
184+
var count: Int32
185+
var lastCount: Int32
186+
var base: _AppearanceActionModifier
187+
var pendingUpdate: Bool
188+
189+
init(resetSeed: UInt32, count: Int32 = 0, lastCount: Int32 = 0, base: _AppearanceActionModifier = .init(), pendingUpdate: Bool = false) {
190+
self.resetSeed = resetSeed
191+
self.count = count
192+
self.lastCount = lastCount
193+
self.base = base
194+
self.pendingUpdate = pendingUpdate
195+
}
196+
197+
func appear() {
198+
defer { count += 1 }
199+
guard count == 0 else { return }
200+
guard !pendingUpdate else {
201+
count = 0
202+
return
203+
}
204+
pendingUpdate = true
205+
update()
206+
}
207+
208+
func update() {
209+
Update.enqueueAction { [self] in
210+
pendingUpdate = false
211+
let count = count
212+
let lastCount = lastCount
213+
self.lastCount = count
214+
if lastCount <= 0, count >= 0, let appear = base.appear {
215+
appear()
216+
} else if lastCount > 0, count <= 0, let disappear = base.disappear {
217+
disappear()
218+
}
219+
}
220+
}
135221
}
136-
137-
/// Adds an action to perform after this view disappears.
138-
///
139-
/// The exact moment that OpenSwiftUI calls this method
140-
/// depends on the specific view type that you apply it to, but
141-
/// the `action` closure doesn't execute until the view
142-
/// disappears from the interface.
143-
///
144-
/// - Parameter action: The action to perform. If `action` is `nil`, the
145-
/// call has no effect.
146-
///
147-
/// - Returns: A view that triggers `action` after it disappears.
148-
@inlinable
149-
public func onDisappear(perform action: (() -> Void)? = nil) -> some View {
150-
modifier(_AppearanceActionModifier(appear: nil, disappear: action))
222+
223+
// MARK: - MergedCallbacks
224+
225+
private struct MergedCallbacks: StatefulRule {
226+
@Attribute var modifier: _AppearanceActionModifier
227+
@Attribute var phase: _GraphInputs.Phase
228+
var box: MergedBox?
229+
230+
typealias Value = _AppearanceActionModifier
231+
232+
mutating func updateValue() {
233+
let newBox: MergedBox
234+
if let box, box.resetSeed == phase.resetSeed {
235+
newBox = box
236+
} else {
237+
newBox = MergedBox(resetSeed: phase.resetSeed)
238+
box = newBox
239+
}
240+
newBox.base = modifier
241+
let box = box!
242+
value = _AppearanceActionModifier(
243+
appear: {
244+
newBox.appear()
245+
},
246+
disappear: {
247+
box.count -= 1
248+
guard box.count == 0, !box.pendingUpdate else {
249+
return
250+
}
251+
box.update()
252+
}
253+
)
254+
}
151255
}
152256
}

0 commit comments

Comments
 (0)