how to wake up iOS parent app with sendMessage from complicationController - ios

I am trying to wake up the iOS parent app by sending a message from watchkit extension.
This though does only work when below sendMessage function is called from the watchApp / ViewController. When it is called from ComplicationController, the message is sent, but the iOS parent app does now wake up.
Any advice appreciated. (please any code reference in Swift)
Here the simplified code:
In AppDelegate and ExtensionDelegate:
override init() {
super.init()
setupWatchConnectivity()
}
private func setupWatchConnectivity() {
if WCSession.isSupported() {
let session = WCSession.defaultSession()
session.delegate = self
session.activateSession()
}
}
In ExtensionDelegate: (no problem here, message is successfully sent)
func sendMessage(){
let session = WCSession.defaultSession()
let applicationData:[String:AnyObject] = ["text":"test", "badgeValue": 100 ]
session.sendMessage(applicationData, replyHandler: {replyMessage in
print("reply received from iphone")
}, errorHandler: {(error ) -> Void in
// catch any errors here
print("no reply message from phone")
})
}
print("watch sent message")
}
In AppDelegate: (not received when iOS app not running / not in foreground)
func session(session: WCSession, didReceiveMessage message: [String : AnyObject], replyHandler: ([String : AnyObject]) -> Void) {
let text = message["text"] as! String
let badgeValue = message["badgeValue"] as! Int
dispatch_async(dispatch_get_main_queue()) { () -> Void in
print("iphone received message from watch App")
self.sendNotification(text, badgeValue: badgeValue)
let applicationDict = ["wake": "nowAwake"]
replyHandler(applicationDict as [String : String])
}
}
this is how the function is called from Complication Controller (which does send the message but not awake the parent app):
func requestedUpdateDidBegin(){
dispatch_async(dispatch_get_main_queue()) { () -> Void in
let extensionDelegate = ExtensionDelegate()
extensionDelegate.loadData()
}
}

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.

Related

Detect changes in NSUserDefaults with suiteName

In my project i use app group to transfer data to apple watch!
This look like this
let sharedDefaults = NSUserDefaults(suiteName: "group.com.myappname.defaults")
sharedDefaults?.setObject(MyData, forKey: "DataKey")
sharedDefaults?.synchronize()
In WKInterfaceController i getting my data with this code:
let sharedDefaults = NSUserDefaults(suiteName: "group.com.myappname.defaults")
let MyData = sharedDefaults?.objectForKey("DataKey") as! [[AnyObject]]
All work fine!
Now i try to detect if data in sharedDefaults?.objectForKey("DataKey") did changed. I try to use addObserver method:
override func willActivate() {
NSUserDefaults(suiteName: "group.com.myappname.defaults")!.addObserver(self, forKeyPath: "DataKey", options: NSKeyValueObservingOptions.New, context: nil)
}
override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>){
print("Data Changed")
}
But "override func observeValueForKeyPath" calling only when WKInterfaceController will Activate and didn't calling when i change Data in NSUserDefaults(suiteName: "group.com.myappname.defaults")
Also i try to use NSNotificationCenter:
override func willActivate() {
let sharedDefaults = NSUserDefaults(suiteName: "group.com.myappname.defaults")
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(userDefaultsDidChangeNotificationMethod(_:)), name: NSUserDefaultsDidChangeNotification, object: nil)
}
func userDefaultsDidChangeNotificationMethod(notification: NSNotification){
print("Data Changed")
}
It doesn't work(
What i doing wrong? How to detect if data changed?
I know that this is a bad way, but i didn't find any others...
My way is endless cycle:
var NeedCheking = Bool()
override func willActivate() {
super.willActivate()
NeedChecking = true
CheckDefaults()
}
override func didDeactivate() {
NeedChecking = false
super.didDeactivate()
}
func CheckDefaults(){
let priority = DISPATCH_QUEUE_PRIORITY_DEFAULT
dispatch_async(dispatch_get_global_queue(priority, 0)) {
repeat {
let sharedDefaults = NSUserDefaults(suiteName: "group.com.myappname.defaults")
let NewData = sharedDefaults?.objectForKey("DataKey") as! [[AnyObject]]
if NewData != self.MyData {
dispatch_async(dispatch_get_main_queue()) {
self.MyData = NewData
//Here do update actions!
}
}
sleep(2)
}while self.NeedChecking
}
}
If anybody know other way, please post your solution as answer to this question!
Relationship between the Watch app interface, the WatchKit extension, and the iOS app
Base the relationship, i think KVO doesn't work between watchos and ios, if you want to get the latest data from ios app, there are two ways.
Check the latest data in willActivate
Background Tasks
Background tasks are a way for you to keep your app’s interfaces up-to-date. Receiving a background task object from the system is your signal to perform specific types of operations. The task object defines the type of task to perform and contains any data needed to complete the task. The system delivers background task objects to your app by calling the handleBackgroundTasks: method of your app’s extension delegate.
watchOS supports the following types of background tasks:
Background App Refresh Tasks. Use a WKApplicationRefreshBackgroundTask object to handle general updates to your app’s state. For example, you might use this type of task to check in with your company’s server or begin downloading new content. You schedule this type of background task explicitly from your your app’s WKExtension object.
Background Snapshot Refresh Tasks. Use a WKSnapshotRefreshBackgroundTask object to update your app’s interface in preparation of having its snapshot taken. The system automatically takes the snapshot when this task completes. The system schedules background snapshot refresh tasks periodically to update your snapshot. You can also schedule a task of this type explicitly from your app’s WKExtension object when your interface changes.
Background Watch Connectivity Tasks. Use a WKWatchConnectivityRefreshBackgroundTask object to receive data sent by your iOS app using the Watch Connectivity framework. The system automatically creates this type of task when your Watch app receives data from the its corresponding iOS app running on the paired iPhone. You do not schedule tasks of this type yourself.
Background NSURLSession Tasks. Use a WKURLSessionRefreshBackgroundTask object to receive data you previously requested using an NSURLSession object. This task is triggered when a background transfer requires authorization or when a background transfer completes (successfully or unsuccessfully). You do not schedule tasks of this type yourself.
Remember that background transfers may not be delivered immediately. Files and contextual data are delivered as quickly as possible, but transfers are not instantaneous. Data files involving large files or large amounts of data also take a commensurately long time to complete.

