iOS push notifications are (in my personal opinion) suprisingly difficult to implement properly and debug.
This is especially suprising given that Apple have provided such in depth documentation, and given that there are such an abundance of tutorials on the topic.
The problem is that no single resource has a succinct yet complete explanation. Here it is.
The user needs to be registered for APNS to receive notifications
-
A user can not, and will not ever receive APNS notifications from your app unless they have given explicit permission.
-
In Swift, this can be done with the following code snippet:
let types = UIUserNotificationSettings(forTypes: [UIUserNotificationType.Alert, UIUserNotificationType.Badge, UIUserNotificationType.Sound], categories: nil)
UIApplication.sharedApplication().registerUserNotificationSettings(types)
UIApplication.sharedApplication().registerForRemoteNotifications()
- Many suggest placing this in your app delegate to request permission on the initial application load. I disagree - you should request permission in response to any explicit user action e.g in response to clicking 'Set up notifications' on your 'Settings' page.
If you want to download data in response to receiving a notification
- you must add the following property to your plist
<key>UIBackgroundModes</key>
<array>
<string>remote-notification</string>
</array>
- You can make your notifications silent by specifying an empty 'sound' key in your payload. Likewise you can display no notification message by setting the 'message' key to an empty value.
Handling all application states
-
If your app is open, and in the foreground you will receive one call to
application:didReceiveRemoteNotification:fetchCompletionHandler:
. -
You can check the
applicationState
property of the passed inapplication
argument. In this case the state will beUIApplicationState.Active
-
To respond to receipt of a call to this delegate method I find it best to dispatch a
NSNotification
from the default notification center instance. Having already set up an observer in the appropriate view controller, you can pass control over to said controller easily on receipt of the notification. -
If your app is in the background, you will receive two calls to
application:didReceiveRemoteNotification:fetchCompletionHandler:
. The first one when the notification is received. The second when the notification is clicked. -
The first time your application state will be
UIApplicationState.Background
. The second time it will beUIApplicationState.Inactive
-
Because the application is open (it is just in the background), on receipt of the first call I (again) dispatch a NSNotification that the appropriate controller is listening for. On the second call (when you are bringing the app to the foreground), I do nothing - there is no need to dispatch a second notification.
-
*If your app is closed, you will only receive one call to
application:didReceiveRemoteNotification:fetchCompletionHandler:
- when the notification is clicked. -
When closed, there is the additional consideration that your view controller has not been instantiated fully at the point that the
application:didReceiveRemoteNotification:fetchCompletionHandler:
delegate method is called. As such there is no point dispatching a NSNotification as the observer to listen for it is not set up. -
In this case, I access and manipulated my view controller directly. You can cast the
visibleViewController
as you (should) know what your initial view controller is.
let postsController = rootNavController?.visibleViewController as! PostsController
- You can then set properties directly on the view controller instance which you can then check for (and act upon) in the
viewDidLoad
method of the controller.
Tips and tricks
-
If you pass a 'badge' value in the array set as the value for the 'aps' key of your payload, then the system will set the badge on your application icon even if your application is closed.
-
When your application is open or in the background, you can access the current badge value using:
let currentBadgeNumber = UIApplication.sharedApplication().applicationIconBadgeNumber
-
You can use this knowledge to increment/decrement the badge count within your
didReceiveRemoteNotification
based on downloaded data. You cannot increment/decrement when the application is closed. -
When you open the (previously closed) app by clicking on a notification, you can see if the app is being opened due to such a click in the
didFinishLaunchingWithOptions
method of your AppDelegate with the following code:
let notificationInfo = launchOptions?[UIApplicationLaunchOptionsRemoteNotificationKey]
if (notificationInfo != nil) {
//do something
}
- APNS IDs change ! Even the same version of an application on the same device can have a new APNS ID should you uninstall and then reinstall it. Always check, and keep your server in sync. You might not be receiving notifications because you are sending them to a registration ID that is no longer valid
Register the registered every time
-
That final tip is important enough to warrant its own section. I like to register any previously registered user for APNS in my
didFinishLaunchingWithOptions
AppDelegate method every time -
registerForRemoteNotifications
is clever. As noted in the documentation:
If your app has previously registered, calling registerForRemoteNotifications results in the operating system passing the device token to the delegate immediately without incurring additional overhead.
- That is to say, you may as well check every time the application is loaded if the registration ID has changed. When you sync you registration ID with the server you should also save it locally (perhaps in NSUserDefaults or in your keychain - see SSKeychain). Then in the
didRegisterForRemoteNotificationsWithDeviceToken
delegate method you can check whether the registration ID has changed.. and if it has, resync it with the server.
The server
-
I am not going to reinvent the wheel. The PHP server code outlined in this tutorial from Ray Wenderlich is good.
-
One thing to note. I found that sometimes, without reason I would get disconnected from the APNS server. I recommend that you dont hide the error output of the following line:
$result = fwrite($this->fp, $msg, strlen($msg));
- That way you can debug any issues that occur. I didn't find any particular reason for my occasional disconnects, so I simply implemented the appropriate code to establish a new connection should a disconnection occur.
Testing APNS
-
You obviously want to test that your notification setup works correctly prior to releasing your application.
-
Given that APNS profiles are associated with your application bundle identifier I have found that is significantly easier to have distinct bundle identifiers for your development and production applications. This has the added benefit that you can have a development version, and production version of your app on your device side by side. Likewise you can utilise different app icons for each. I recommend that you follow this guide from the team at Circle to get things set up.
-
You can utilise a single bundle identifier (if you wish), but to provision an application for testing (in a manner that will work with the APNS sandbox server) you have to export the application for 'Ad Hoc' deployment from your 'Organizer' and manually put it on your device (through iTunes).
-
Assuming you have gone for the former (better) option, you can simply setup a local server instance to send notifications to the APNS sandbox. Easy.
-
If you are not receiving notifications, my first recommendation as regards debugging is to always make sure you are connecting to the correct APNS server (sandbox vs production), and to make sure that you are using the correct APNS registration ID. Debug and production keys are different.
Conclusion
Having spent a long time looking into the intricacies of APNS it now seems incredibly simple. That said, there are a number of moving parts, and the smallest oversight can cause your notification system to not work at all.
Hopefully the above provides a succinct explanation of those intricacies such that you can implement APNS within your applications without issue.
Let me know how you get on, and if you have any questions/require further clarification.. let me know.