Core Data fetch request with WatchConnectivity - ios

I'm currently trying to get CoreData data from my iOS app to the watchOS extension. I'm using the WatchConnectivity Framework to get a dictionary via the sendMessage(_ message: [String : Any], replyHandler: (([String : Any]) -> Void)?, errorHandler: ((Error) -> Void)? = nil) function. The basic connection is working fine. The iOS app is reachable and if I try to reply a sample dictionary everything is working.
So far so good, but as I start doing a fetch request on the iOS app in background, the Watch App never receives data. After a while I just get this error: Error while requesting data from iPhone: Error Domain=WCErrorDomain Code=7012 "Message reply took too long." UserInfo={NSLocalizedFailureReason=Reply timeout occurred., NSLocalizedDescription=Message reply took too long.}
If I open the iOS app on the iPhone and relaunch the Watch App the reply handler is getting the result. But forcing the user to actively open the iOS app on the iPhone is useless.
Can someone explain why this is happen? And what's the right way to do it? App Groups seem to be obsolete since watchOS 2.
I'm using Swift 4 btw…
On Apple Watch:
import WatchConnectivity
class HomeInterfaceController: WKInterfaceController, WCSessionDelegate {
// (…)
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
session.sendMessage(["request": "persons"],
replyHandler: { (response) in
print("response: \(response)")
},
errorHandler: { (error) in
print("Error while requesting data from iPhone: \(error)")
})
}
On iPhone:
import CoreData
import WatchConnectivity
class ConnectivityHandler: NSObject, WCSessionDelegate {
var personsArray:[Person] = []
// (…)
func session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: #escaping ([String : Any]) -> Void) {
// only using the next line is working!
// replyHandler(["data": "test"])
if message["request"] as? String == "persons" {
fetchAllPersons()
var allPersons: [String] = []
for person in personsArray {
allPersons.append(person.name!)
}
replyHandler(["names": allPersons])
}
}
// this seems to be never executed (doesn't matter if it's in an extra function or right in the didReceiveMessage func)
func fetchAllPersons() {
do {
// Create fetch request.
let fetchRequest: NSFetchRequest<Person> = Person.fetchRequest()
// Edit the sort key as appropriate.
let sortDescriptor = NSSortDescriptor(key: #keyPath(Person.name), ascending: true)
fetchRequest.sortDescriptors = [sortDescriptor]
personsArray = try DatabaseController.getContext().fetch(fetchRequest)
} catch {
fatalError("Failed to fetch: \(error)")
}
}

After looking into this problem I found the solution by myself. The problem was that I'm using the sendMessage(_:replyHandler:errorHandler:) protocol. This is only used for transferring data when both apps are active.
Use the sendMessage(_:replyHandler:errorHandler:) or sendMessageData(_:replyHandler:errorHandler:) method to transfer data to a reachable counterpart. These methods are intended for immediate communication between your iOS app and WatchKit extension. The isReachable property must currently be true for these methods to succeed.
If you want to transfer data in the background you have to use updateApplicationContext(_:) or transferUserInfo(_:) depending on your needs. That's exactly what I needed!
Use the updateApplicationContext(_:) method to communicate recent state information to the counterpart. When the counterpart wakes, it can use this information to update its own state. For example, an iOS app that supports Background App Refresh can use part of its background execution time to update the corresponding Watch app. This method overwrites the previous data dictionary, so use this method when your app needs only the most recent data values.
Use the transferUserInfo(_:) method to transfer a dictionary of data in the background. The dictionaries you send are queued for delivery to the counterpart and transfers continue when the current app is suspended or terminated.
Now if the iPhone App counterpart opens the ApplicationContext or UserInfo queue is passed trough and I can add the data to my core data library.

Sadly, most WatchConnectivity methods have a time limit (as told you by the error) and it seems your CoreData request is taking too much time and hence it exceeds the time limit. According to this Q&A it seems that you need to take specific precautions for doing CoreData queries in the background, so that might be the cause for your issue.
However, for best user experience I would recommend you to stop using CoreData and the WatchConnectivity framework, since the latter requires your iOS app to be running at least in the background, hence making the watchOS app dependent on the state of the iOS app and degrading the user experience on watchOS. I'd recommend you switch to Realm, since Realm supports watchOS fully and hence your watchOS app can be fully independent from your iOS app, making the user experience more fluid, since the user won't have to start the iOS app and wait for the data transmission through BLE using the WatchConnectivity framework.

Related

Firebase Analytics / Firebase Crashlytics - reporting errors

I am working on an iOS app that uses Firebase Analytics and Firebase Crashlytics. I wonder what is the best way to report errors. Crashes are reported automatically, so probably I should log errors as events? I am talking about caught cases where for example the data from the server cannot be parsed and used for some reason, but the app does not crash, just doesn't work as expected.
I am looking at the predefined event app_exception and its predefined parameter firebase_event_origin. Is this the right way to do it and if yes what should be logged as firebase_event_origin? Or should I define some custom event with custom params, or maybe there is a better way?
I'm doing something like this in my project and it's working great:
public protocol ErrorRecorder {
func recordError(_ error: NSError, userInfo: [String: Any]?)
}
extension Crashlytics: ErrorRecorder {
public func recordError(_ error: NSError, userInfo: [String: Any]?) {
Crashlytics.sharedInstance().recordError(error, withAdditionalUserInfo: userInfo)
}
}

Store URLSessionDownloadTask in Core Data

I want to save URLSessionDownloadTask in core data when app is gone in closed state or my download state is changed e.g from waiting state to downloading state or to completed state.
All other attributes of my custom class are stored perfectly but app crashes when it stores download task.
reason to crash is
[__NSCFLocalDownloadTask encodeWithCoder:]: unrecognized selector sent to instance 0x7ff189f181c0
-[NSKeyedArchiver dealloc]: warning: NSKeyedArchiver deallocated without having had -finishEncoding called on it.
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[__NSCFLocalDownloadTask encodeWithCoder:]: unrecognized selector sent to instance 0x7ff189f181c0'
this is my class
class VideoDownloadModel : NSManagedObject {
#NSManaged var videoID : NSNumber?
#NSManaged var vid : Video?
#NSManaged var downloadTask : URLSessionDownloadTask?
#NSManaged var downloadStatus : String?
}
storing it like this
let request = NSFetchRequest<NSFetchRequestResult>(entityName: (COREDATA_ENTITY_Description?.name)!)
request.returnsObjectsAsFaults = false
request.predicate = NSPredicate(format: "videoID == %#", videoModel.videoID!)
do {
let result = try COREDATA_CONTEXT.fetch(request)
print(result)
var vidArr = result as! [VideoDownloadModel]
if vidArr.count != 0 {
vidArr[0] = videoModel
COREDATA_MANAGER.saveContext()
}
} catch {
let fetchError = error as NSError
print(fetchError)
}
when URLSessionDownlaodTask is nil it works fine but when any download is started it crashes on saving.
scenerio :
I initialized my custom class object with all attributes but set task to nil.
I store that object in core data it saved perfectly.
I initialize the task of that object the download work perfectly.
Then i update the object in core data while updating the app got crash because URLSssionTask is not inheriting from NSCoding. so it don't have encoding and decoding methods.
I want some solution to solve this issue
Any help will be appreciated. Thanks.
You can't save the URLSessionDownloadTask in Core Data because-- as you mentioned-- it doesn't conform to NSCoding. In many cases the answer would be to write your own code to convert to/from Data but that doesn't work in this case. A URLSessionDownloadTask can only be created by a URLSession, so you can't serialize and deserialize the task object.
That doesn't really matter though because saving and restoring them doesn't make sense. A URLSessionDownloadTask represents something that is in progress while the app is running. If your app is closed, that activity ends. Restoring a URLSessionDownloadTask after the app closes and relaunches doesn't make sense because there's no activity for it to represent. Basically, even if you could store the task object and restore it, it would be useless after restoring. There's no reason to bother.
If your interest is that you want to resume incomplete downloads, you'll have to start over from the beginning. Create your URLSession and then use it to create a new URLSessionDownloadTask. If your interest is in getting information about a background download, you can use the session object with getTasksWithCompletionHandler(_:) to find out whether they completed.
Saving URLSessionDownloadTask doesn't make sense. You would actually want to save the data obtained from downloadTask.cancel(byProducingResumeData: ) while pausing.
Once you need to resume the download create a new downloadtask with the saved data by using downloadTaskWithResumeData(:) and resume.
When app is terminated - dosen't include the case when the app is forcefully terminated by the user.
The apple documentation on URLSession clearly explains what to do when app is terminated.
From the docs - In both iOS and OS X, when the user relaunches your app, your app should immediately create background configuration objects with the same identifiers as any sessions that had outstanding tasks when your app was last running, then create a session for each of those configuration objects. These new sessions are similarly automatically reassociated with ongoing background activity.
When user forcefully terminate the app
In this case, URLSession delegate urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) will be fired, in which the userInfo dict in error object will have the resume data corresponding to the key NSURLSessionDownloadTaskResumeData, which could be used to resume the task using downloadTask.cancel(byProducingResumeData: ). Also please note that you will have to typecast error to NSError to retrieve the userInfo dict.
It would be good to read through the docs here before using NSURLSession

