You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用WCSession的sendMessage无法唤醒后台/关闭状态的iPhone App

Apple Watch唤醒后台iPhone应用失败问题排查与解决

问题描述

我正在开发一款Apple Watch应用,核心需求是唤醒iPhone端配套应用,让它通过WKWebView加载网页、截取快照后回传至手表。目前双端前台时功能完全正常,但iPhone后台运行时效果不稳定,完全关闭后直接无法运行。

我尝试用WCSession的sendMessage唤醒后台iPhone应用,查资料说这个功能是支持的,但一直没实现。不确定是因为iPhone应用没回复手表的初始消息(要等WKWebView加载完成才能回传文件,没法在replyHandler里即时回复),还是漏了plist配置?

当前代码流程:

  • Apple Watch用户点击按钮,触发ExtensionDelegate中已激活的WCSession调用sendMessage
  • iPhone端通过AppDelegate中激活的WCSession接收消息
  • 在didReceiveMessage方法里,iPhone把URL传入WKWebView并开始加载
  • WKWebView的didFinish触发后,截取网页快照,通过transferFile回传手表
  • 手表接收快照后传递到对应ViewController

相关代码

Watch端ExtensionDelegate发送消息

@objc func transmit(_ notification: Notification) {
    // The paired iPhone has to be connected via Bluetooth.
    if let session = session, session.isReachable {
        session.sendMessage(["SWTransmission": notification.userInfo as Any],
            replyHandler: { replyData in
                // handle reply from iPhone app here
                print(replyData)
            }, errorHandler: { error in
                // catch any errors here
                print(error)
        })
    } else {
        // when the iPhone is not connected via Bluetooth
    }
}

iPhone端激活WCSession

fileprivate let session: WCSession? = WCSession.isSupported() ? WCSession.default : nil

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    // Override point for customization after application launch.
   session?.delegate = self
   session?.activate()
   webView.navigationDelegate = self
   webView.scrollView.contentInsetAdjustmentBehavior = .never
    return true
}

iPhone端接收消息并加载网页

func session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: @escaping ([String : Any]) -> Void) {
    
    DispatchQueue.main.async { [self] in
    let dictionary = message["SWTransmission"] as! [String: Any]
            let link = URL(string: dictionary["URL"] as! String)!
            let request = URLRequest(url: link)
            webView.frame = CGRect(x: 0, y: 0, width: Int(((dictionary["width"] as! Double) * 1.5)), height: dictionary["height"] as! Int)
            webView.load(request)
    }
}

iPhone端截取快照并回传

func getDocumentsDirectory() -> URL {
    let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
    return paths[0]
}

func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
        
        webView.takeSnapshot(with: nil) { [self] (image, error) in
            
            let filename = getDocumentsDirectory().appendingPathComponent("webImage.jpg")
            if let data = image!.jpegData(compressionQuality: 0.8) {
                try? data.write(to: filename)
            }
            
            self.session?.transferFile(filename, metadata: nil)
            
        }
    }

Watch端接收快照

func session(_ session: WCSession, didReceive file: WCSessionFile) {
    DispatchQueue.main.async { [self] in
        do {
            NotificationCenter.default.post(name: NSNotification.Name("openSite"), object: nil, userInfo: ["imageURL": file.fileURL] as [String: Any])
        } catch {
            print(error)
        }
    }
}

问题根源与修复方案

1. 核心问题:未调用replyHandler导致唤醒失败

WCSession的sendMessage要求接收方必须在didReceiveMessage中调用replyHandler,哪怕只是回复空字典。如果不调用,系统会认为消息处理失败,后台唤醒的iPhone应用会被立刻挂起,后续WKWebView的加载和快照流程根本无法完成。

修复代码:

func session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: @escaping ([String : Any]) -> Void) {
    // 先回复手表,确保系统不挂起应用
    replyHandler(["status": "received"])
    
    DispatchQueue.main.async { [self] in
        guard let dictionary = message["SWTransmission"] as? [String: Any],
              let urlString = dictionary["URL"] as? String,
              let link = URL(string: urlString),
              let width = dictionary["width"] as? Double,
              let height = dictionary["height"] as? Int else {
            return
        }
        
        let request = URLRequest(url: link)
        webView.frame = CGRect(x: 0, y: 0, width: Int(width * 1.5), height: height)
        webView.load(request)
    }
}

2. 添加后台任务延长活跃时间

后台状态下系统会限制应用活跃时长,需要启动后台任务来保证WKWebView加载和快照完成:

var backgroundTask: UIBackgroundTaskIdentifier = .invalid

func session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: @escaping ([String : Any]) -> Void) {
    replyHandler(["status": "received"])
    
    // 启动后台任务
    backgroundTask = UIApplication.shared.beginBackgroundTask(withName: "WebSnapshotTask") {
        // 任务超时,结束后台任务
        UIApplication.shared.endBackgroundTask(self.backgroundTask)
        self.backgroundTask = .invalid
    }
    
    DispatchQueue.main.async { [self] in
        // 原有加载WKWebView的代码
    }
}

func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
    webView.takeSnapshot(with: nil) { [self] (image, error) in
        // 原有保存快照和transferFile的代码
        
        // 结束后台任务
        if backgroundTask != .invalid {
            UIApplication.shared.endBackgroundTask(backgroundTask)
            backgroundTask = .invalid
        }
    }
}

3. 配置Info.plist

添加以下键值对,确保后台加载权限:

  • 键:UIBackgroundModes,类型:Array,添加字符串项:fetch
  • 键:WKSupportsIndirectLoading,类型:Boolean,值:YES

4. 优化WCSession激活逻辑

确保iPhone端在应用激活时重新激活会话,避免会话失效:

func applicationDidBecomeActive(_ application: UIApplication) {
    session?.activate()
}

额外注意事项

  • 避免原代码中的强制解包(as!),用可选绑定处理异常,防止应用崩溃
  • 测试后台唤醒时,确保iPhone处于锁屏/后台状态,且与Watch保持蓝牙连接
  • transferFile为异步传输,手表端需正确处理文件URL的权限问题

内容的提问来源于stack exchange,提问作者David

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.05 13:34:37