At TaskRabbit, we use push notifications heavily in our apps. The first implementations of push were simple blocks of conditional logic in view controllers that could respond to notifications. After we started adding many notification events to the app, we realized pain points of this design and iterated on how to implement push.
A small APNS payload
As payloads from APNS are limited in size, we use a short string to indicate
what type of event should happen in the app: event_type
. For most
notifications, the unique object id of the related model is also included.
The payload is immediately serialized to a TRRemoteNotification
- the
application’s representation of the event. We use an enum to represent the
remote event type which is serialized from the event_type
string in the
payload. An enum is easy to work with and is defined in the app at build time.
This enum also serves to ensure that is a finite set of event types.
Dispatching notifications via two simple protocols
@protocol TRUIRemoteNotificationProtocol <NSObject>
- (id)initWithNotification:(TRRemoteNotification *)notification;
@end
@protocol TRUIRemoteNotificationResponderProtocol <NSObject>
- (BOOL)isFirstResponderForNotification:(TRRemoteNotification *)notification;
- (void)didReceiveNotification:(TRRemoteNotification *)notification;
@end
Notifications in navigation controller based apps
We dispatch notifications immediately after serializing a
TRRemoteNotification
, by setting the notification on the navigation
controller.
@interface UINavigationController (TaskRabbit) <UINavigationControllerDelegate>
- (void)pushNotificationFirstResponder:(id
<TRUIRemoteNotificationResponderProtocol>)responder;
- (__weak id
<TRUIRemoteNotificationResponderProtocol>)popNotificationFirstResponder;
- (void)setNotification:(TRRemoteNotification *)notification
animated:(BOOL)animated;
@end
In order to respond to notifications, an object must be pushed onto the
navigation controller’s responder stack. This responder chain is like
UIResponder
, but for remote events.
Similar to UIResponder
, we enumerate through the responder chain,
checking if each object can respond to the notification by calling
isFirstResponderForNotification:
. If the object does respond, the
notification dispatch process is complete. If not the next object in the
responder chain is given the opportunity to respond. Only 1
TRUIRemoteNotificationResponderProtocol
can respond to an instance of a
TRRemoteNotification
.
Any object can sign up to respond to notifications on by adding its self to the
responder chain. In our applications, only view controllers implement these
protocols and respond to notifications. viewWillAppear:
is the point in the
view controller life-cycle where most view controllers will sign up.
Since a deallocated view controller cannot receive notifications and the
responder chain shouldn’t create a strong reference to the responder: a
NSPointerArray
with weak references is the perfect collection to implement
the responder chain. The original implementation used a NSMutableArray
as a
data structure, which required each responder to be explicitly removed.
Building the same hierarchy a user would navigate to
If there is no responder for the first notification, the next step is to check if we need to build a new navigation hierarchy.
We load the notification completely before presenting the new UI stack.
At this point in the notifications life-cycle we need new view controllers to
display. We added a asynchronous factory method on UIViewController
to start the
loading process. In the case of the application being active, the user is not
notified until loading has completed.
@interface UIViewController (TRRemoteNotification)
+ (void)viewControllersForNotification:(TRRemoteNotification *)notification
completion:(void (^)(NSArray *viewControllers))completion;
@end
We save an array of UIViewController
class names that conform to
TRUIRemoteNotificationProtocol
so that we can build an array of view
controllers. Each TRRemoteNotificationEventType
has a hierarchy of view
controller classes defined.
Beyond push - deep linking support and URL schemes.
After we implemented this pattern to respond to notifications, we realized we
wanted the same functionality when the app was opened from a URL. Using the
notification protocol to handle URL was obvious because it only requires an
object id and event_type
to load the model and present the UI. Since this
design only requires 2 strings, our URL scheme is simple and the deep links are
short.
Conclusion
We handle lots of push notification types as well as URL schemes in our apps. This design isn’t a magic bullet for handling remote events, but it is easier to follow and more robust than massive blocks of conditional logic.