简体   繁体   中英

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

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!

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 Logger . For example:

import SwiftUI
import os.log

private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "MyAppApp")

@main
struct MyAppApp: App {
    @Environment(\.scenePhase) var scenePhase

    var body: some Scene {
        WindowGroup {
            ContentView()
                .onChange(of: scenePhase) {
                    guard $0 == .background else { return }
                    
                    logger.debug("starting background task")
                                        
                    var bgTaskId: UIBackgroundTaskIdentifier = .invalid
                    
                    bgTaskId = UIApplication.shared.beginBackgroundTask(withName: "ContentView") {
                        logger.error("background task timed out")
                        guard bgTaskId != .invalid else { return }
                        UIApplication.shared.endBackgroundTask(bgTaskId)
                        bgTaskId = .invalid
                    }
                    
                    Task {
                        await UserManager.user.save()
                        
                        logger.debug("finished background task")

                        guard bgTaskId != .invalid else { return }
                        UIApplication.shared.endBackgroundTask(bgTaskId)
                        bgTaskId = .invalid
                    }
                }
        }
    }
}

class UserManager {
    static let user = User()
}
        
struct User {
    func save() async {
        logger.debug("start save")
        try? await Task.sleep(nanoseconds: 3_000_000_000)
        logger.debug("finished save")
    }
}

You could then

  • install the app on the device
  • quit Xcode
  • fire up macOS Console app
  • select your mobile device
  • make sure Console is logging “info” and “debug” messages (eg “Action” » “Include Info Messages” and “Action” » “Include Debug Messages”)
  • I personally add the “Subsystem” and “Category” columns to the log by control -clicking on the column headers in the messages and adding them
  • I always filter for the salient process, subsystem, or category (so I do not get lost in the voluminous logging output) by typing some search criteria in the upper right corner
  • tap “Start streaming"
  • launch your app on your mobile device
  • return to the Home Screen, to trigger the background process

As you can see, the save task runs correctly and all the log messages appear:

在此处输入图像描述


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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM