Ruby All the iOS 8 Push Notifications!

Ruby is awesome. At Codelation we write a lot of Ruby code and love every new project that requires Ruby. It should come as no surprise for you to learn that we don't just use Ruby for our web apps, but we also use RubyMotion for creating native iOS and OS X apps. I won't be going into the advantages or disadvantages to using RubyMotion for native apps in this blog post, so let's just get down to business: How do you send push notifications from a Rails app to a RubyMotion app?

The following instructions will take you through every step of the process of sending push notifications from a Rails app to an iOS 8 app. I have a feeling I will be re-reading my own blog post a lot because while it is easy, you really need to remember every last step or it won't work.

image

1. Add Push Notifications to Your App ID in iOS Dev Center

The first thing we need to do is create the App ID. Navigate to the App IDs area and click the button for adding a new App ID and fill out the form.

Important: You need to select Explicit App ID in order to enable push notifications in your app.

After submitting the form, select the App ID you just created and select Edit. This will allow you add services for the App, and we need to add Push Notifications as shown here:

image

Now we need to create SSL Certificates for the App ID before push notifications will actually be enabled. These certificates will be used by our Rails app to securely communicate with Apple's push notification servers. The instructions for Development and Production will be the same:

Click on Create Certificate... to start the process and follow the instructions presented by Apple. The instructions are very good, so no surprises here. After adding both certificates the Push Notification section should look like this:

image

From here you'll want to download both the Development SSL Certificate and the Production SSL Certificate. After downloading them, double-click each of them to add them to Keychain Access. Here are my new keys as shown in the Certificates section of Keychain access:

image

2. Export the SSL Certificates From Keychain Access.app

Now that we have our development and production certificates from Apple, we need them in the proper format. If you don't still have the Keychain Access app open, go ahead and open it back up.

From within Keychain Access, select Certificates and right click on Apple Development IOS Push Services: com.yourcompany.appname and select Export:

image

In the save file dialog box, make sure the file format is set to Personal Information Exchange (.p12) and save the exported certificate as development.p12.

Export the Apple Production IOS Push Services: com.yourcompany.appname certificate the same way and save it as production.p12. You can name them whatever you want, but then you can't be as lazy with this next copy and paste bit.

3. Convert the SSL Certificates From .p12 to .pem

I don't claim to know what I'm doing here, but I know what needs to be done. We have our certificates in the .p12 format, and we need to create .pem files from them. This is easy. Open up a Terminal window, and use the following commands to convert each file:

# Development
openssl pkcs12 -in development.p12 -out development.pem -nodes -clcerts

# Production
openssl pkcs12 -in production.p12 -out production.pem -nodes -clcerts  

4. Set Up the APNS Gem in Your Rails App

There are a few RubyGems for sending push notifications to the Apple Push Notification Service if you search around. We're going to use the APNS gem because it's nice and simple.

Add the APNS gem to your Rails application's Gemfile:

gem "apns"  

And install with Bundler:

$ bundle install

We need to set up the APNS gem to work for us in a development environment as well as a production environment, and it's a little bit confusing when we are talking about the Rails environment vs the push notification SSL certificate.

Rails Environment vs APS Environment

If you don't have a good way to connect your device to your Rails app running locally, you probably want to deploy your Rails app to a remove server during initial development. It's much easier to build the RubyMotion app using rake device for testing than distributing via Ad Hoc, so our RubyMotion environment is going to be development if we build using rake device. This likely won't match up with our Rails environment on a remote server.

This is the initializer file I've come up with for dealing with different Rails and APS environments. It will always use the development certificate while the Rails environment is also development, but also allows us to set an environment variable so we can use the development certificate even if our Rails environment is set to production or staging.

# config/initializers/apns.rb

APNS.host = "gateway.sandbox.push.apple.com"  
APNS.pem = File.join(Rails.root, "development.pem")

# Set the environment variable `APPLE_SANDBOX` to use the development certificate in production
if Rails.env.production? && !ENV["APPLE_SANDBOX"]  
  APNS.host = "gateway.push.apple.com"
  APNS.pem = File.join(Rails.root, "production.pem")
end  

5. Set Up Your RubyMotion App to Receive Push Notifications

First we need to add the push notification entitlement by setting the aps-environment. When building the app with rake device, the environment should be set to development. When building the app for distribution, the environment should be set to production.

# Rakefile

