I am getting some very strange behavior with my WatchKit handling of local notification actions that I'm pretty sure is a system bug. I'm wondering if anybody else is seeing the same thing.
(This is using iOS 8.4 and WatchKit 1.0, with an Objective-C app build with Xcode 6.4)
It's too much code to post, and the code is property of the client, so I'll have to describe it.
The background:
I am adding custom "long look" notification support to a client's app.
The app creates geofences around vendor locations. When the user enters one of the geofences, the location manager send a didEnterRegion message to my class that handles geofences.
I turn around and generate a local notification of a defined category. That category is defined as having 2 different UIMutableUserNotificationActions attached to it, one for showing more info about the vendor, and one for displaying driving directions to that vendor's location. (We won't talk about the fact that the user is in shouting distance of the vendor when the geofence fires, so they can SEE the vendor's shop. This is what the client wants, and he's paying me to do it this way.)
The local notification is set to fire immediately (or for testing, I create notifications set to fire in a few seconds.)
The system can do one of 3 things when the notification fires.
1. If the app is running in the foreground, it will send the app delegate a `application:didReceiveLocalNotification:` message.
2. If phone is awake but the app is in the background, it displays the local notification with a system banner/alert (depending on the user's settings.) That banner/alert has buttons for my 2 actions.
3. If the phone is locked and the user has a paired Apple Watch that is allowed to receive notifications, the local notification is sent to the watch.
The Apple Watch app has a custom subclass of WKUserNotificationInterfaceController that is set up to handle this category of user notifications. It adds an image, a title, and a message body to a custom interface controller, with data it gets from the the userInfo dictionary attached to the local notification.
If the user taps one of the action buttons on the WKUserNotificationInterfaceController ("more info" or "directions"), the watch's main interface controller gets a handleActionWithIdentifier:forLocalNotification: message. The code is set up to then send an `openParentApplication:reply:error:' message to the iPhone app. It passes along the user info dictionary it received in the local notification.
The iPhone app responds to the openParentApplication:reply:error message by either requesting driving directions from the location manager (which launches the maps app) or displaying the appropriate info page from the app for the specified vendor.
If the phone is locked when the watch sends the `openParentApplication:reply:error: message to the iPhone, the user doesn't get any feedback, since the phone is locked and Apple doesn't allow a phone to wake itself up.
In that case I therefore invoke the reply block with a dictionary entry of #{#inBackGround: #(YES)}. The watch's reply block checks for inBackground==YES, and if it is, it displays a message to the user that they nee to open the iPhone app in order to see the info/directions.
The problem:
If I launch the iPhone app and trigger a local notification when the phone is locked the first time, the message goes to the watch, the watch displays my custom long look with "more info" and "directions" buttons, and tapping one of the action buttons invokes the watch's handleActionWithIdentifier:forLocalNotification: method, as expected. The watch's handleActionWithIdentifier:forLocalNotification: method sends an openParentApplication:reply:error message to the phone, and the phone displays the appropriate response to the user when the user goes back to the app.
However, the problem comes in if I then trigger a new local notification (also with the phone locked) for a different vendor, with different GPS coordinates and userInfo that points to a different screen of information to display on the phone. When my watch buzzes and I raise it to my wrist, as the "long look" notification for the new local notification is displayed, the watch's handleActionWithIdentifier:forLocalNotification: method fires again, with the identifier and userInfo dictionary from the previous local notification. (I haven't tapped any action buttons on this new notification, or responded to a local notification message on the phone.)
Then, if the user clicks the "more info" action button on the watch's new long look notification controller, that action fires.
The result of this is that when the user goes to his phone, he sees the information for the new vendor he asked about, but when he clicks that away, there is a duplicate copy of the info for the first vendor on his screen.
I've debugged this very carefully, and confirmed that the watch app's interface controller's handleActionWithIdentifier:forLocalNotification: method is being called spuriously. I added a unique NSDate timestamp to the userInfo in the local notification that the iPhone posts, and I see that exact same timestamp repeated in the second (spurious) invocation of the first handleActionWithIdentifier:forLocalNotification: when the second long look notification is displayed.
Has anybody else run across this problem? I guess it's time to file a Radar bug, but I'm not sure what set of steps from my client's app triggers the problem, and it might take me a full day or more to work out a minimum demo app to demonstrate the problem. I know from experience that Apple won't pay any attention to my bug report unless I give them an app that lets them create a repeatable fail-case, along with painfully detailed instructions on how to use it.
The fix:
The fix I have come up with is a dreadful hack. On the phone, I embed a unique "actionFireDate" NSDate into the userInfo dictionary for the local notification. On the watch, I create an empty NSMutableSet "actionFireDates" at startup. When I get a handleActionWithIdentifier:forLocalNotification: call, I get the userInfo for the local notification, get the timestamp NSDate I put in the userInfo dictionary, and check to see if that unique NSDate is in my "actionFireDates" set. If it is, I simply ignore the action. If it's not, I add the new NSDate from the userInfo dictionary in to my set of action fire dates.
I'm pretty sure this is a system bug, and have opened a bug at Apple's bug reporter website (A.K.A. Filed a "radar bug".)
The fix I came up with for this was to add code to my custom WKInterfaceController class's awakeWithContext method that calls dispatch_after to call dismissController 10 seconds after the alert is displayed. If the user does nothing, the alert goes away on it's own.
I'm not going to accept my answer for another couple of days in case somebody else has insights to share on the subject.
EDIT:
Apple closed my Radar bug as a duplicate. I take that to mean that this really is a system bug.
Related
As of the new 3D Touch capabilities with the new iPhone 6s/6s+, I'm trying to add some home screen quick actions to my app.
I was able to implement the normal flow of force touching the app's icon in the home screen -> choose one of the quick actions available -> taking care of it properly in all possible app states.
My question is: Is it possible to create a silent action among the available quick actions? By silent I mean that a certain action will take place, yet the app won't complete its launch? Or alternatively launch but won't be in foreground?
UPDATE
I'll elaborate on what I'm trying to achieve - I want to have similar behaviour to the one HealthKit offer with its background delivery - where upon a change in the store, HealthKit wakes my app and give me a chance to do something in the background (with HealthKit example - query for the new data in the store).
After reading much of Apple's documentation on the topic I have the feeling it is not possible with the current API available - but I hope someone will surprise me...
Nope. The user invoking a home screen Quick Action always activates the app.
If your app was already running and is suspended, it comes to the foreground and your app delegate gets the application:performActionForShortcutItem:completionHandler: message. If your app has not been running (i.e. has not been run since install, or was previously backgrounded/suspended but later purged from memory), it launches and your app delegate gets the application:didFinishLaunchingWithOptions: message and then the application:performActionForShortcutItem:completionHandler: message. (So, your did/willFinishLaunching handler needs to check the options dictionary for the possibility of launch via quick action.)
Either way, your app comes to the foreground.
I'm using Plot Projects service to send geofencing notifications to users of iOS and Android application.
I want to use "dwelling" event to trigger specific notifications when user remains at one specific geofence for some extended time. The documentation states that dwelling event can be used on iOS, but has certain specifics:
Please note that due to restrictions in iOS the notification filter for dwelling notifications is called when the user enters the geofence or beacon region and that the returned notifications are only shown when the user remains in the region for the specified amount of time.
By my understanding, this would mean that the Notification filter gets triggered as soon as user enters the geofence, but the notification, if properly filtered, will be displayed after user dwells there. Filtering logic in my case is done on server-side - iOS app sends notification info to server, and then appropriate logic is applied to decide whether to show notification or not.
So, server-side logic for checking whether to show notification or not would be triggered at the time user trips geofence, but the notification would be shown to the user once he dwells there for some time. In my specific case, in order to properly decide whether to show the notification or not, I'd need the check to be done at the time the user really dwells, and not on entering. My understanding is that this cannot be done on iOS (unlike Android).
Am I right to assume this? If not, what would be the way to achieve a dwell-time filtering check, as opposed to enter-time filtering check?
You're correct about the moment when the Notification Filter will trigger on iOS. This is done because of platform limitations. The filter will be called directly when you enter the geofence. When you want filter out messages, that is indeed the time to do so. There is no way to filter at the time of the end of the dwelling period.
This, as mentioned, differs from the behaviour on Android. There it will be called at the end of the dwelling period.
As of the new 3D Touch capabilities with the new iPhone 6s/6s+, I'm trying to add some home screen quick actions to my app.
I was able to implement the normal flow of force touching the app's icon in the home screen -> choose one of the quick actions available -> taking care of it properly in all possible app states.
My question is: Is it possible to create a silent action among the available quick actions? By silent I mean that a certain action will take place, yet the app won't complete its launch? Or alternatively launch but won't be in foreground?
UPDATE
I'll elaborate on what I'm trying to achieve - I want to have similar behaviour to the one HealthKit offer with its background delivery - where upon a change in the store, HealthKit wakes my app and give me a chance to do something in the background (with HealthKit example - query for the new data in the store).
After reading much of Apple's documentation on the topic I have the feeling it is not possible with the current API available - but I hope someone will surprise me...
Nope. The user invoking a home screen Quick Action always activates the app.
If your app was already running and is suspended, it comes to the foreground and your app delegate gets the application:performActionForShortcutItem:completionHandler: message. If your app has not been running (i.e. has not been run since install, or was previously backgrounded/suspended but later purged from memory), it launches and your app delegate gets the application:didFinishLaunchingWithOptions: message and then the application:performActionForShortcutItem:completionHandler: message. (So, your did/willFinishLaunching handler needs to check the options dictionary for the possibility of launch via quick action.)
Either way, your app comes to the foreground.
This may be contrary to HIG...it is not standard..but it occurred to me that after a user sends feedback from withinmy app, it might be nice to flash a quick unobtrusive message "Thank you for your feedback" or something. I don't want to hit the user with a full blown alert. But a discreet notification banner along the top might be nice.
Is it possible to do this or is it disallowed?
Thanks for any suggestions.
If this notification is initiated from an action from within your app, an Apple notification may not be necessary. You want to simply show a thank you message, so it doesn't even have to wait for a response from a server, but you may want to check if you have Internet connectivity, just to be able to say that the message couldn't be sent, and offer the option to retry.
These are good options for a Toast-style alert that Android uses and is unobtrusive:
https://www.cocoacontrols.com/controls/toast
https://github.com/scalessec/Toast
You can configure it to slide in from the top or bottom. And, it slides away without user interaction.
Local notification will be received in this case but will not be displayed as you want it. However you can make a custom view similar to iOS view. Also please check https://github.com/OpenFibers/OTNotification
Unless the notification is from other application in background,
your currently active app has every right and responsibility in interacting with user with any types of visuals.
There are no class from iOS SDK to allow you to use the same notification banner used for Push Notification. To achieve the same result while your app is active, you may need to adopt your own solution or a module.
From Apple's Push Notification Guide:
If the target application isn’t running when the notification arrives, the alert message, sound, or badge value is played or shown. If the application is running, iOS delivers it to the application delegate as an NSDictionary object. The dictionary contains the corresponding Cocoa property-list objects (plus NSNull).
I have implemented this in my app and everything works fine. If the app is in focus, the app gets the message directly. If not active, an alert is shown, the app launches when the user clicks the alert, and finally the app gets hold of the message.
Would it be possible, however, in the case of a message arriving when the app is not active, to get iOS to activate the app and pass the message on without showing any message or requiring any user interaction?
I would like this behavior because the push message from my server only might be of interest to the user, depending on her current position. The app works like this: When it starts, it registers for push and tells my server: I am at this position and would like to be notified when something interesting happens near me. At a later point the server sends a message, but since the user might have moved from the area, I would like the app to check the user's position again, and not bother the user if she now is too far from the original position.
I suppose it would be possible to have a background service that notifies the server about the current position every n minutes, but I fear that this will drain the battery.
Any thoughts on this?
Unfortunately, you can't — in iOS — directly open your app when a notification is received. The user must choose himself to open it via the alert displayed by the system.
However, using the background location is not that battery-unfriendly. It depends on the location accuracy you set for your CLLocationManager object.
All the informations about location accuracy can be found here : Location Awareness Programming Guide
In your case, you may want to use the significant location changes methods or the kCLLocationAccuracyKilometer accuracy for example.
Here is a good tutorial to get started : iOS Multitasking: Background Location
Hope this will help.