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 totransferCurrentComplicationUserInfo:
usetransferUserInfo:
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
andupdateApplicationContext
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
Problems Accessing Calendar Using Ekeventstore on Osx Sierra with Swift 3
Calling Nsexception.Raise() in Swift
How to Make Swiftui Uiviewrepresentable View Hug Its Content
Why Do I Get "Static Member '...' Cannot Be Used on Instance of Type '...'" Error
Alamofire 5 Upload Encodingcompletion
Swiftui MACos Scroll a List with Arrow Keys While a Textfield Is Active
What Is the Slice Compare Logic in Swift
Having Trouble with Nstimer (Swift)
How to I Turn Off the Ambient Light in Scene Kit (With Swift)
Different Colors for Bars in Barchart Depend on Value
What Does a "Do Statement" Without Catch Block Mean
Select All Text in a Nstextfield Using Swift
Swiftui Hierarchical Picker with Dynamic Data
Firebase Retrieve Image from Url Save with Firebase Database
iOS 12 Errors: Appears to Be from a Different Nsmanagedobjectmodel Than This Context'S