Call ExtensionDelegate to create/refresh data for Complication

All my data creation is done in the ExtensionDelegate.swift.
The problem is ExtensionDelegate.swift doesn't get called before the function getCurrentTimelineEntryForComplication in my ComplicationController.swift.
Any ideas? Here is my code and details:
So my array extEvnts is empty in my ComplicationController.swift:
func getCurrentTimelineEntryForComplication(complication: CLKComplication, withHandler handler: ((CLKComplicationTimelineEntry?) -> Void)) {
let extEvnts = ExtensionDelegate.evnts
}
Because my ExtensionDelegate.swift hasn't gotten called yet, which is what creates the data for the array:
class ExtensionDelegate: NSObject, WKExtensionDelegate, WCSessionDelegate {
private let session = WCSession.defaultSession()
var receivedData = Array<Dictionary<String, String>>()
static var evnts = [Evnt]()
func session(session: WCSession, didReceiveUserInfo userInfo: [String : AnyObject]) {
if let tColorValue = userInfo["TeamColor"] as? String, let matchValue = userInfo["Matchup"] as? String {
receivedData.append(["TeamColor" : tColorValue , "Matchup" : matchValue])
ExtensionDelegate.evnts.append(Evnt(dataDictionary: ["TeamColor" : tColorValue , "Matchup" : matchValue]))
} else {
print("tColorValue and matchValue are not same as dictionary value")
}
}
func applicationDidFinishLaunching() {
// Perform any final initialization of your application.
if WCSession.isSupported() {
session.delegate = self
session.activateSession()
}
}
}
EDIT:
Per Apple, it looks like this has something to do with it, but for some reason I have no idea how to actually implement it because I'm not able to call mydelegate.evnts:
// Get the complication data from the extension delegate.
let myDelegate = WKExtension.sharedExtension().delegate as! ExtensionDelegate
var data : Dictionary = myDelegate.myComplicationData[ComplicationCurrentEntry]!
So I've tried something like this, and still can't get it working because I'm still getting no data:
func someMethod() {
let myDelegate = WKExtension.sharedExtension().delegate as! ExtensionDelegate
let dict = ExtensionDelegate.evnts
print("ExtensionDel.evnts: \(dict.count)")
}
Useful question that helped me here
In the function requestedUpdateDidBegin() you can update the information that you will display in your complication. So in this method you may make a call to your parent app using a WatchConnectivity method like sendMessage:replyHandler:errorHandler: to receive new information.
You can use NSUserDefaults to store your imperative data that will be used in your ComplicationController, then load this information from NSUserDefaults for your complication. I store this data in user defaults so that I always have old data to display in case the new data fails to load.
TL/DR: Have the extension tell ClockKit to update the complication after the data is received.
First issue:
So my array extEvnts is empty in my ComplicationController.swift ... Because my ExtensionDelegate.swift hasn't gotten called yet, which is what creates the data for the array
Your array is empty because the data hasn't been received at that point.
You can't (get the complication controller to) force the watch (extension) to receive data which may not have even been transmitted yet.
If you look at the WCSession Class Reference, transferUserInfo queues data to be transferred in the background, when the system decides it's a good time to send the info.
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.
Second issue:
You're trying to combine updating your app and your complication based on data sent from your phone. But your app and your complication don't necessarily run together. It's not surprising or unexpected that the watch updates the complication before any data has even been sent/received. The App Programming Guide for watchOS mentions that
Complications exist entirely in the WatchKit extension. Their user interface is not defined in the Watch app. Instead, it is defined by an object implementing the CLKComplicationDataSource protocol. When watchOS needs to update your complication, it launches your WatchKit extension. However, the Watch app’s executable is not launched.
There's no mechanism for the complication controller to say, "Wait, I'm not ready to provide an update. The complication controller can't wait on (or as mentioned, force) the watch extension to receive data.
It's only responsibility is to immediately return data based on what's currently available to it. If there's no data, it must return an empty timeline.
Approaching this problem:
You shouldn't necessarily think of app updates and complication updates as the same thing. The first is not budgeted, but the second is budgeted. If you update your complication too often, you may exceed your daily budget, and no further updates will occur for the remainder of the day.
Complications aren't meant to be frequently updated. Complications should provide as much data as possible during each update cycle. You shouldn't ask the system to update your complication within minutes. You should provide data to last for many hours or for an entire day.
Having covered that, you could wait until your extension has received data, then can ask ClockKit to extend your timeline, so new entries can be added to it. extendTimelineForComplication: is documented in the CLKComplicationServer Class reference.
As an aside, if your data is urgent, you should use transferCurrentComplicationUserInfo. It's a high-priority message, which is placed at the head of the queue, and the extension is woken up to receive it. See this answer for a comparison between it and transferUserInfo.
You also could setup a singleton to hold your data which the watch app and complication controller both use. This was mentioned in an answer to an old question of yours, and also recommended by an Apple employee on the developer forums.

