WatchKit complication不更新求助:iPhone改日期后表盘组件未同步
需求概述
- iPhone端可修改目标日期
- Apple Watch的complication组件显示距离目标日期的剩余天数
- iPhone修改日期后,Watch端数据同步并持久化
当前实现
在Watch应用和complication中使用同一个OneDayState单例(@ObservedObject)展示数据:
@ObservedObject state = OneDayState.shared ... Text( state.daysUntilValue )
实际问题
- Watch应用活跃运行时:
- 应用内数据正常更新
- 返回主屏后,complication仍显示旧值
- 重启Watch后,complication显示正确值
- Watch应用未活跃时:
- complication和Watch应用均无法获取新值
期望效果
- 即使Watch应用未运行,修改iPhone端日期后也能同步新值
- complication在iPhone修改日期后立即更新
提供的OneDayState代码
class OneDayState : NSObject, ObservableObject, WCSessionDelegate { static let shared = OneDayState() // connection to the settings let session = WCSession.default // connection to the user defaults let settings = UserDefaults(suiteName: "[removed]")!; // what is watched by the UI var daysUntilValue : String { return String( Calendar.current.dateComponents( [.day], from: .now, to: theDate).day!) } // the target date @Published var theDate : Date = Date.now // setup this override init() { super.init() session.delegate = self session.activate() theDate = settings.object(forKey: "target" ) as? Date ?? Date.now; } // you seem to have to override this func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) { print("sesison activated") } // when the application context changes, we just store the new date func session(_ session: WCSession, didReceiveApplicationContext applicationContext: [String : Any]) { let newDate = applicationContext["target"] as? Date ?? Date.now; DispatchQueue.main.async { self.settings.set( newDate, forKey: "target") self.theDate = newDate; } } }
错误点分析与解决方案
核心问题1:WCSession仅在Watch应用活跃时接收数据
WCSession的didReceiveApplicationContext只有当Watch应用处于前台活跃状态时才会触发。如果Watch应用未运行或在后台,无法接收到iPhone发送的上下文更新,自然无法同步数据到UserDefaults,complication也无法获取新值。
核心问题2:Complication未监听UserDefaults变化
当前complication依赖OneDayState的@Published属性更新,但OneDayState单例只有在Watch应用启动时才会初始化。如果Watch应用未运行,单例不会被创建,complication无法感知UserDefaults的变化;即使应用在后台,complication也不会主动监听UserDefaults的更新。
核心问题3:Complication未设置主动刷新机制
Complication需要主动从共享UserDefaults读取最新值,而不是仅依赖应用内的ObservableObject更新。同时,需要让iPhone端发送数据时触发Watch complication的刷新。
具体修复步骤
1. 优化iPhone端数据发送逻辑
iPhone端发送目标日期时,不仅要更新ApplicationContext,还要处理Watch不可达的情况,同时触发Watch后台刷新:
// iPhone端代码示例 let sharedDefaults = UserDefaults(suiteName: "[你的suiteName]")! sharedDefaults.set(targetDate, forKey: "target") if WCSession.default.isReachable { do { try WCSession.default.updateApplicationContext(["target": targetDate]) } catch { print("发送上下文失败: \(error)") } } else { // 发送离线数据,Watch后台接收后更新 WCSession.default.transferUserInfo(["target": targetDate]) }
2. 修改Watch端OneDayState,支持后台接收数据
实现WCSessionDelegate的didReceiveUserInfo方法,处理iPhone离线发送的数据,并触发complication刷新:
func session(_ session: WCSession, didReceiveUserInfo userInfo: [String : Any]) { let newDate = userInfo["target"] as? Date ?? Date.now DispatchQueue.main.async { self.settings.set(newDate, forKey: "target") self.theDate = newDate // 通知所有complication刷新 CLKComplicationServer.sharedInstance().reloadComplications() } }
3. Complication直接从共享UserDefaults读取数据
不要依赖OneDayState单例,complication数据源直接读取UserDefaults,并配置刷新规则:
// Complication数据源类 class ComplicationDataSource: NSObject, CLKComplicationDataSource { let sharedDefaults = UserDefaults(suiteName: "[你的suiteName]")! func getCurrentTimelineEntry(for complication: CLKComplication, withHandler handler: @escaping (CLKComplicationTimelineEntry?) -> Void) { let targetDate = sharedDefaults.object(forKey: "target") as? Date ?? Date.now let daysUntil = Calendar.current.dateComponents([.day], from: .now, to: targetDate).day ?? 0 let textProvider = CLKSimpleTextProvider(text: "\(daysUntil)天") let template = CLKComplicationTemplateModularLargeStandardBody() template.headerTextProvider = CLKSimpleTextProvider(text: "剩余天数") template.bodyTextProvider = textProvider let entry = CLKComplicationTimelineEntry(date: Date(), complicationTemplate: template) handler(entry) } // 设置complication支持向前时间旅行 func getSupportedTimeTravelDirections(for complication: CLKComplication, withHandler handler: @escaping (CLKComplicationTimeTravelDirections) -> Void) { handler([.forward]) } // 设置刷新策略,支持应用启动时刷新 func getRefreshPolicy(for complication: CLKComplication, withHandler handler: @escaping (CLKComplicationRefreshPolicy) -> Void) { handler(.onApplicationLaunch) } }
4. 配置Watch App的Background Modes
在Watch App的Capabilities中开启Background Modes,勾选Background fetch和Remote notifications,确保Watch能在后台接收数据并刷新complication。
5. 监听UserDefaults变化,同步到@Published属性
在OneDayState中添加监听,确保应用在后台时也能感知数据更新:
override init() { super.init() session.delegate = self session.activate() theDate = settings.object(forKey: "target") as? Date ?? Date.now // 监听共享UserDefaults变化 NotificationCenter.default.addObserver(self, selector: #selector(defaultsChanged), name: UserDefaults.didChangeNotification, object: settings) } @objc func defaultsChanged() { if let newDate = settings.object(forKey: "target") as? Date { DispatchQueue.main.async { self.theDate = newDate } } } // 销毁时移除观察者 deinit { NotificationCenter.default.removeObserver(self) }
内容的提问来源于stack exchange,提问作者Darren Oakey