Inconsistent behaviour with WatchKit app - Swift

I'm trying to make iOS app to communicate with watch, but i get inconsistent behaviour all the time - either the communication is too slow, or none of the data gets transferred at all.
Besides, i don't see any "Phone disabled" screen when the watchKit runs (which causes a crash, because i need to get data from the phone first).
This is what i have in regards to establishing the WCSession in the iPhone app
App Delegate
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
if NSClassFromString("WCSession") != nil {
if #available(iOS 9.0, *) {
if(WCSession.isSupported()){
self.session = WCSession.defaultSession()
self.session.delegate = self
self.session.activateSession()
if session.paired {
print("Watch connected")
} else {
print("No watch")
}
}
} else {
}}
if NSClassFromString("WCSession") != nil {
if(WCSession.isSupported()){
session.sendMessage(["b":"delegateSaysHi"], replyHandler: nil, errorHandler: nil)
}}
}
MainViewController
(viewDidLoad)
if NSClassFromString("WCSession") != nil {
if(WCSession.isSupported()){
self.session = WCSession.defaultSession()
self.session.delegate = self
self.session.activateSession()
if session.paired {
print("Watch connected")
} else {
print("No watch")
}
}}
MainViewController (Method for transferring bunch of data from iOS app to watchKit app)
func transferData(){
do {
let dataArray = ["somedata": array2d1]
try WCSession.defaultSession().updateApplicationContext(dataArray)
let dataArray1 = ["somedata1": array2d2]
try WCSession.defaultSession().updateApplicationContext(dataArray1)
let dataArray2 = ["somedata2": array2d3]
try WCSession.defaultSession().updateApplicationContext(dataArray2)
let dataArray3 = ["somedata3": array2d4]
try WCSession.defaultSession().updateApplicationContext(dataArray3)
// and up to 12
}
catch {
print("Something wrong happened")
}
}
And this is for watchKit app
App Delegate
func applicationDidFinishLaunching() {
if(WCSession.isSupported()){
self.session = WCSession.defaultSession()
self.session.delegate = self
self.session.activateSession()
}
}
func applicationDidBecomeActive() {
if(WCSession.isSupported()){
self.session.sendMessage(["b":"peek"], replyHandler: nil, errorHandler: nil)
}
InterfaceController (awakeWithContext)
if(WCSession.defaultSession().reachable){
self.session.sendMessage(["b":"peek"], replyHandler: nil, errorHandler: nil)
}
Method for receiving ApplicationContext data
func session(session: WCSession, didReceiveApplicationContext applicationContext: [String : AnyObject]) {
dispatch_async(dispatch_get_main_queue()) { () -> Void in
if let retrievedArray1 = applicationContext["somedata"] as? [[String]] {
self.watchAppArray = retrievedArray1
}
if let retrievedArray2 = applicationContext["somedata2"] as? [[String]] {
self.watchAppArray = retrievedArray1
// and so on for 12 arrays sent from phone
}
}
}}
Any advices on clearing out the situation are very welcome!
Thank you.
Multiple delegates/activations:
You're repeatedly setting up, delegating, and activating sessions in different parts of your app. You keep changing your delegate, so code in one part of the app will no longer be used after you delegated handling to a different part of your app.
You should use a single session/delegate throughout your app. One solution is to setup a WCSession singleton which would be available app-wide. Here's a guide which walks you through that process.
Only the most recent application context would get sent:
By trying to queue up multiple application context requests, the earlier ones would no longer be in the queue when the system gets around to transmitting it, as the system would have already replaced the preceding context with the later one. So only the last (dataArray3) would ever get transmitted.
Use the updateApplicationContext:error: method to communicate recent state information to the counterpart. When the counterpart wakes, it can use this information to update its own state. ... This method overwrites the previous data dictionary, so use this method when your app needs only the most recent data values.
If all of the arrays represent the recent state of your application, you want to transmit them together in a single dictionary.
var dataArray = [String: AnyObject]()
dataArray["somedata"] = array2d1
dataArray["somedata1"] = array2d2
dataArray["somedata2"] = array2d3
dataArray["somedata3"] = array2d4
do {
try session.updateApplicationContext(dataArray)
}
catch {
print(error)
}
It may also help to add some error handling to your sendMessage code, as the paired device may not always be reachable.
Slow communication:
As for the communication being too slow, there are two issues at hand.
Transfers may not happen immediately.
When only one session is active, the active session may still send updates and transfer files, but those transfers happen opportunistically in the background.
Remember that background transfers are not be delivered immediately. The system sends data as quickly as possible but transfers are not instantaneous, and the system may delay transfers slightly to improve power usage. Also, sending a large data file requires a commensurate amount of time to transmit the data to the other device and process it on the receiving side.
The more data you send, the longer it takes to transmit/receive it all.
When sending messages, send only the data that your app needs. All transfers involve sending data wireless to the counterpart app, which consumes power. Rather than sending all of your data every time, send only the items that have changed.
You can control how much data you send, as well as whether the data is sent interactively or in the background. If the watch is reachable, you could use sendMessage for immediate communication. If it's not reachable, you could fall back on a background method.

