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