Update data model on Apple Watch when phone app is in background - ios

I have written an iOS app that refreshes its data model when a push notification is received whilst the app is in the foreground, once the data is retrieved from the server I send that information to the watch kit app using:
// This code resides in ErrorsViewController.swift
func updateWatchContext() {
do {
let messages = convertParseObjectsToJSON(tasks)
try session?.updateApplicationContext(["messages" : messages])
} catch let error as NSError {
NSLog("Updating the context failed: " + error.localizedDescription)
}
}
func convertParseObjectsToJSON(data:[PFObject])->[[String : AnyObject]]
{
var data = [[String:AnyObject]]()
for var i = 0; i < tasks.count; i++
{
let object = tasks[i]
data.append([
"createDate" : object["createDate"],
"errorMessage" : object["errorCode"]
])
}
return data
}
This works fine when the application is in the foreground, the data model gets updated on the watch as expected. However in the scenario that the phone is running in the background, how can I make a background fetch, parse the data and send it to the watchkit app without waking the iPhone, using watch connectivity?
I was thinking about trying to add the code in AppDelegate, but I don't believe that will work. I'd like to note that I do not want to make any network requests directly from the watch itself due to the limited CPU power; it would be un-necessary to handle the data parsing there.

Related

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.

Apple Watch Background Mode?

I am developing apple watch application. when i run the app it is working fine. Now my problem is when the app goes to background mode, the app on the apple watch app will closing automatically. I am writing small code in iPhone app:
func viewDidLoad() {
if (WCSession.isSupported()) {
let session = WCSession.defaultSession()
session.delegate = self
session.activateSession()
// In your WatchKit extension, the value of this property is true when the paired iPhone is reachable via Bluetooth.
// On iOS, the value is true when the paired Apple Watch is reachable via Bluetooth and the associated Watch app is running in the foreground.
// In all other cases, the value is false.
if session.reachable {
lblStatus.text = "Reachable"
}
else
{
lblStatus.text = "Not Reachable"
}
func sessionReachabilityDidChange(session: WCSession)
{
if session.reachable {
dispatch_async(dispatch_get_main_queue(), {
self.lblStatus.text = "Reachable"
})
}
else
{
dispatch_async(dispatch_get_main_queue(), {
self.lblStatus.text = "Not Reachable"
})
}
}
}
}
in WatchExtention Code is
func someFunc() {
if (WCSession.isSupported()) {
let session = WCSession.defaultSession()
session.delegate = self
session.activateSession()
if session.reachable {
ispatch_async(dispatch_get_main_queue(), {
self.lblStatus.setText("Reachable")
})
}
else
{
dispatch_async(dispatch_get_main_queue(), {
self.lblStatus.setText("Not Reachable")
})
}
func sessionReachabilityDidChange(session: WCSession)
{
if session.reachable {
dispatch_async(dispatch_get_main_queue(), {
self.lblStatus.setText("Reachable")
})
}
else
{
dispatch_async(dispatch_get_main_queue(), {
self.lblStatus.setText("Not Reachable")
})
}
}
}
}
Now when enter to background in apple Watch the iPhone app showing Not reachable why ?
It's the default behavior of the AppleWatch, mainly to spare with resources like battery.
session.reachable property is true only when Apple Watch is reachable via Bluetooth and the associated Watch app is running in the
foreground in all other cases, the value is false.
In your case the second option which caused the problem I suppose the bluetooth connection is working.
Anyway the question is what do you like to reach.
Actually the simple rule is that you couldn't wake up the Watch from the iPhone but you could wake up the iPhone app from the Watch.
Two ways with three options to reach the watch when it's counterpart is in the background: send a complication update or send a message (2 options) in the background which will be available for the Watch when it will awake again.
All of them are part of the WCSession Class.
The two options for sending messages are:
- updateApplicationContext:error:
You can use this method to transfer a dictionary of data to the counterpart Watch app.iPhone sends context data when the opportunity arises, means when the Watch app arises.The counterpart’s session on the Watch gets the data with the session:didReceiveUpdate: method or from the receivedApplicationContext property.
You may call this method when the watch is not currently reachable.
The other option is sending data in the background like
- transferUserInfo:
You can use this method when you want to send a dictionary of data to the Watch and ensure that it is delivered. Dictionaries sent using this method are queued on the other device and delivered in the order in which they were sent. After a transfer begins, the transfer operation continues even if the app is suspended.
BUT true for both methods that they can only be called while the session is active. Calling any of these methods for an inactive or deactivated session is a programmer error.
The complication solution is a little bit different but belongs to the same WCSession Class as the earliers.
-transferCurrentComplicationUserInfo:
This method is specifically designed for transferring complication user info to the watch with the aim to be shown on the watch face immediately.
Of course it's available only for iOS, and using of this method counts against your complication’s time budget, so it's availability is limited.
The complication user info is placed at the front of the queue, so the watch wakes up the extension in the background to receive the info, and then the transfer happens immediately.
All messages received by your watch app are delivered to the session delegate serially on a background thread, so you have to switch to the main queue in case you'd like to use or presenting them for UI.
The WWDC talk on WatchConnectivity discusses "reachability" and its nuances in quite a lot of detail, so you should definitely give it a watch.
TL;DR: reachable on the watch is for the most part only going to be true/YES when the watch app's UI is visible on the screen.

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 to wake up iOS parent app with sendMessage from complicationController

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.

Swift iOS: Closure will not run in background when Remote Notification received

I am writing an iOS app that requires the device's GPS loction to be updated when a push notification is received.
I use a closure to get the current GPS location. This code runs perfectly when the app is in the foreground (Both "New remote notification" and "Got location" is printed to console), but when the app is in the background, only "New remote notification" (and no location error) is printed to console. As a result I don't have the GPS location of the device at this point, which is very important for my app's functionality.
Any help would be greatly appreciated.
Thanks.
I have in my Info.plist file for 'Required background modes';
App registers for location updates
App downloads content from the network
App downloads content in response to push notifications
My app also has access to location at all times, including the background (successfully tested at other points in the code): NSLocationAlwaysUsageDescription is in the Info.plist file
In my AppDelegate file:
func application(application: UIApplication!, didReceiveRemoteNotification userInfo:NSDictionary!) {
println("New remote notification")
var notification:NSDictionary = userInfo as NSDictionary
var loc: LocationManager?
loc = LocationManager()
loc!.fetchWithCompletion {location, error in
// fetch location or an error
if let myloc = location {
var lat = myloc.coordinate.latitude as Double
var lon = myloc.coordinate.longitude as Double
println("Got location")
} else if let err = error {
println(err.localizedDescription)
}
loc = nil
}
}
If app is not in foreground, make sure to ask for a little time to complete the request with beginBackgroundTaskWithExpirationHandler and then call endBackgroundTask when done.
For more information, see the Executing Finite Length Tasks in the Background Execution chapter of the App Programming Guide for iOS.

Resources