1
1
//
2
2
// AppearanceActionModifier.swift
3
- // OpenSwiftUI
3
+ // OpenSwiftUICore
4
4
//
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)
8
9
9
- // MARK: - AppearanceActionModifier
10
+ package import OpenGraphShims
10
11
11
- import OpenGraphShims
12
+ // MARK: - _AppearanceActionModifier [WIP]
12
13
13
14
/// A modifier that triggers actions when its view appears and disappears.
14
15
@frozen
15
- public struct _AppearanceActionModifier : PrimitiveViewModifier {
16
+ public struct _AppearanceActionModifier : ViewModifier , PrimitiveViewModifier {
16
17
public var appear : ( ( ) -> Void ) ?
18
+
17
19
public var disappear : ( ( ) -> Void ) ?
18
20
19
21
@inlinable
@@ -38,115 +40,217 @@ public struct _AppearanceActionModifier: PrimitiveViewModifier {
38
40
inputs: _ViewListInputs ,
39
41
body: @escaping ( _Graph , _ViewListInputs ) -> _ViewListOutputs
40
42
) -> _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) )
42
97
}
43
98
}
44
99
45
100
// MARK: - AppearanceEffect
46
101
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
52
105
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
+ }
56
118
57
119
mutating func appeared( ) {
58
120
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
+ }
64
133
}
65
-
134
+
66
135
mutating func disappeared( ) {
67
136
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
73
141
}
74
- }
75
142
76
- // MARK: AppearanceEffect + StatefulRule
143
+ package typealias Value = Void
77
144
78
- extension AppearanceEffect : StatefulRule {
79
- typealias Value = Void
80
-
81
- mutating func updateValue( ) {
145
+ package mutating func updateValue( ) {
82
146
if node. attribute == nil {
83
147
node. attribute = . current
84
148
}
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
+ }
90
154
lastValue = modifier
91
155
appeared ( )
92
156
}
93
- }
94
-
95
- // MARK: AppearanceEffect + RemovableAttribute
96
157
97
- extension AppearanceEffect : RemovableAttribute {
98
- static func willRemove( attribute: AnyAttribute ) {
158
+ package static func willRemove( attribute: AnyAttribute ) {
99
159
let appearancePointer = UnsafeMutableRawPointer ( mutating: attribute. info. body)
100
160
. assumingMemoryBound ( to: AppearanceEffect . self)
101
161
guard appearancePointer. pointee. lastValue != nil else {
102
162
return
103
163
}
104
164
appearancePointer. pointee. disappeared ( )
105
165
}
106
-
107
- static func didReinsert( attribute: AnyAttribute ) {
166
+
167
+ package static func didReinsert( attribute: AnyAttribute ) {
108
168
let appearancePointer = UnsafeMutableRawPointer ( mutating: attribute. info. body)
109
169
. assumingMemoryBound ( to: AppearanceEffect . self)
110
170
guard let nodeAttribute = appearancePointer. pointee. node. attribute else {
111
171
return
112
172
}
113
173
nodeAttribute. invalidateValue ( )
114
- nodeAttribute. graph. graphHost ( ) . graphInvalidation ( from: nil )
174
+ let context = nodeAttribute. graph. graphHost ( )
175
+ context. graphInvalidation ( from: nil )
115
176
}
116
177
}
117
178
118
- // MARK: - View Extension
179
+ extension _AppearanceActionModifier {
180
+ // MARK: - MergedBox
119
181
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
+ }
135
221
}
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
+ }
151
255
}
152
256
}
0 commit comments