How To Share Data with Watch OS 2 to display in WKInterfaceTable when working with CoreData

I am using WatchConnectivity to try to send data of type NSManagedObject called arrayOfOjects to the Watch. Each object has a string property called title.
The InterfaceController on the Watch loads and displays and empty table - as intended because the array is empty, then when the user requests the data it is sent using the didReceiveMessage method on the phone.
I am unsure how to add the dictionary array to the objectsArray to display in the WKInterfaceTable.
Does anyone know how I can send the data to the watch to display in the table to make changes and sync them back with the phone ?
Apple Watch:
class ObjectsInterfaceController: WKInterfaceController, WCSessionDelegate {
var session : WCSession!
var objectsArray = [[AnyObject]]()
#IBOutlet var table: WKInterfaceTable!
#IBOutlet var titleLabel: WKInterfaceLabel!
func loadTableData() {
table.setNumberOfRows(self.objectsArray.count, withRowType: "CellRow")
if self.objectsArray.count > 0 {
for (index, name) in self.objectsArray.enumerate() {
let row = self.table.rowControllerAtIndex(index) as! CellRowController
row.objectCellLabel.setText(name.title)
}
}
}
override init() {
super.init()
loadTableData()
}
override func awakeWithContext(context: AnyObject?) {
super.awakeWithContext(context)
// Interface Objects
}
override func willActivate() {
// This method is called when watch view controller is about to be visible to user
super.willActivate()
//Check if session is supported and Activate
if (WCSession.isSupported()) {
session = WCSession.defaultSession()
session.delegate = self
session.activateSession()
}
}
//Swift
func session(session: WCSession, didReceiveMessage message: [String : AnyObject], replyHandler: ([String : AnyObject]) -> Void) {
let value = message["Value"]
dispatch_async(dispatch_get_main_queue()) {
self.objectsArray.removeAll()
self.objectsArray.append(value! as! Array)
self.loadTableData()
}
//send a reply
replyHandler(["Value":"Yes"])
}
}
iPhone
I already fetch all the objects and store in array.
var objectsArray = [Objects]()
func session(session: WCSession, didReceiveMessage message: [String : AnyObject], replyHandler: ([String : AnyObject]) -> Void) {
//send a reply
replyHandler(["Value": [objectsArray]])
}
I need to be able to modify the properties of the objects and save the changes on the iPhone but atm I cannot even send the data and display in the table :( I have been able to send simple string values within a dictionary between devices but not arrays or actual data.
TL/DR:
You can only send basic types (such as strings, integers, doubles) to your watch. This question has more details about sending custom objects.
The other issue:
Even if you archived or serialized the managed objects, it's still not possible to send that particular data from the phone to the watch.
A NSManagedObject is only valid within its own context. In this case, the managed object are registered with a specific NSMangedObjectContext on your iOS app. The managed object is not useful apart from its context, and its managed object context doesn't exist anywhere else but the phone.
NSManagedObject instances are not intended to be passed between queues. Doing so can result in corruption of the data and termination of the application. When it is necessary to hand off a managed object reference from one queue to another, it must be done through NSManagedObjectID instances.
Since it's not possible pass a managed object from one context (or thread or queue) to another on the same platform, you definitely can't pass a managed object between the phone and its paired watch.
What can you do?
If you had a way to share your Core Data store between the phone and the watch, you could convert the managed object IDs to strings (using URIRepresentation), then pass those strings to the watch, then convert those strings back to object IDs and fetch the corresponding objects. This is explained in detail in this question.
However, app groups are no longer supported on watchOS 2, and it would be very complex to keep two different stores in sync across devices.
A much lighter solution is to pass details about the title, keep track of whatever changes you make on the watch, then send back a dictionary of titles that were inserted, deleted, or otherwise changed.
The phone would then update the managed objects corresponding to those changed titles.
This is similar to how NSFetchedResultsControllerDelegate responds to changes to keep its results in sync.
Does anyone know how I can send the data to the watch to display in the table to make changes and sync them back with the phone?
I gave you a general overview. Anything more detailed would be far too broad to cover in this answer. All I can suggest is that developers either used a third-party framework, or wrote their own implementation.
Some considerations:
Just keep in mind that you don't want to degrade the user's watch experience by transferring large amounts of data back and forth. Keep your watch app lightweight and responsive, as it's ideally only designed to be used for a few seconds.
If you can simplify your watch app design (e.g., only marking a todo list item as completed), you can eliminate much of the "sync" overhead, and delegate the more complex tasks to the iOS app.

How to share data using Watch Connectivity when working with Core Data

In my iOS application I use Core Data to store data and a fetch request to create an array of NSManagedObjects to display in a UITableView.
On the Watch OS I check if WCSession is supported and active a session, then send the iOS application a message from the watchOS extension.
When the iOS application receives the message from the watchOS it should send the array of Objects to the watchOS extension to display the data in the WKInterfaceTable, but I am unsure how to do this. Ultimately what I am trying to achieve is;
How to share the array of Objects with the watchOS extension?
If the user adds/edits/deletes objects in the array on the Watch, how can we update the data on the iPhone ?
Also, the iOS application is embedded within a UITabBarController so does it matter which view controller I communicate with?
Watch OS FavouritesInterfaceController
var session : WCSession!
override func willActivate() {
// This method is called when watch view controller is about to be visible to user
super.willActivate()
//Check if session is supported and Activate
if (WCSession.isSupported()) {
session = WCSession.defaultSession()
session.delegate = self
session.activateSession()
}
}
override func awakeWithContext(context: AnyObject?) {
super.awakeWithContext(context)
// Interface Objects
//Send Message
sendmessagetoiphone()
}
func sendMessageToIphone() {
if(WCSession.isSupported()){
session.sendMessage(["b":"goodBye"], replyHandler: nil, errorHandler: nil)
}
}
IOS Application : FavouritesViewController
var objects = [Objects]()
func loadData() {
let moc = (UIApplication.sharedApplication().delegate as! AppDelegate).managedObjectContext
let request = NSFetchRequest(entityName: "Objects")
request.sortDescriptors = [NSSortDescriptor(key: "date", ascending: true)]
do {
try
self.objects = moc.executeFetchRequest(request) as! [Objects]
// success ...
} catch {
// failure
print("Fetch failed")
}
}
func session(session: WCSession, didReceiveMessage message: [String : AnyObject], replyHandler: ([String : AnyObject]) -> Void) {
//handle received message
let value = message["Value"] as? String
dispatch_async(dispatch_get_main_queue()) {
self.messageLabel.text = value
}
//send a reply
replyHandler(["Value":"Hello Watch"])
}
How to share the array of Objects with the Watch OS Extension ?
Since you are using WatchConnectivity framework, send the array of objects from iPhone using sendMessage method and in your FavoritesInterfaceController implement the func session(session: WCSession, didReceiveMessage method in order to get the response or you can get the array in replyhandler to.
If the user adds/edits/deletes objects in the array on the Watch OS
how can we update the data on the iPhone ?
Send the objectId along the with new changes in your sendMessage method from watch to phone, on receiving on phone made the changes in your database save it and send the updated value in your replyHandler so that your watch content will be updated accordingly.
Also the iOS application is embedded within a UITabBarController so
does it matter which view controller I communicate with ?
You desired viewController to which you are communicating OR the one that is responsible for doing changes should be alive. If multiple ViewControllers are listening to WCSessionDelegates then when you send any message from watch all of the live controllers will receive that message. You should include some kind of identifier in your sendMessage dictionary so you can know which operation to perform. Like if you want to delete an object then when watch sends a message the identifier will contain delete so that on receiving you can check the identifier value and perform the delete operation.
You can use the replyHandler in the sendMessage to do this. Make sure you implement the reply handler on both Watch and iOS App to get this.
Basically, if you get it right, your reply handler can ensure what your iOS app does in response for a watch app's message.
Also, speaking of your response (of sending an array of objects) - you should send it as a dictionary and fetch it on the watch.
First of, this is a really good question. For starters I'd recommend that you watch this session from the WWDC 2015: Session 713 - Introducing Watch Connectivity. This can be found here.
Now to your actual question. There is a great tutorial and Github repo that show you how to communicate Core Data between your Apple Watch app and the container app using App Groups, as this enables you to access all shared content, such as Core Data and even NSUSerdefaults.
You can then find the complete tutorial on how to do this under the following link.
Hope that helps, Julian.

How can i check from watchOS 2 if application on iPhone is opened or not and be able to send NSUserDefaults no matter the app status?

How can I check from watchOS 2 if application on iPhone is opened or not?
I want to send a message with NSUserDefaults from watch to iPhone via sendMessage (to be able to update interface on phone when message received) when both applications are running and I want to send NSUserDefaults even if only watchOS 2 app is running.
From what I read I found this:
/** The counterpart app must be reachable for a send message to succeed. */
#property (nonatomic, readonly, getter=isReachable) BOOL reachable;
It's always reachable from what I check.
Reachable means the apple watch and iPhone are connected via bluetooth or wifi. It doesn't necessarily mean the iPhone app is running. If reachable is true, when you try to sendMessage from the apple watch it will launch the iPhone app in the background. You need to assign the WKSession delegate as soon as possible because the delegates methods (sendMessage) will fire soon. I think what you are saying you want to do is call sendMessage if you can, and it not use the transferUserInfo method instead. To do this, first on your apple watch:
func applicationDidFinishLaunching() {
let session = WCSession.defaultSession()
session.delegate = self
session.activateSession()
// NOTE: This should be your custom message dictionary
// You don't necessarily call the following code in
// applicationDidFinishLaunching, but it is here for
// the simplicity of the example. Call this when you want to send a message.
let message = [String:AnyObject]()
// To send your message.
// You could check reachable here, but it could change between reading the
// value and sending the message. Instead just try to send the data and if it
// fails queue it to be sent when the connection is re-established.
session.sendMessage(message, replyHandler: { (response) -> Void in
// iOS app got the message successfully
}, errorHandler: { (error) -> Void in
// iOS app failed to get message. Send it in the background
session.transferUserInfo(message)
})
}
Then, in your iOS app:
// Do this here so it is setup as early as possible so
// we don't miss any delegate method calls
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
self.watchKitSetup()
return true
}
func watchKitSetup() {
// Stop if watch connectivity is not supported (such as on iPad)
if (WCSession.isSupported()) {
let session = WCSession.defaultSession()
session.delegate = self
session.activateSession()
}
}
func session(session: WCSession, didReceiveMessage message: [String : AnyObject], replyHandler: ([String : AnyObject]) -> Void) {
// Handle the message from the apple watch...
dispatch_async(dispatch_get_main_queue()) {
// Update UI on the main thread if necessary
}
}
func session(session: WCSession, didReceiveUserInfo userInfo: [String : AnyObject]) {
// Handle the message from the apple watch...
dispatch_async(dispatch_get_main_queue()) {
// Update UI on the main thread if necessary
}
}
You probably want to use the application context of WatchConnectivity:
have a look at WCSession.updateApplicationContext( )
It sends the most important configuration info to the counterpart as soon as the counterpart is reachable, even if the counterpart is not reachable at the time of sending. If you call updateApplicationContext multiple times, only the latest is sent.
For much deeper info watch the WWDC 2015 session about WatchConnectivity: https://developer.apple.com/videos/wwdc/2015/?id=713
It describes more means to send data, but I think the application context fits best for you.
The session also details how to find out if the counterpart is reachable, but I think you don't need that for your use case.

Resources