肇鑫的技术博客

业精于勤,荒于嬉

WKWatchConnectivityRefreshBackgroundTask is never triggered in background, but WKSnapshotRefreshBackgroundTask

Conclusion first

Currently WKWatchConnectivityRefreshBackgroundTask is only called for sure on a watchOS Simulator when the watchOS extension's WCSession is in notActivated state and the extension is not running in foreground (in background or terminated).

In real devices, it won't be called in my tests. But Apple docs says it may. So you shouldn't rely on it won't be called until Apple changes its docs.

WCSession Cores

WCSession Core

For WCSession, when it is activated, you can transfer userInfo, and when the counterpart is active, it can get the userInfo. The counterpart won't need to be in foreground to be activated, it can be in a high priority background.

Testing Results

Here are my testing results.

In Simulators and in devices

How to make WCSession notActivated?

  1. Using Xcode terminate your watchOS Extension. Xcode will send a kill signal to your WKExtension.
  2. Or don't run WCSession.activate() in your code on watchOS Extension side. As WCSession is notActivated by default.

WatchConnectivity的一些坑

func updateApplicationContext([String : Any])的传输

这个方法的传输会对数据进行优化,连续发送相同的数据,后发的不会被传输。

正如这个函数的名称那样,这个函数用于发送程序状态的信息,如果有多个状态,那么连续发送相同的,只有第一个会被发送。

与WKWatchConnectivityRefreshBackgroundTask的关系


  1. When transferring data between iOS and watchOS, there must be an activated WCSession in the sending app. That is the first condition to check before sending data.
  2. For sendMessage(_:replyHandler:errorHandler:) and sendMessageData(_:replyHandler:errorHandler:), it is needed to check isReachable to be true as well.
  3. For a watchOS app sending data to iOS, you need to check iOSDeviceNeedsUnlockAfterRebootForReachability for if the iOS device needs to unlock after rebooting.
  4. For an iOS app sending data to watchOS, you may also check isPaired and isWatchAppInstalled before sending any data.
// iOS app
class AppDelegate: UIResponder, UIApplicationDelegate, WCSessionDelegate {

    var window: UIWindow?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.
        if WCSession.isSupported() {
            let session = WCSession.default()
            session.delegate = self
            session.activate()
        }
        
        return true
    }
    
    extension AppDelegate:WCSessionDelegate {
    public func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
        guard error == nil else { fatalError(error!.localizedDescription) } 
    }
}
// view controller
class ViewController: UIViewController {
    @IBAction func sendButtonClicked(_ sender: Any) {
        if WCSession.isSupported() {
            let session = WCSession.default()
            if session.activationState == .activated {
                if session.isWatchAppInstalled && session.isPaired {
                    session.transferUserInfo(["send test":nil])
                }
            }
        }
    }
}

// watchOS app
class ExtensionDelegate: NSObject, WKExtensionDelegate {

    func applicationDidFinishLaunching() {
        // Perform any final initialization of your application.
        let session = WCSession.default()
        session.delegate = self
        session.activate()
    }

    func applicationDidBecomeActive() {
        // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
    }

    func applicationWillResignActive() {
        // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
        // Use this method to pause ongoing tasks, disable timers, etc.
    }
    
    var total = 0

    func handle(_ backgroundTasks: Set<WKRefreshBackgroundTask>) {
        // Sent when the system needs to launch the application in the background to process tasks. Tasks arrive in a set, so loop through and process each one.
        for task in backgroundTasks {
            // Use a switch statement to check the task type
            switch task {
            case let backgroundTask as WKApplicationRefreshBackgroundTask:
                // Be sure to complete the background task once you’re done.
                backgroundTask.setTaskCompleted()
            case let snapshotTask as WKSnapshotRefreshBackgroundTask:
                // Snapshot tasks have a unique completion call, make sure to set your expiration date
                snapshotTask.setTaskCompleted(restoredDefaultState: true, estimatedSnapshotExpiration: Date.distantFuture, userInfo: nil)
            case let connectivityTask as WKWatchConnectivityRefreshBackgroundTask:
                // Be sure to complete the connectivity task once you’re done.
                total += 1
                DispatchQueue.main.async {
                    if let viewController = WKExtension.shared().rootInterfaceController as? InterfaceController {
                        viewController.activeBurnedEnergyLabel.setText(String(self.total))
                    }
                }
                
                connectivityTask.setTaskCompleted()
            case let urlSessionTask as WKURLSessionRefreshBackgroundTask:
                // Be sure to complete the URL session task once you’re done.
                urlSessionTask.setTaskCompleted()
            default:
                // make sure to complete unhandled task types
                task.setTaskCompleted()
            }
        }
    }

}

extension ExtensionDelegate: WCSessionDelegate {
    public func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
        guard error == nil else { fatalError(error!.localizedDescription) }
    }
    
    /** -------------------------- Background Transfers ------------------------- */
    
    public func session(_ session: WCSession, didReceiveUserInfo userInfo: [String : Any] = [:]) {
        total += 4
        DispatchQueue.main.async {
            if let viewController = WKExtension.shared().rootInterfaceController as? InterfaceController {
                viewController.activeBurnedEnergyLabel.setText(String(self.total))
            }
        }
    }
}

WKAudioFilePlayer的播放问题

假设手表扩展中存在xishuai.mp3文件。

let url = Bundle.main.url(forResource: "xishuai", withExtension: "mp3")!
let playAsset = WKAudioFileAsset(url: url)
let playItem = WKAudioFilePlayerItem(asset: playAsset)
player = WKAudioFilePlayer(playerItem: playItem)

WKAudioFilePlayer目前在真机下,如果真机没有连接蓝牙耳机,则播放无声音。因此,不能保证作为通知声音的替代。