Combining Watchconnectivity and Complications

Combining WatchConnectivity and Complications

The main issue is that you're trying to make an asynchronous call within your complication controller.

The code following your sendMessage: call will be executed before your reply handler has even gotten a response. This is why your complication shows "not" as the template's text has been set, before you have received a reply.

Sometime later, after getCurrentTimelineEntryForComplication has returned, sendMessage will receive a response and call the reply hander, which will merely set respondedString, then exit that block.

What you should avoid doing:

You should consider Apple's recommendations, and not try to fetch any data within the complication controller.

The job of your data source class is to provide ClockKit with any requested data as quickly as possible. The implementations of your data source methods should be minimal. Do not use your data source methods to fetch data from the network, compute values, or do anything that might delay the delivery of that data. If you need to fetch or compute the data for your complication, do it in your iOS app or in other parts of your WatchKit extension, and cache the data in a place where your complication data source can access it. The only thing your data source methods should do is take the cached data and put it into the format that ClockKit requires.

Also, any activity you perform within your data source will needlessly use up the daily execution time budget that is allotted to your complication.

How can you provide data to your complication?

Apple provides a Watch Connectivity transferCurrentComplicationUserInfo method which will immediately transfer (a dictionary of) complication info from the phone to the watch.

When your iOS app receives updated data intended for your complication, it can use the Watch Connectivity framework to update your complication right away. The transferCurrentComplicationUserInfo: method of WCSession sends a high priority message to your WatchKit extension, waking it up as needed to deliver the data. Upon receiving the data, extend or reload your timeline as needed to force ClockKit to request the new data from your data source.

On the watch side, you have your WCSessionDelegate handle didReceiveUserInfo and use the data you received to update your complication:

func session(session: WCSession, didReceiveUserInfo userInfo: [String : AnyObject]) {
if let ... { // Retrieve values from dictionary

// Update complication
let complicationServer = CLKComplicationServer.sharedInstance()
guard let activeComplications = complicationServer.activeComplications else { // watchOS 2.2
return
}

for complication in activeComplications {
complicationServer.reloadTimelineForComplication(complication)
}
}
}

Apple engineers generally recommend setting up a data manager to hold the data. In your complication controller, you would retrieve the latest information from the data manager to use for your timeline.

There are several existing projects on GitHub which use this approach.

If you still prefer to request data from the watch side:

You'd want to move your WCSession code out of the complication controller, into the watch extension, and activate it as part of the WKExtension init.

The key is to have the reply handler manually update the complication once the data is received.

When your session delegate's reply handler is called, you can use the update complication code I supplied earlier to reload your complication's timeline.

If you use scheduled complication updates to trigger this, the downside to that particular approach is that you'll be performing two updates. The first update would initiate the request for data, but not have any new data to use. The second (manual) update happens after the data is received, and this is when the new data would appear on the timeline.

This is why the approach of supplying data in the background from the phone works better, as it only requires one update.

Is transferCurrentComplicationUserInfo more suitable for complication update?

The distinction between these two WCSession methods involve when the data is sent, and whether the watchkit extension is woken up or not.

transferCurrentComplicationUserInfo: is specifically designed for transferring complication user info meant to be shown on the watch face right now.

  • The complication user info is marked "Urgent", and is placed at the front of the queue,
  • the watch wakes the extension in the background to receive the info, and
  • the transfer happens immediately. (Other queued information might also transfer at that point.)

transferUserInfo: queues up information, to be transferred when the system determines it's a good time to process the queue:

  • The user info is placed at the back of the queue,
  • the transferred information is stored if the extension is not awake,
  • the transfer does not happen immediately, and
  • the information is delivered in the order in which they were sent.

More details can be found in the WWDC 2015 Introducing Watch Connectivity video.

Update for iOS 10:

In iOS 10, WCSession adds a remainingComplicationUserInfoTransfers property which can affect which method that iOS will use to transfer the user info:

The number of remaining times that you can call transferCurrentComplicationUserInfo: during the current day. If this property is set to 0, any additional calls to transferCurrentComplicationUserInfo: use transferUserInfo: instead.

If the complication is on the active watch face, you are given 50 transfers a day. If the complication is not active, this property defaults to 0.

how to wake up iOS parent app with sendMessage from complicationController

The main problem is that you're trying to include (nested) asynchronous calls within your complication data source. However, your requested update will have reached the end of its method, and no timeline update will actually take place (since you didn't reload or extend the timeline, and even if you had, no new data would have been received in time for the current update).

