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 2.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.
via
Chebli Mohamed