WKInterfaceController.openParentApplication is delayed on real device? - ios

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?

Related

How to resolve these CloudKit Background Task Warnings

I have a functioning iOS app that is producing some troubling warning messages and I'd like to resolve them.
From the console:
Background Task 37 ("CoreData: CloudKit Import"), was created over 30 seconds ago. In applications running in the background, this creates a risk of termination. Remember to call UIApplication.endBackgroundTask(_:) for your task in a timely manner to avoid this.
This warning is repeated periodically, however, each time with a different Task Id. I presume that if I can capture the Task Id, I could figure where to call
UIApplication.endBackgroundTask(_:)
I do not know if it's possible to obtain the Task Id. I do have a notification observer set to check for data changes. I'd prefer not to remove that.
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [NSObject : AnyObject]) {
OperationQueue.main.addOperation({ () -> Void in
self.fetchProjects()
})
}

Core Data fetch request with WatchConnectivity

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.

Pushes open tracking with Parse

I have implemented the push open tracking like described here https://parse.com/docs/ios/guide#push-notifications-tracking-pushes-and-app-opens.
If i open the push on the device, in the parse dashboard, the push opens number is updated on the previous push sent.
For example, if i send the push "Hello" twice and open it twice on the device, the push opens will be 1 for the first push, 0 for the last one.
I can reproduce it if the app is in background or if it's closed.
I use the latest pod Parse (1.11.0) and the application is in Swift.
The source code :
func application(application: UIApplication, didReceiveRemoteNotification userInfo: [NSObject : AnyObject]) {
if application.applicationState == .Inactive {
PFAnalytics.trackAppOpenedWithRemoteNotificationPayload(userInfo)
}
}
I tried with the following code to check if the BFTask is completed. Everything is fine and the task has the completed status.
PFAnalytics.trackAppOpenedWithRemoteNotificationPayload(userInfo).continueWithBlock({ (task) -> AnyObject! in
assert(task.completed, "task should have completed")
assert(!task.cancelled, "task should not have been cancelled")
assert(!task.faulted, "task should not complete due to error or exception")
return task
})
Did someone experience the same issue ?

Parse Push Notification Payload nil in didFinishLaunchingWithOptions

Parameters: iOS 9.1, iphone 5S, xcode 7.1, Swift 2, Parse SDK 1.9.0
I am trying to capture push notification messages sent via Parse for all states of the receiving device (i.e. foreground/background/not loaded/device turned off). For all cases, my device receives the push notifications but in some cases the payload/userInfo dictionary does not arrive.
For situations where:
func application(application: UIApplication, didReceiveRemoteNotification userInfo: [NSObject : AnyObject], fetchCompletionHandler completionHandler: (UIBackgroundFetchResult) -> Void)
is called (e.g. app in foreground) in the AppDelegate, the userInfo dictionary is received correctly and I can save the message, issue a notification & display it in an appropriate ViewController.
However, where
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool
is called in the AppDelegate (e.g. if device is turned off and the push notification banner is "swiped" to open the App), the payload obtained from the statement:
if let notificationPayload:NSDictionary = launchOptions?[UIApplicationLaunchOptionsRemoteNotificationKey] as? NSDictionary
is always nil and I cannot complete the same actions as above.
The statements that generate the push are:
push.setData(["alert" : message, "badge": "Increment", "content-available": NSNumber(integer: 1)])
push.sendPushInBackground()
where message is a String containing the message to be displayed.
I have read numerous posts on SO, etc. & tried various ways to solve this but cannot find a solution. Any ideas/assistance would be greatly appreciated. I can post more code if necessary but I am basically following the standard approach from Parse examples & other SO posts.
RB

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