Home > OS >  How do you execute code when scene goes to background using new Swift async/await concurrency model?
How do you execute code when scene goes to background using new Swift async/await concurrency model?

Time:09-05

I'm struggling to execute code when app goes to background using new async/await concurrency model in Swift. Here my code:

@main
struct TestApp: App {

  @Environment(\.scenePhase) var scenePhase

  var body: some View {
    ContentView()
  }
  .onChange(of: scenePhase) {
    if $0 == .background {
      Task {
        var bgTaskId: UIBackgroundTaskIdentifier = .invalid
        bgTaskId = UIApplication.shared.beginBackgroundTask {
          print("ran out of time")
          UIApplication.shared.endBackgroundTask(bgTaskId)
          bgTaskId = .invalid
        }

        await UserManager.user.save() // func save() async {...}

        print("executes this immediately")
        UIApplication.shared.endBackgroundTask(bgTaskId)
        bgTaskId = .invalid
      }
    }
  }
}

struct User {
  func save() async {
    print("Never calls this...")
    await Task.sleep(nanoseconds: 3_000_000_000)
  }
}

So I never see the print call in save() but I do see all the print calls in the Task.init of onChange(). Is there something about how async/await executes that makes this not possible or am I missing something here?

I am able to execute background fetches the pre-async/await way with a DispatchQueue.global().async{...} and subsequent calls to .beingBackgroundTask and endBackgroundTask within the onChange() call so I know it's something about async/await specifically. Thanks!

CodePudding user response:

This pattern works fine. But the print statements make me suspect that you must be attempting to watch this in the Xcode debugger console. But when testing background lifecycle events, you do not want to be attached to the Xcode debugger, as that artificially changes the app lifecycle.

That begs the question as to how one would watch for logging messages while not connected to Xcode. Personally, I use the unified logging system, namely enter image description here


A few unrelated observations:

  • My code snippet uses guard early exits, to avoid towers of braces.

  • I consciously avoid starting the background task in an asynchronous process. We want to avoid races between the app suspending and when the background task is created. So I create the background task before launching the asynchronous process.

  • I always make sure that the background task identifier is not .invalid already before trying to end the task. You want to avoid races between the timeout and the graceful termination.

  • It's probably prudent to supply a name for your background task.

None of these four points are terribly critical to the question at hand, but I wanted to simply demonstrate some best practices: The key observation is to (a) test outside of Xcode; and (b) use unified logging system so that you can monitor the progress. There are other approaches to monitor the progress, but this is probably easiest.

  • Related