WKInterfaceController.openParentApplication is delayed on real device?

I've been tracking this bug for days and finally realized the issue is that there is an actual delay with WKInterfaceController.openParentApplication. I am not referring to the callback in the Apple Watch, but the AppDelegate event in the iPhone.
Here's what I'm doing in the Apple Watch:
override func awakeWithContext(context: AnyObject?) {
var userInfo = [
"test": 123
]
WKInterfaceController.openParentApplication(userInfo, reply: nil)
}
Then in the iPhone, I'm doing this in AppDelegate:
func application(application: UIApplication, handleWatchKitExtensionRequest userInfo: [NSObject : AnyObject]?, reply: (([NSObject : AnyObject]!) -> Void)!) {
let test = userInfo["test"]
// Do something
}
The problem is when I call WKInterfaceController.openParentApplication, there is a 2 or 3 second delay before the handleWatchKitExtensionRequest event is triggered in the iPhone. This is problematic if the user performs an action on their watch really quickly and puts down their arm right after. I can't assume the user is going to hold up their arm for 3 seconds until the watch sends the command.
Here's the catch: if the user wakes up the watch, the command still doesn't get sent to the iPhone.. UNTIL the user opens up the original app that sent the WKInterfaceController.openParentApplication. I don't think any of this would be a problem without this catch, the command should be queued until the watch is awakened, not awakened AND reopen the app.
Any workaround that can be done for this? I plan on logging this as a bug, but wanted to see if anyone has any thoughts or experience with this?

