@@ -32,7 +32,7 @@ import Foundation
32
32
/// caches being purged based on the users behavior and the memory footprint
33
33
/// used by your app has a much lower upper bound and much smaller drops.
34
34
@available ( iOS 13 . 0 , macOS 10 . 15 , tvOS 13 . 0 , watchOS 6 . 0 , visionOS 1 . 0 , * )
35
- final public class Footprint : Sendable {
35
+ final public class Footprint : @ unchecked Sendable {
36
36
37
37
/// A structure that represents the different values required for easier memory
38
38
/// handling throughout your apps lifetime.
@@ -71,10 +71,13 @@ final public class Footprint : Sendable {
71
71
/// The state describing where your app sits within the scope of its memory limit.
72
72
public let state : State
73
73
74
+ /// The state of memory pressure (aka. how close the app is to being Jetsamed/Jetisoned).
75
+ public let pressure : State
76
+
74
77
/// The time at which this snapshot was taken in monotonic milliseconds of uptime.
75
78
public let timestamp : UInt64
76
79
77
- init ( ) {
80
+ init ( memoryPressure : State = . normal ) {
78
81
var info = task_vm_info_data_t ( )
79
82
var infoCount = TASK_VM_INFO_COUNT
80
83
@@ -101,6 +104,7 @@ final public class Footprint : Sendable {
101
104
usedRatio < 0.50 ? . warning :
102
105
usedRatio < 0.75 ? . urgent :
103
106
usedRatio < 0.90 ? . critical : . terminal
107
+ pressure = memoryPressure
104
108
timestamp = {
105
109
let time = mach_absolute_time ( )
106
110
var timebaseInfo = mach_timebase_info_data_t ( )
@@ -124,19 +128,31 @@ final public class Footprint : Sendable {
124
128
/// it is best to call this API as soon as possible at startup.
125
129
public static let shared = Footprint ( )
126
130
127
- /// Notification name sent when the Footprint.Memory.state changes.
131
+ /// Notification name sent when the Footprint.Memory.state and/or
132
+ /// Footprint.Memory.pressure changes.
128
133
///
129
- /// The notification userInfo dict will contain they `.oldMemoryStateKey`
130
- /// and `.newMemoryStateKey ` keys.
131
- public static let stateDidChangeNotification : NSNotification . Name = NSNotification . Name ( " FootprintMemoryStateDidChangeNotification " )
134
+ /// The notification userInfo dict will contain they `.oldMemoryKey`,
135
+ /// .newMemoryKey` and `.changesKey ` keys.
136
+ public static let memoryDidChangeNotification : NSNotification . Name = NSNotification . Name ( " FootprintMemoryDidChangeNotification " )
132
137
133
138
/// Key for the previous value of the memory state in the the
134
139
/// `.stateDidChangeNotification` userInfo object.
135
- public static let oldMemoryStateKey : String = " oldMemoryState "
140
+ /// Type is `Footprint.Memory`.
141
+ public static let oldMemoryKey : String = " oldMemory "
136
142
137
143
/// Key for the new value of the memory statein the the `.stateDidChangeNotification`
138
- /// userInfo object.
139
- public static let newMemoryStateKey : String = " newMemoryState "
144
+ /// userInfo object. Type is `Footprint.Memory`.
145
+ public static let newMemoryKey : String = " newMemory "
146
+
147
+ /// Key for the changes of the memory in the the `.stateDidChangeNotification`
148
+ /// userInfo object. Type is `Set<ChangeType>`
149
+ public static let changesKey : String = " changes "
150
+
151
+ /// Types of changes possible
152
+ public enum ChangeType : Comparable {
153
+ case state
154
+ case pressure
155
+ }
140
156
141
157
/// Returns a copy of the current memory structure.
142
158
public var memory : Memory {
@@ -162,6 +178,13 @@ final public class Footprint : Sendable {
162
178
return _memory. state
163
179
}
164
180
181
+ /// The currently tracked memory pressure.
182
+ public var pressure : Memory . State {
183
+ _memoryLock. lock ( )
184
+ defer { _memoryLock. unlock ( ) }
185
+ return _memory. pressure
186
+ }
187
+
165
188
private init ( ) {
166
189
_memory = Memory ( )
167
190
@@ -192,7 +215,7 @@ final public class Footprint : Sendable {
192
215
}
193
216
194
217
private func heartbeat( ) {
195
- let memory = Memory ( )
218
+ let memory = Memory ( memoryPressure : currentPressureFromSource ( ) )
196
219
storeAndSendObservers ( for: memory)
197
220
#if targetEnvironment(simulator)
198
221
// In the simulator there are no memory terminations,
@@ -204,13 +227,34 @@ final public class Footprint : Sendable {
204
227
#endif
205
228
}
206
229
207
- private func update( with memory: Memory ) -> Memory ? {
230
+ private func currentPressureFromSource( ) -> Memory . State {
231
+ guard let source = _memoryPressureSource else {
232
+ return . normal
233
+ }
234
+ if source. data. contains ( . critical) {
235
+ return . critical
236
+ }
237
+ if source. data. contains ( . warning) {
238
+ return . warning
239
+ }
240
+ return . normal
241
+ }
242
+
243
+ private func update( with memory: Memory ) -> ( Memory , Set < ChangeType > ) ? {
208
244
209
245
_memoryLock. lock ( )
210
246
defer { _memoryLock. unlock ( ) }
211
247
212
248
// Verify that state changed...
213
- guard _memory. state != memory. state else {
249
+ var changeSet : Set < ChangeType > = [ ]
250
+
251
+ if _memory. state != memory. state {
252
+ changeSet. insert ( . state)
253
+ }
254
+ if _memory. pressure != memory. pressure {
255
+ changeSet. insert ( . pressure)
256
+ }
257
+ guard !changeSet. isEmpty else {
214
258
return nil
215
259
}
216
260
@@ -221,27 +265,30 @@ final public class Footprint : Sendable {
221
265
return nil
222
266
}
223
267
224
- print ( " Footprint.state changed after \( memory. timestamp - _memory. timestamp) " )
268
+ print ( " Footprint changed after \( memory. timestamp - _memory. timestamp) " )
225
269
let oldMemory = _memory
226
270
_memory = memory
227
271
228
- return oldMemory
272
+ return ( oldMemory, changeSet )
229
273
}
230
274
231
275
private func storeAndSendObservers( for memory: Memory ) {
232
276
233
- guard let oldMemory = update ( with: memory) else {
277
+ guard let ( oldMemory, changeSet ) = update ( with: memory) else {
234
278
return
235
279
}
236
280
237
281
// send all observers outside of the lock on the main queue.
238
282
// main queue is important since most of us will want to
239
283
// make changes that might touch the UI.
284
+ print ( " Footprint changes \( changeSet) " )
240
285
print ( " Footprint.state \( memory. state) " )
241
- DispatchQueue . main. sync {
242
- NotificationCenter . default. post ( name: Footprint . stateDidChangeNotification, object: nil , userInfo: [
243
- Footprint . newMemoryStateKey: memory. state,
244
- Footprint . oldMemoryStateKey: oldMemory. state
286
+ print ( " Footprint.pressure \( memory. pressure) " )
287
+ DispatchQueue . main. async {
288
+ NotificationCenter . default. post ( name: Footprint . memoryDidChangeNotification, object: nil , userInfo: [
289
+ Footprint . newMemoryKey: memory,
290
+ Footprint . oldMemoryKey: oldMemory,
291
+ Footprint . changesKey: changeSet
245
292
] )
246
293
}
247
294
}
@@ -250,7 +297,7 @@ final public class Footprint : Sendable {
250
297
private var _timerSource : DispatchSourceTimer ? = nil
251
298
private let _heartbeatInterval = 500 // milliseconds
252
299
private var _memoryLock : NSLock = NSLock ( )
253
- private var _memory : Memory
300
+ private var _memory : Memory = Memory ( )
254
301
private var _memoryPressureSource : DispatchSourceMemoryPressure ? = nil
255
302
}
256
303
@@ -261,28 +308,55 @@ import SwiftUI
261
308
extension View {
262
309
263
310
/// A SwiftUI extension providing a convenient way to observe changes in the memory
264
- /// state of the app through the `onFootprintMemoryStateDidChange ` modifier.
311
+ /// state of the app through the `onFootprintMemoryDidChange ` modifier.
265
312
///
266
313
/// ## Overview
267
314
///
268
- /// The `onFootprintMemoryStateDidChange` extension allows you to respond
269
- /// to changes in the app's memory state by providing a closure that is executed
270
- /// whenever the memory state transitions.
315
+ /// The `onFootprintMemoryDidChange` extension allows you to respond
316
+ /// to changes in the app's memory state and pressure by providing a closure that is executed
317
+ /// whenever the memory state transitions. You can also use specific modifiers for
318
+ /// state (`onFootprintMemoryStateDidChange`) or
319
+ /// pressure (`onFootprintMemoryPressureDidChange`).
271
320
///
272
321
/// ### Example Usage
273
322
///
274
323
/// ```swift
275
324
/// Text("Hello, World!")
276
- /// .onFootprintMemoryStateDidChange { newState, oldState in
325
+ /// .onFootprintMemoryDidChange { newMemory, oldMemory, changeSet in
277
326
/// print("Memory state changed from \(oldState) to \(newState)")
278
- /// // Perform actions based on the memory state change
327
+ /// // Perform actions based on the memory change
279
328
/// }
329
+ @inlinable public func onFootprintMemoryDidChange( perform action: @escaping ( _ state: Footprint . Memory , _ previousState: Footprint . Memory , _ changes: Set < Footprint . ChangeType > ) -> Void ) -> some View {
330
+ _ = Footprint . shared // make sure it's running
331
+ return onReceive ( NotificationCenter . default. publisher ( for: Footprint . memoryDidChangeNotification) ) { note in
332
+ if let changes = note. userInfo ? [ Footprint . changesKey] as? Set < Footprint . ChangeType > ,
333
+ let memory = note. userInfo ? [ Footprint . newMemoryKey] as? Footprint . Memory ,
334
+ let prevMemory = note. userInfo ? [ Footprint . oldMemoryKey] as? Footprint . Memory {
335
+ action ( memory, prevMemory, changes)
336
+ }
337
+ }
338
+ }
339
+
280
340
@inlinable public func onFootprintMemoryStateDidChange( perform action: @escaping ( _ state: Footprint . Memory . State , _ previousState: Footprint . Memory . State ) -> Void ) -> some View {
281
341
_ = Footprint . shared // make sure it's running
282
- return onReceive ( NotificationCenter . default. publisher ( for: Footprint . stateDidChangeNotification) ) { note in
283
- if let state = note. userInfo ? [ Footprint . newMemoryStateKey] as? Footprint . Memory . State ,
284
- let prevState = note. userInfo ? [ Footprint . oldMemoryStateKey] as? Footprint . Memory . State {
285
- action ( state, prevState)
342
+ return onReceive ( NotificationCenter . default. publisher ( for: Footprint . memoryDidChangeNotification) ) { note in
343
+ if let changes = note. userInfo ? [ Footprint . changesKey] as? Set < Footprint . ChangeType > ,
344
+ changes. contains ( . state) ,
345
+ let memory = note. userInfo ? [ Footprint . newMemoryKey] as? Footprint . Memory ,
346
+ let prevMemory = note. userInfo ? [ Footprint . oldMemoryKey] as? Footprint . Memory {
347
+ action ( memory. state, prevMemory. state)
348
+ }
349
+ }
350
+ }
351
+
352
+ @inlinable public func onFootprintMemoryPressureDidChange( perform action: @escaping ( _ pressure: Footprint . Memory . State , _ previousPressure: Footprint . Memory . State ) -> Void ) -> some View {
353
+ _ = Footprint . shared // make sure it's running
354
+ return onReceive ( NotificationCenter . default. publisher ( for: Footprint . memoryDidChangeNotification) ) { note in
355
+ if let changes = note. userInfo ? [ Footprint . changesKey] as? Set < Footprint . ChangeType > ,
356
+ changes. contains ( . pressure) ,
357
+ let memory = note. userInfo ? [ Footprint . newMemoryKey] as? Footprint . Memory ,
358
+ let prevMemory = note. userInfo ? [ Footprint . oldMemoryKey] as? Footprint . Memory {
359
+ action ( memory. pressure, prevMemory. pressure)
286
360
}
287
361
}
288
362
}
0 commit comments