Hello,
I'm facing problems when attempting to update my watchOS complication when relevant data on the iPhone app changes.
From what I gather reading the documentation I have to use the Watch Connectivity Framework to send said new data from the phone to the watch:
- use transferCurrentComplicationUserInfo() to send a dictionary of data from the phone to the watch
- implement the didReceiveUserInfo delegate method to handle incoming data on the watch
- in said handler, save the incoming data to UserDefaults using an App Group so the widget-extension can read that data
- after saving the data to UserDefaults, call WidgetCenter.shared.reloadAllTimelines() so watchOS can request fresh timelines for my complications
- change the getTimeline() method of my TimelineProvider so it uses the received data from UserDefaults OR async fetch fresh data if received data from phone is too old
If I understand correctly, transferCurrentComplicationUserInfo() is limited to be used a maximum of 50 times a day. I'm running the apps in debug mode, so this should be no problem.
Here is my current implementation:
1 : Setup of my WC class:
final class Connectivity: NSObject
{
// singleton approach
static let shared = Connectivity()
// used to rate limit transmissions from phone → watch
private var lastSentBalanceContext: Date? = nil
private override init()
{
super.init()
// no need to check availability on watchOS
#if !os(watchOS)
guard WCSession.isSupported() else { return }
#endif
WCSession.default.delegate = self
WCSession.default.activate()
}
}
2 : The method enabling transmission from phone to watch:
#if os(iOS)
extension Connectivity: WCSessionDelegate
{
func sendBalanceContext(sample: HealthData)
{
guard WCSession.default.activationState == .activated else { return }
guard WCSession.default.isWatchAppInstalled else { return }
// rate limitat transmissions
guard self.lastSentBalanceContext == nil || abs(Date.now.timeIntervalSince(self.lastSentBalanceContext!)) > 10
else { return }
if WCSession.default.remainingComplicationUserInfoTransfers > 0
{
WCSession.default.transferCurrentComplicationUserInfo([
"context": "balance",
"date": sample.date,
"burnedActive": sample.burnedActive,
// more data...
])
self.lastSentBalanceContext = .now
}
}
// boilerplate handlers here
}
#endif
3 : Delegete method that handles incoming data on the watch:
#if os(watchOS)
extension Connectivity: WCSessionDelegate
{
func session(_ session: WCSession, didReceiveUserInfo userInfo: [String : Any] = [:])
{
guard let context = userInfo["context"] as? String,
context == "balance"
else { return }
guard let date = userInfo["date"] as? Date,
let burnedActive = userInfo["burnedActive"] as? Int
/* more data... */
else { return }
guard let SharedDefaults = UserDefaults(suiteName: "group.4DXABR577J.com.count.kcal.app")
else { return }
// TimelineProvider uses this to determine wether to use this data or fetch data on its own
SharedDefaults.set(Date.now, forKey: "lastReceivedBalanceContext")
SharedDefaults.set(date, forKey: "date")
SharedDefaults.set(burnedActive, forKey: "burnedActive")
// more data...
WidgetCenter.shared.reloadAllTimelines()
}
// boilerplate handlers
}
#endif
4 : Finally, the TimelineProvider:
struct HealthDataEntry: TimelineEntry
{
let date: Date
let data: HealthData
}
struct HealthDataTimelineProvider: TimelineProvider
{
// other callbacks here
func getTimeline(in context: Context, completion: @escaping (Timeline<HealthDataEntry>) -> Void)
{
let SharedDefaults: UserDefaults = UserDefaults(suiteName: "group.4DXABR577J.com.count.kcal.app")!
let nextUpdateDate = Calendar.current.date(byAdding: .minute, value: 15, to: .now)!
// use data from phone if it is less than 60 seconds old
if let lastReceivedBalanceContext = SharedDefaults.object(forKey: "lastReceivedBalanceContext") as? Date
{
let interval = lastReceivedBalanceContext.timeIntervalSince(.now)
if interval > -60 && interval <= 0
{
let data = HealthData(date: SharedDefaults.object(forKey: "date") as? Date ?? Date(timeIntervalSinceReferenceDate: 0),
burnedActive: SharedDefaults.integer(forKey: "burnedActive"),
burnedActive7: SharedDefaults.integer(forKey: "burnedActive7") /* other data ... */)
let timeline = Timeline(
entries: [HealthDataEntry(date: .now, data: data)],
policy: .after(nextUpdateDate)
)
completion(timeline)
return
}
}
// default: fetch from HealthKit (if received data from phone is > 60s)
Task
{
let timeline = Timeline(
entries: [HealthDataEntry(date: .now, data: try! await asyncFetchData())],
policy: .after(nextUpdateDate)
)
completion(timeline)
}
}
}
The issue I am facing is that the watchOS complication only gets refreshed when I acitvely build and run the watchOS app in Xcode and then initiate a transmission of data to the watch. This works even if I do it back to back to back. As soon as I stop the watchOS app from within Xcode, my complications won't update anymore.
I noticed this behavior when I used print() statements throughout my code to see whether it is beeing executed as expected. The iPhone sends data, the watch receives it but then the watch fails to update the complications ONLY when not running from Xcode.
Can you spot any flaws in my implementation or in my understanding? Maybe transferCurrentComplicationUserInfo() just isn't as reliable as I think it should be? I interpreted it as being practically guaranteed to refresh the complications 50 times a day, pretty much instantly?
Any help or guidance would be greatly appreciated!