How to trigger communication with backend from Apple Watch?

I want to click on a push notification on my Apple Watch trigger communication with backend and show the results in a table on my Apple Watch.
I know how to show the result in a table on my Apple Watch. I also know the openParentApplication:reply: method.
But if I want to trigger my backend communication in application:handleWatchKitExtensionRequest:reply: I get a error that reply() is never called. It seems that iOS kill this method if it takes to much time.
If I test application:handleWatchKitExtensionRequest:reply: with a hard coded dictionary with only one entry, all works fine.
That is the recommended way to do this?
In my opinion I should do something in NotificationController.swift in didReceiveRemoteNotification method and app group but how can I trigger the backend communication on my iPhone?
UPDATE:
Part of my code in AppDelefate.swift:
func application(application: UIApplication!, handleWatchKitExtensionRequest userInfo: [NSObject : AnyObject]!, reply: (([NSObject : AnyObject]!) -> Void)!) {
var workaround: UIBackgroundTaskIdentifier?
workaround = UIApplication.sharedApplication().beginBackgroundTaskWithExpirationHandler({
UIApplication.sharedApplication().endBackgroundTask(workaround!)
})
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), {
UIApplication.sharedApplication().endBackgroundTask(workaround!)
})
var realBackgroundTaks: UIBackgroundTaskIdentifier?
realBackgroundTaks = UIApplication.sharedApplication().beginBackgroundTaskWithExpirationHandler({
reply(nil)
UIApplication.sharedApplication().endBackgroundTask(realBackgroundTaks!)
})
let testDict = ["Hello" : "World"]
reply(testDict)
UIApplication.sharedApplication().endBackgroundTask(realBackgroundTaks!)
}
This code works fine.
But if I change testDict to the backend communication code this method will killed.
Today I found the solution for my problem. You can not pass custom objects in the reply dictionary, you have to use primitive types.
The splution is described on the following page from Kristina Thai.
http://realm.io/news/watchkit-mistakes/
You'll need to start a background task on the iPhone to ensure your app isn't killed in the background before your API request can complete. I've shared some resources in a similar answer here: https://stackoverflow.com/a/29848521/3704092

Resources