Combine: how to make sure publisher run to completion even if app is put to background/killed and how to force any pending publish event before app dying?

I'm trying to use this Published property wrapper extension for persisting data to UserDefaults. This is how it does persist:

        projectedValue
            .sink { newValue in
                print("In .sink, saving data")
                let data = try? JSONEncoder().encode(newValue)
                _store.set(data, forKey: key)
            }
            .store(in: &cancellableSet)

there are two problems here I want to fix: 1) don't want to do persist on every change. 2) app can be background'ed or killed while persisting, which can cause data lost. .debounce was suggested as a solution:

        projectedValue
            // slow down event publish to not persist on every changes
//            .debounce(for: .seconds(10), scheduler: RunLoop.main)
            // is .background here mean the same as beginBackgroundTask()?
            .debounce(for: .seconds(10), scheduler: DispatchQueue.main, options: .init(qos: .background))
            .sink { newValue in
                print("In .sink, saving data")
                let data = try? JSONEncoder().encode(newValue)
                _store.set(data, forKey: key)
            }
            .store(in: &cancellableSet)

So I found if I use DispatchQueue.main, I can pass SchedulerOptions, qos: .background. I assume here .backgroun mean make the execution thread like beginBackgroundTask()? If it's, then it solve both of my problems. But then there is a new problem of app getting background'ed/killed while .debounce() has pending event before dueTime is up yet.

Is there anyway to force the debounce publisher publish any pending event and not wait?

If this is possible, then I can force this to happen in SwiftUI.App.body when .scenePhase change to .background. Right now I’m duplicating the persist logic and running it there.

No, .background has nothing to do with background operation. You'll want to look into taking background tasks for your persistence engine. Apple's documentation and forums may help, as well as this post from Quinn (@eskimo).

So I can use RunLoop.main and bracket the code in .sink() with beginBackgroundTask()/endBackgroundTask(_:)?

Since UserDefaults.set() says the actual persist to storage is "run asynchronously", is there any mechanism to "wait/force write" synchronize to make sure data is actually saved before endBackgroundTask(_:)?

Or the OS make sure guarantees UserDefault.set() does the save?

synchronize()

Waits for any pending asynchronous updates to the defaults database and returns; this method is unnecessary and shouldn't be used.

Maybe I don't have to worry about asynchronous

@main
struct PersistentPublishedApp: App {
    @Environment(\.scenePhase) private var phase
    @StateObject var model = DataModel()

    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(model)
                .onChange(of: phase) {
                    switch $0 {
                    case .active:
                        break
                    case .inactive:
                        break
                    case .background:
// will the OS let me finish and not kill me?
                        model.save()
                        break
                    @unknown default:
                        break
                    }
                }
        }
    }
}

will the OS let me finish and not kill me?

My test show OS give about 10seconds before killing the app.

You can’t custom block the app even in background to publish events if the app is getting killed. If the app is crashing you get some 4 odd seconds to publish the events caught by NSUncaughtExceptionHandler and if most cases they won’t be sufficient to post the event.

Best option is to persist the data to the local storage and publish the data on next launch of the app.

Terms of Service

Privacy Policy

Cookie Policy