iOS Push Messaging with ZeroPush
I have recently been working on two applications that deliver push messages to subscribed iPhones and iPads. While building out the infrastructure, I learned quite a bit about the idiosyncrasies of the Apple Push Notificiation System and various solutions that have sprung up to provide a better interface to APNS.
Your ideas are intriguing to me and I wish to subscribe to your newsletter
When you allow an application to subscribe you to their push messages, here’s what happens in a nutshell:
- Your application sends a subscription request to Apple’s APNS gateway
- APNS responds with a unique token. This token is what ties your device to a registered iOS application.
- Your application then takes that token and sends it to some server-backed application.
One long-time iOS developer I talked to expressed surprise and disappointment that Apple still hasn’t provided their own service to execute delivery of pushes. As it stands, if you want to send pushes to your app users, you must have your own server-side application or register with a service like Urban Airship or ZeroPush.
You send push messages to a given application by sending APNS a message that consists of the token along with some combination of message text, an update for the application’s badge count, a sound to be played, and some optional data that can be used to determine things such as where to go in the application when the push message is swiped. It’s important that you don’t know anything about the user from their token other than that they registered. If you want to tie the subscription to a specific user (which you’ll want to do if any of your messaging is targeted or individualized rather than just broadcasts to all users), you’ll want to send a UID or email address or other identifier to your server application at the same time you forward along the token.
The biggest challenge with push messaging is dealing with Apple’s quirky behavior when you start sending push messages at any kind of volume. When you send a push message through APNS, you open a socket connection to the APNS gateway, send your message, and then close the socket. If you are opening and closing sockets too frequently, Apple will think you are a potential DOS attacker and temporarily ban you from connecting. The way around this is to open your socket and then send many messages on a single connection.
Here’s the problem: This connection is asynchronous, which means you’re sending data over the socket and not waiting for a response from Apple for any particular message. If this sounds perilous, you’re right! If you send a message that is declined for any reason, such as sending a bad token, etc., APNS unceremoniously terminates the socket without any error message. Because the connection is asynchronous, APNS may terminate the socket due to a message that is not the last one you sent, which makes it very difficult to know which message or token was the culprit.
After experimenting with APNS on Rails, Grocer, Orbiter (via Helios) and Houston, I was extremely frustrated. None of the gems handled the issue of bad tokens (all of which had entered my database during user testing with my client’s client) causing APNS to kill my socket. A pull request to Houston that has since been accepted appears to have solved the issue, but at the time of our development, but we began by using a combination of a fork of Houston including the socket fix pull request. Apple requires app developers to clean out bad tokens, which are usually the result of users unsubscribing from push messages, so we were also using a small part of the Grocer to managing the sweeping and removal of bad tokens. Many of these gems are still very immature, and there isn’t yet a clear standard in this space.
I began to investigate using a service rather than home-built code to abstract away the rougher edges of dealing with APNS. UrbanAirship is the dominant player in this space, but their pricing for lower-volume senders is fairly high. Through the Heroku addons portal we found ZeroPush.
ZeroPush is still in beta, but we found that they had the holy trinity of service-layer software: an easy API, cheap pricing, and awesome customer service. After some initial testing with ZeroPush and their gem, I found that it wasn’t designed for multi-threaded operation. My client has multiple iOS applications powered by our backend, so I needed to be able to open multiple simultaneous connections to APNS, one for each application that might be sending messages. I sent a message to ZeroPush’s customer service address, and minutes later Stefan at ZeroPush called me to discuss the problem. Less than two hours later I had a recommended workaround in place. Less than two days later, ZeroPush had already made upgrades to their gem that helped solve my problem without the workaround we had created.
I cannot recommend ZeroPush highly enough. Their API is very simple, has good documentation, and we found that it was a very reliable way to interface with APNS.
Using ZeroPush’s interface, we were able to trim our APNS code down to just a few classes. Here’s how we send batch messages to ZeroPush:
# Delivers push notifications to APNS.
@organization = organization
@connection = ZeroPushConnection.new(organization)
attr_reader :connection, :organization
delegate :apns_notifications, :ios_application, to: :organization
unsent_notifications.each do |notification|
ZeroPush handles the socket connection for us, sparing us from the worry of having to manage the connection. We just send messages to ZeroPush, and they handle delivery and reporting unsubscribed tokens for us. It’s worth noting that the PushNotificationsDelivery class described above is provides us with a layer of abstraction that will allow us to change providers or add Android push messages (as the client will be doing this month) easily without huge changes to our code.
Using a third party service also keeps our specs clean. Our model test verifies that the right message gets sent to the ZeroPush client object, and integration tests verify that we are hitting the ZeroPush sandbox gateway.
Give it a try with your own app.