Motion::Project::App.setup do |app|  
  # Building with `rake device`
  app.development do
    app.entitlements["aps-environment"] = "development"
  end

  # Building for Ad Hoc or App Store distribution
  app.release do
    app.entitlements["aps-environment"] = "production"
  end
end  

The next thing we need to do is register the device to receive push notifications. The following method is compatible with iOS 7 and 8 because we're first checking to see if we have the registerUserNotificationSettings method available on UIApplication.sharedApplication. Both code blocks then prompt the user to allow push notifications for your app and will register the app to allow alert, badge, and sound notification types.

def register_push_notifications  
  if UIApplication.sharedApplication.respondsToSelector("registerUserNotificationSettings:")
    settings = UIUserNotificationSettings.settingsForTypes(UIRemoteNotificationTypeAlert | UIRemoteNotificationTypeBadge | UIRemoteNotificationTypeSound, categories: nil)
    UIApplication.sharedApplication.registerUserNotificationSettings(settings)
  else
    UIApplication.sharedApplication.registerForRemoteNotificationTypes(UIRemoteNotificationTypeAlert | UIRemoteNotificationTypeBadge | UIRemoteNotificationTypeSound)
  end
end  

In iOS 8, we're not immediately registering for push notifications, but actually registering user notification settings, so in our AppDelegate, we need to define the didRegisterUserNotificationSettings delegate method for registering for remote notifications upon registering user notification settings. This step will be skipped on iOS 7 devices.

# app_delegate.rb

def application(application, didRegisterUserNotificationSettings: notificationSettings)  
  application.registerForRemoteNotifications
end  

Upon successful registration, the didRegisterForRemoteNotificationsWithDeviceToken delegate method will be called on the AppDelegate and will give us the device token we need for sending push notifications to the registered device. We'll want to save this token back to our Rails app. I'm going to leave this up to you, but I recommend supporting multiple devices per user so you don't run into any issues when users have multiple iOS devices.

# app_delegate.rb

def application(application, didRegisterForRemoteNotificationsWithDeviceToken: device_token)  
  # Save the device token back to the Rails app.
  # The token first needs to be converted to a string before saving
  string = token_to_string(device_token)
end

def token_to_string(device_token)  
  device_token.description.stringByTrimmingCharactersInSet(NSCharacterSet.characterSetWithCharactersInString("<>"))
                          .stringByReplacingOccurrencesOfString(" ", withString: "")
end  

You might also want to define the didFailToRegisterForRemoteNotificationsWithError delegate method for handling failed push notification registrations.

# app_delegate.rb

def application(application, didFailToRegisterForRemoteNotificationsWithError: error)  
  NSLog("%@", error.localizedDescription)
end  

6. Send Push Notifications From Your Rails App

It took a lot to get here, but we're finally ready to actually send push notifications to registered iOS devices. In the app I'm currently working on, a user can have multiple iOS devices registered. I created a Device model for storing the device token, and a User can have many devices. I also created a Notification model so that I don't have to change the application code if we need to support pushing to Android devices some day.

# app/models/notification.rb

class Notification < ActiveRecord::Base  
  after_save :push
  belongs_to :recipient, class_name: "User"

  # Pushes notification to each of the recipient's devices
  def push
    notifications = self.recipient.devices.map{|device|
      APNS::Notification.new(device.token,
        alert: self.alert,
        other: { some_extra_data: "can be sent too" }
      )
    }
    unless notifications.empty?
      APNS.send_notifications(notifications)
    end
  end
end  

With the Notification model defined, we can easily push notifications to each of the user's devices by creating a notification with the user as the recipient.

Notification.create(  
  alert:     "Hello World",
  recipient: user
)

7. Receive Push Notifications in Your RubyMotion App

Finally, we want to do something with the push notifications received by the device. By setting alert on the APNS::Notification, the message will show up in Notification Center on the device and alert the user based on their settings, but you'll need to define the didReceiveRemoteNotification delegate method on your AppDelegate in order to do anything more.

# app_delegate.rb

def application(application, didReceiveRemoteNotification: user_info)  
  # The push notification information can be accessed on the `:aps` key
  NSLog("%@", user_info[:aps][:alert])

  # The `:other` data you set on the APNS::Notification can be accessed directly
  NSLog("%@", user_info[:some_extra_data])
end  

8. Profit!

Because that's how all good blog posts should end. If you have any questions or comments, please feel free to contact me @brianpattison.