Since no new data would be available for the scheduled update, you'd have to perform a second update to use the new data once it was received. Performing two back-to-back updates is not only unnecessary, it wastes more of your daily complication budget.

Apple recommends that you fetch and cache the data in advance of the update, so the complication data source can directly return the requested data to the complication server.

The job of your data source class is to provide ClockKit with any requested data as quickly as possible. The implementations of your data source methods should be minimal. Do not use your data source methods to fetch data from the network, compute values, or do anything that might delay the delivery of that data. If you need to fetch or compute the data for your complication, do it in your iOS app or in other parts of your WatchKit extension, and cache the data in a place where your complication data source can access it. The only thing your data source methods should do is take the cached data and put it into the format that ClockKit requires.

How can you update the complication?

  • Use background updates from the phone to transfer the data to be on hand for the complication's next scheduled update. transferUserInfo and updateApplicationContext are suited for this type of update.

  • Use transferCurrentComplicationUserInfo to immediately transfer complication data and update your timeline.

Both of these approaches have the advantage of only needing one update to occur.

Watch connectivity session issues

Thanks to this answer, I figured out the issue. Calling from the Complication (which is what I was doing) in the requestedUpdateDidBegin() executes an asynchronous method in an asynchronous method, resulting in the update function ending before the sendMessage function returns.

WatchOS2 WCSession sendMessage doesn't wake iPhone on background

After hours of trying and hint from @jeron. I finally figured out the problem myself.

In my session:didReceiveMessage delegate method, I have two calls. 1.replyHandler call. 2. I have an async process running (RXPromise) in my case, It nested quite a few RXPromise callbacks to fetch various data from cloud service. I didn't pay attention to it, because it is supposed to call and return right away. But now that I commented out RXPromise block all together, it can wake up iOS app in the background every time.

Finally I figure out the trouble make is because after RXPromise call, it is not guaranty to be landed back to main thread anymore. And I believe session:didReceiveMessage has to be return on the main thread. I didn't see this mentioned anywhere on the Apple documentation.

Final solution:

- (void)session:(WCSession *)session
didReceiveMessage:(NSDictionary<NSString *, id> *)message
replyHandler:(void (^)(NSDictionary<NSString *, id> *_Nonnull))replyHandler {

replyHandler(@{ @"schedule" : @"OK" });

dispatch_async(dispatch_get_main_queue(), ^{
Nested RXPromise calls.....
});

}

WatchConnectivity, How to Share an Array between iPhone and Apple Watch

In WKInterfaceController:

I agree with the comment from Jens Peter that you shouldn't wrap didReceiveApplicationContext in the load() function.

Handle errors in activationDidCompleteWith as such:

func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
if let error = error {
print("Activation failed with error: \(error.localizedDescription)")
return
}
print("Watch activated with state: \(activationState.rawValue)")
}

That will also help determine if WatchConnectivity is even becoming established.

Remove var watchSession: WCSession? completely and declare WCSession in awake(withContext) as:

if WCSession.isSupported() {
let watchSession = WCSession.default
watchSession = self
watchSession()
}

Also remove the call to load() in awake(withContext).

In InterfaceController it's helpful to put some print statements in your WCSession stubs even if you're not entirely using them. You can see if WatchConnectivity is activating.

func sessionDidBecomeInactive(_ session: WCSession) {
// To support multiple watches.
print("WC Session did become inactive.")
}

func sessionDidDeactivate(_ session: WCSession) {
// To support multiple watches.
WCSession.default.activate()
print("WC Session did deactivate.")
}

func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
if let error = error {
print("WC Session activation failed with error: \(error.localizedDescription)")
return
}
print("Phone activated with state: \(activationState.rawValue)")
}

This line WCSession.default.activate() as you see in sessionDidDeactivate will allow for multiple watch support in the off case the user happens to have more than one watch.

Remove the var watchSession: WCSession? as you did in WKInterfaceController. Once again in viewDidLoad() check for WCSession support with:

if WCSession.isSupported() {
let watchSession = WCSession.default
watchSession = self
watchSession()
}

Update sendToWatch() to:

func sendToWatch() {
let session = WCSession.default()

if session.activationState == .activated {
let appDictionary = ["Array": initalArray]
do {
try session.updateApplicationContext(appDictionary)
} catch {
print(error)
}
}
}

I think that cleans it up a bit and will hopefully work or at least move you forward. I didn't build this in a project however. Hope this helps.



Related Topics



Leave a reply



Submit