Skip to content

Commit 9ad5801

Browse files
authored
Save to file storage when app is about to be terminated (#2992)
* Save file storage on termination too. * wip
1 parent 6c31fd7 commit 9ad5801

File tree

4 files changed

+88
-23
lines changed

4 files changed

+88
-23
lines changed

Sources/ComposableArchitecture/Internal/NotificationName.swift

+13-2
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ public var willResignNotificationName: Notification.Name? {
2626
}
2727

2828
@_spi(Internals)
29-
public var willEnterForegroundNotificationName: Notification.Name? {
29+
public let willEnterForegroundNotificationName: Notification.Name? = {
3030
#if os(iOS) || os(tvOS) || os(visionOS)
3131
return UIApplication.willEnterForegroundNotification
3232
#elseif os(macOS)
@@ -38,7 +38,18 @@ public var willEnterForegroundNotificationName: Notification.Name? {
3838
return nil
3939
}
4040
#endif
41-
}
41+
}()
42+
43+
@_spi(Internals)
44+
public let willTerminateNotificationName: Notification.Name? = {
45+
#if os(iOS) || os(tvOS) || os(visionOS)
46+
return UIApplication.willTerminateNotification
47+
#elseif os(macOS)
48+
return NSApplication.willTerminateNotification
49+
#else
50+
return nil
51+
#endif
52+
}()
4253

4354
var canListenForResignActive: Bool {
4455
willResignNotificationName != nil

Sources/ComposableArchitecture/SharedState/PersistenceKey/AppStorageKey.swift

+14-7
Original file line numberDiff line numberDiff line change
@@ -300,16 +300,23 @@ extension AppStorageKey: PersistenceKey {
300300
else { return }
301301
didSet(self.store.value(forKey: self.key) as? Value ?? initialValue)
302302
}
303-
let willEnterForeground = NotificationCenter.default.addObserver(
304-
forName: willEnterForegroundNotificationName,
305-
object: nil,
306-
queue: nil
307-
) { _ in
308-
didSet(self.store.value(forKey: self.key) as? Value ?? initialValue)
303+
let willEnterForeground: (any NSObjectProtocol)?
304+
if let willEnterForegroundNotificationName {
305+
willEnterForeground = NotificationCenter.default.addObserver(
306+
forName: willEnterForegroundNotificationName,
307+
object: nil,
308+
queue: nil
309+
) { _ in
310+
didSet(self.store.value(forKey: self.key) as? Value ?? initialValue)
311+
}
312+
} else {
313+
willEnterForeground = nil
309314
}
310315
return Shared.Subscription {
311316
NotificationCenter.default.removeObserver(userDefaultsDidChange)
312-
NotificationCenter.default.removeObserver(willEnterForeground)
317+
if let willEnterForeground {
318+
NotificationCenter.default.removeObserver(willEnterForeground)
319+
}
313320
}
314321
}
315322
}

Sources/ComposableArchitecture/SharedState/PersistenceKey/FileStorageKey.swift

+40-14
Original file line numberDiff line numberDiff line change
@@ -81,30 +81,56 @@ public final class FileStorageKey<Value: Codable & Sendable>: PersistenceKey, @u
8181
didSet(self.load(initialValue: initialValue))
8282
}
8383
}
84-
#if canImport(AppKit) || canImport(UIKit) || canImport(WatchKit)
85-
let willResign = NotificationCenter.default.addObserver(
84+
let willResign: (any NSObjectProtocol)?
85+
if let willResignNotificationName {
86+
willResign = NotificationCenter.default.addObserver(
8687
forName: willResignNotificationName,
8788
object: nil,
8889
queue: nil
8990
) { [weak self] _ in
90-
guard
91-
let self,
92-
let workItem = self.workItem
91+
guard let self
9392
else { return }
94-
self.storage.async(execute: workItem)
95-
self.storage.async(
96-
execute: DispatchWorkItem {
97-
self.workItem?.cancel()
98-
self.workItem = nil
99-
}
100-
)
93+
performImmediately()
94+
}
95+
} else {
96+
willResign = nil
97+
}
98+
let willTerminate: (any NSObjectProtocol)?
99+
if let willTerminateNotificationName {
100+
willTerminate = NotificationCenter.default.addObserver(
101+
forName: willTerminateNotificationName,
102+
object: nil,
103+
queue: nil
104+
) { [weak self] _ in
105+
guard let self
106+
else { return }
107+
performImmediately()
101108
}
102-
#endif
109+
} else {
110+
willTerminate = nil
111+
}
103112
return Shared.Subscription {
104113
cancellable.cancel()
105-
NotificationCenter.default.removeObserver(willResign)
114+
if let willResign {
115+
NotificationCenter.default.removeObserver(willResign)
116+
}
117+
if let willTerminate {
118+
NotificationCenter.default.removeObserver(willTerminate)
119+
}
106120
}
107121
}
122+
123+
private func performImmediately() {
124+
guard let workItem = self.workItem
125+
else { return }
126+
self.storage.async(execute: workItem)
127+
self.storage.async(
128+
execute: DispatchWorkItem {
129+
self.workItem?.cancel()
130+
self.workItem = nil
131+
}
132+
)
133+
}
108134
}
109135

110136
extension FileStorageKey: Hashable {

Tests/ComposableArchitectureTests/FileStorageTests.swift

+21
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ final class FileStorageTests: XCTestCase {
7777

7878
func testWillResign() throws {
7979
guard let willResignNotificationName else { return }
80+
8081
let testScheduler = DispatchQueue.test
8182
let fileStorage = InMemoryFileStorage(scheduler: testScheduler.eraseToAnyScheduler())
8283
try withDependencies {
@@ -94,6 +95,26 @@ final class FileStorageTests: XCTestCase {
9495
}
9596
}
9697

98+
func testWillTerminate() throws {
99+
guard let willTerminateNotificationName else { return }
100+
101+
let testScheduler = DispatchQueue.test
102+
let fileStorage = InMemoryFileStorage(scheduler: testScheduler.eraseToAnyScheduler())
103+
try withDependencies {
104+
$0.defaultFileStorage = fileStorage
105+
} operation: {
106+
@Shared(.fileStorage(.fileURL)) var users = [User]()
107+
XCTAssertNoDifference(fileStorage.fileSystem.value, [.fileURL: Data()])
108+
109+
users.append(.blob)
110+
XCTAssertNoDifference(fileStorage.fileSystem.value, [.fileURL: Data()])
111+
112+
NotificationCenter.default.post(name: willTerminateNotificationName, object: nil)
113+
testScheduler.advance()
114+
try XCTAssertNoDifference(fileStorage.fileSystem.value.users(for: .fileURL), [.blob])
115+
}
116+
}
117+
97118
func testWillResignAndDebounce() async throws {
98119
guard let willResignNotificationName else { return }
99120
let testScheduler = DispatchQueue.test

0 commit comments

Comments
 (0)