iOS Mobile Push Notifications
HTTP/2-based APNs
This APNs tutorial uses the HTTP/2-based Apple Push Notification service (APNs), not the legacy APNs binary interface, to send notifications.
PubNub Mobile Push Notifications gateway bridges native message publishing with third-party push notification services including Apple Push Notification service (APNs) and Firebase Cloud Messaging (FCM). The following tutorial will show a sample implementation of the iOS app delegate methods needed to register for remote notifications, receive the corresponding token and add the device token to a PubNub channel.
Step 1: Configure Account Settings
Follow the steps of this APNs tutorial to configure APNs Mobile Push Notifications on your app.
Step 1a: Create an APNs Authentication Token
Log in to your Apple Developer account.
Go to Certificates, IDs & Profiles, and select Keys.
Create and obtain a valid APNs Authentication Token (.p8
file extension).
After downloading, the Auth Key filename will look like this AuthKeyABCD1234.p8, the ABCD1234 is the Key ID for this key, we will need this Key ID later. You can also find the Auth Key ID by selecting your push token from your list of available Apple Developer Account Keys.
Step 1b: Configure Mobile Push Notifications in the Admin Portal
On the Admin Portal, go to the selected app's keyset.
Enable Mobile Push Notifications and provide values for the Team ID and Auth Key ID (Apple's key identifier) fields.
Then, upload your APNs Authentication Token through the Token File option.
Step 2: Request Device Token
To add Notification support to your application, start by enabling the Mobile Push Notifications capability in your Xcode project.
For this example, all the code will be put inside the AppDelegate
, but any globally accessible object can be used to provide the additional non-system functionality.
Import PubNub into your project:
- Swift
- Objective-C
import UserNotifications
import PubNubSDK
#import <UserNotifications/UserNotifications.h>
#import <PubNub/PubNub.h>
When your application is launched, or whenever appropriate for your use cases, you can use UNUserNotificationCenter
to request notification permissions from your user.
Inside the completion block, you can request a Device Token specific to your application.
- Swift
- Objective-C
For more information regarding requesting tokens, go to Apple Docs.
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil
) -> Bool {
UNUserNotificationCenter.current().delegate = self
UNUserNotificationCenter.current().requestAuthorization(options: [.badge, .alert, .sound]) { (granted, err) in
DispatchQueue.main.async() {
UIApplication.shared.registerForRemoteNotifications()
}
}
return true
}
For more information regarding requesting tokens, go to Apple Docs.
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[self registerCustomDispatchQueues];
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
center.delegate = self;
[center requestAuthorizationWithOptions:(UNAuthorizationOptionSound | UNAuthorizationOptionAlert | UNAuthorizationOptionBadge)
completionHandler:^(BOOL granted, NSError * _Nullable error) {
dispatch_async(dispatch_get_main_queue(), ^{
[[UIApplication sharedApplication] registerForRemoteNotifications];
});
}];
show all 17 linesRefer to the Apple Developer documentation for more information on how to register your app and obtain an APNs Authentication Token.
Step 3: Receive & Monitor Device Token
Calling registerForRemoteNotfiications()
will initiate the registration process with Apple Push Notification service (APNs).
The system will provide a token via the UIApplicationDelegate
by calling the following delegate method. After the initial APNs registration and device token has been delivered, new (sometimes duplicate) tokens will be delivered to this delegate method and your PubNub channel registrations will need to be updated.
- Swift
- Objective-C
For more information on receiving token, go to Apple Docs.
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
let oldDeviceToken = self.cachedToken
guard deviceToken != oldDeviceToken else { return }
self.cachedToken = deviceToken
updateAPNSDevicesOnChannels(
pushChannels.allObjects, newDevice: deviceToken, oldDevice: oldDeviceToken,
on: "com.mycompany.mybundleid", environment: .production
) { result in
switch result {
case .success: print("Successfully updated channels with new token")
case let .failure(error): print("Failed to update device token due to: \(error)")
}
}
}
For more information on receiving token, go to Apple Docs.
- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
NSData *oldDevice = self.cachedToken;
if ([deviceToken isEqualToData:oldDevice]) { return; }
self.cachedToken = deviceToken;
[self updateAPNSDevicesOnChannels:[self.pushChannels allObjects]
withDevicePushToken:deviceToken
replacingDevicePushToken:oldDevice
pushType:PNAPNS2Push
environment:PNAPNSProduction
topic:@"com.mycompany.mybundleid"
andCompletion:^(PNAcknowledgmentStatus * _Nonnull status) {
if (!status.isError) {
NSLog(@"Successfully updated channels with new token");
} else {
show all 19 linesStep 3a: Cache Device Token & Registered Channels
To ensure that an application can properly handle adding, updating, and removing registered push channels there are two pieces of information that should be cached to avoid race conditions: the Device Token and the list of registered channels. Not only will properly caching allow for easy access from anywhere inside the application, but it will also prevent race conditions when multiple registration operations are queued at the same time.
UserDefaults
provides basic persistent storage, but can be replaced by more sophisticated storage as your use-case requires. The following code will ensure the accessing of the cached information will be thread-safe regardless of storage choice.
A new Device Token can be provided from the system at any time, and should be stored whenever it's received.
- Swift
- Objective-C
let tokenDispatch = DispatchQueue(label: "com.pubnub.deviceToken", attributes: .concurrent)
var cachedToken: Data? {
get {
var token: Data?
tokenDispatch.sync {
token = UserDefaults.standard.data(forKey: "com.pubnub.deviceToken")
}
return token
}
set {
tokenDispatch.async(flags: .barrier) {
UserDefaults.standard.set(newValue, forKey: "com.pubnub.deviceToken")
}
}
}
static dispatch_queue_t deviceTokenDispatch;
static dispatch_queue_t pushChannelsDispatch;
static dispatch_once_t registerCustomDispatchOnceToken;
- (void) registerCustomDispatchQueues {
dispatch_once(®isterCustomDispatchOnceToken, ^{
deviceTokenDispatch = dispatch_queue_create("com.pubnub.deviceToken", DISPATCH_QUEUE_CONCURRENT);
pushChannelsDispatch = dispatch_queue_create("com.pubnub.pushChannels", DISPATCH_QUEUE_CONCURRENT);
});
}
- (nullable NSData *) cachedToken {
__block NSData* token;
dispatch_sync(tokenDispatch, ^{
token = [[NSUserDefaults standardUserDefaults] dataForKey: @"com.pubnub.deviceToken"];
show all 23 linesThe list of registered channels should also be cached in a similar manner as the Device Token and should be updated whenever registered channels are added or removed. Set is used for the convenience of ensuring there are no duplicate channels.
- Swift
- Objective-C
let pushChannelDispatch = DispatchQueue(label: "com.pubnub.pushChannels", attributes: .concurrent)
var pushChannels: Set<String> {
get {
var channels: Set<String>?
pushChannelDispatch.sync {
channels = UserDefaults.standard.object(forKey: "com.pubnub.pushChannels") as? Set<String>
}
return channels ?? []
}
set {
pushChannelDispatch.async(flags: .barrier) {
UserDefaults.standard.set(newValue, forKey: "com.pubnub.pushChannels")
}
}
}
- (nullable NSSet<NSString*> *) pushChannels {
__block NSSet<NSString*>* channels;
dispatch_sync(pushChannelsDispatch, ^{
channels = [[NSUserDefaults standardUserDefaults] objectForKey: @"com.pubnub.pushChannels"];
});
return channels;
}
- (void) setPushChannels:(nullable NSSet<NSString*> *) newValue {
dispatch_barrier_async(pushChannelsDispatch, ^{
[[NSUserDefaults standardUserDefaults] setValue:newValue forKey:@"com.pubnub.pushChannels"];
});
}
Step 3b: Update Existing Registrations
A simple helper method can be created to consolidate the remove-then-add functionality when updating your existing registered channels.
- Swift
- Objective-C
func updateAPNSDevicesOnChannels(_ channels: [String], newDevice: Data, oldDevice: Data?,
on topic: String, environment: PubNub.PushEnvironment, completion: @escaping ((Result<[String], Error>) -> Void)
) {
if let oldDevice = oldDevice {
pubnub.removeAllAPNSPushDevice(for: oldDevice, on: topic, environment: environment) { result in
switch result {
case .success: print("Successfully removed device token")
case let .failure(error): print("Failed to remove device token due to: \(error)")
}
}
}
pubnub.addAPNSDevicesOnChannels(
channels, device: newDevice, on: topic, environment: environment, completion: completion
)
}
- (void) updateAPNSDevicesOnChannels:(NSArray<NSString *> *)channels
withDevicePushToken:(nonnull NSData *)newDevice
replacingDevicePushToken:(nullable NSData *)oldDevice
pushType:(PNPushType)pushType
environment:(PNAPNSEnvironment)environment
topic:(NSString *)topic
andCompletion:(PNPushNotificationsStateModificationCompletionBlock)completion {
if (oldDevice == nil) {
[self.client removeAllPushNotificationsFromDeviceWithPushToken:oldDevice
pushType:PNAPNS2Push
environment:PNAPNSProduction
topic:@"com.mycompany.mybundleid"
andCompletion:^(PNAcknowledgmentStatus *status) {
if (!status.isError) {
NSLog(@"Successfully removed device token");
show all 28 linesStep 4: Manage Device Registrations
Once a Device Token is obtained, it can be registered with a list of channels to allow Mobile Push Notifications to be sent to the device. Channels can be dynamically added and removed based on the use cases of the application, and the current registrations for a Device Token can also be viewed.
Step 4a: Register New Channels
When adding channels it's recommended to obtain the Device Token and List of Registered Channels from a cached source. After successfully registering channels, the newly registered channels should be added to the cached list.
- Swift
- Objective-C
if let deviceToken = (UIApplication.shared.delegate as? AppDelegate)?.cachedToken {
pubnub.addAPNSDevicesOnChannels(
["ch1", "ch2"],
device: deviceToken,
on: "com.mycompany.mybundleid",
environment: .production
) { result in
switch result {
case let .success(channelsAdded):
channelsAdded.forEach { (UIApplication.shared.delegate as? AppDelegate)?.pushChannels.update(with: $0) }
case let .failure(error): print("Failed to add Push due to: \(error.localizedDescription)")
}
}
}
NSData *deviceToken = [(AppDelegate *)[[UIApplication sharedApplication] delegate] cachedToken];
if (deviceToken == nil) { return; }
[self.client addPushNotificationsOnChannels:@[@"ch1",@"ch2"]
withDevicePushToken:deviceToken
pushType:PNAPNS2Push
environment:PNAPNSProduction
topic:@"com.mycompany.mybundleid"
andCompletion:^(PNAcknowledgmentStatus *status) {
if (!status.isError) {
NSSet* cachedChannels = [(AppDelegate *)[[UIApplication sharedApplication] delegate] pushChannels];
[(AppDelegate *)[[UIApplication sharedApplication] delegate] setPushChannels:[cachedChannels setByAddingObjectsFromArray:@[@"ch1",@"ch2"]]];
} else { NSLog(@"Failed to add Push due to: %@", status); }
}];
Step 4b: List Registered Channels
Once device registrations are added, you can confirm the APNs registrations for the device by listing all channels that the device is registered with. Since the list on the server is the source-of-truth, we will update our cached list to reflect the channels currently registered on the server.
- Swift
- Objective-C
guard let deviceToken = (UIApplication.shared.delegate as? AppDelegate)?.cachedToken else { return }
pubnub.listAPNSPushChannelRegistrations(
for: deviceToken,
on: "com.mycompany.mybundleid",
environment: .production
) { result in
switch result {
case let .success(channelsRegistered):
(UIApplication.shared.delegate as? AppDelegate)?.pushChannels = Set(channelsRegistered)
case let .failure(error): print("Failed to add Push due to: \(error.localizedDescription)")
}
}
NSData *deviceToken = [(AppDelegate *)[[UIApplication sharedApplication] delegate] cachedToken];
if (deviceToken == nil) { return; }
[self.client pushNotificationEnabledChannelsForDeviceWithPushToken:deviceToken
pushType:PNAPNS2Push
environment:PNAPNSDevelopment
topic:@"com.mycompany.mybundleid"
andCompletion:^(PNAPNSEnabledChannelsResult *result, PNErrorStatus *status) {
if (!status.isError) {
NSSet* registeredChannels = [NSSet setWithArray:result.data.channels];
[(AppDelegate *)[[UIApplication sharedApplication] delegate] setPushChannels:registeredChannels];
} else {
NSLog(@"Failed to add Push due to: %@", status);
}
}];
Step 4c: Remove Existing Registrations
When removing channels, it's recommended to obtain the Device Token and List of Registered Channels from a cached source. After removing registering channels, the channels that were removed should be also removed from the cached source.
- Swift
- Objective-C
if let deviceToken = (UIApplication.shared.delegate as? AppDelegate)?.cachedToken {
pubnub.removeAPNSDevicesOnChannels(
["ch1", "ch2"],
device: deviceToken,
on: "com.mycompany.mybundleid",
environment: .production
) { result in
switch result {
case let .success(channelsRemoved):
channelsRemoved.forEach { (UIApplication.shared.delegate as? AppDelegate)?.pushChannels.remove($0) }
case let .failure(error): print("Failed to add Push due to: \(error.localizedDescription)")
}
}
}
NSData *deviceToken = [(AppDelegate *)[[UIApplication sharedApplication] delegate] cachedToken];
if (deviceToken == nil) { return; }
[self.client removePushNotificationsFromChannels:@[@"ch1",@"ch2"]
withDevicePushToken:deviceToken
pushType:PNAPNS2Push
environment:PNAPNSProduction
topic:@"com.mycompany.mybundleid"
andCompletion:^(PNAcknowledgmentStatus *status) {
if (!status.isError) {
NSMutableSet* cachedChannels = [[(AppDelegate *)[[UIApplication sharedApplication] delegate] pushChannels] mutableCopy];
for (NSString *channel in @[@"ch1",@"ch2"]) { [cachedChannels removeObject:channel]; }
[(AppDelegate *)[[UIApplication sharedApplication] delegate] setPushChannels:cachedChannels];
} else { NSLog(@"Failed to remove devices due to: %@", status); }
}];
Step 5: Construct the Push Payload
On iOS, APNs is the supported push provider that PubNub uses to relay Mobile Push Notifications to the application. To send a push notification, include the appropriate push notification payload for APNs when you publish a message and PubNub will appropriately parse the message.
The pn_apns
payload is required for all APNs notifications. It tells PubNub that the payload should be forwarded to Apple.
pn_apns
consists of:
{
"pn_apns": {
"aps": {
// necessary items
},
"pn_push": {
// necessary items
},
"pn_debug": true,
"pn_ttl": 60
}
}
aps
The aps
payload is entirely managed by Apple. It's a dictionary that contains the keys used by Apple to deliver the notification to the user's device. The keys specify the type of interactions that you want the system to use when alerting the user. Depending on your use case, providing content for this payload may be required. For example, if you decide to instruct the system to handle the notification in the background, you must add the content-available
key, set it to 1
, and not include any visual or audio keys. Refer to the Payload Key Reference section in the Apple Developer documentation for this and other examples.
pn_push
The pn_push
object contains the configuration of the push message. PubNub uses this payload to set such values as request headers in APNs notifications sent to Apple. This payload is required for all APNs notifications.
Key | Type | Required | Default | Description |
---|---|---|---|---|
push_type | String | No | alert | A type of notification sent to Apple. Available values: alert , background , voip , complication , fileprovider , or mdm . The value you set for this key should always align with the payload values you set inside your aps payload. Refer to the apns_push_type header field in the Apple Developer documentation for more information. |
auth_method | String | No | token | Available values: token , cert , or certificate |
targets:environment | String | No | development | An Apple environment for the certificate associated with the PubNub key-set in use. Available values: development or production . |
targets:topic | String | Yes | A Bundle ID of your target application | |
targets:excluded_devices | [String] | No | A list of device tokens that should be excluded from receiving the notification. Typically, you can add your own device so you don't get a notification you publish. | |
version | String | Yes | Set to v2 . | |
pn_collapse_id | String | No | An identifier to join multiple notifications into a single notification. |
Additional Parameters
Key | Type | Required | Description |
---|---|---|---|
pn_debug | boolean | No | A flag that enables publishing push debugging info to the pndebug channel. For more information, refer to Mobile Push Troubleshooting. |
pn_ttl | int | No | Time in seconds after which the notification expires. |
In the code sample below, the message includes a pn_apns
payload to trigger APNs notifications on iOS devices:
- JSON
- Swift
- Objective-C
For more information on generating notification, go to Apple Docs.
{
"text": "John invited you to chat",
"pn_apns": {
"aps": {
"alert": {
"title": "Chat Invitation",
"body": "John invited you to chat"
}
},
"pn_push":[
{
"push_type": "alert",
"auth_method": "token",
"targets":[
{
show all 25 lineslet message = ["text": "John invited you to chat"]
let pushPayload = PubNubPushMessage(
apns: PubNubAPNSPayload(
aps: APSPayload(alert: .object(.init(title: "Chat Invitation", body: "John invited you to chat"))),
pubnub: [.init(targets: [.init(topic: "com.mycompany.mybundleid", environment: .production)])],
),
additional: message
)
NSDictionary *message = @{
@"text": @"John invited you to chat"
};
PNNotificationsPayload *pushData = [PNNotificationsPayload
payloadsWithNotificationTitle:@"Chat Invitation"
body:@"John invited you to chat"];
// create the APNs target with topic and environment
PNAPNSNotificationTarget *target = [PNAPNSNotificationTarget
targetForTopic:@"com.mycompany.mybundleid"
inEnvironment:PNAPNSProduction
withExcludedDevices:nil];
// create the APNs config object and add the target
show all 23 linesStep 6: Publish the Push Notification
Once the push payload is ready, use the publish
method to publish the message on a channel. When PubNub finds the pn_apns
and/or pn_fcm
payloads, it will retrieve all device tokens that are associated with the push payload on the target channel and forward a push notification request to the appropriate push service for those associated devices.
- Swift
- Objective-C
- JavaScript
pubnub.publish(channel: "ch1", message: pushPayload) { result in
switch result {
case let .success(timetoken):
print("Successfully published message at: \(timetoken)")
case let .failure(error):
print("Failed to publish due to: \(error)")
}
}
[self.pubnub publish:message toChannel:@"ch1" mobilePushPayload:pushPayload
withCompletion:^(PNPublishStatus *status) {
if (!status.isError) {
NSLog(@"Successfully published message");
} else {
NSLog(@"Failed to publish due to: %@", status);
}
}];
//publish on channel
pubnub.publish(
{
channel: "ch1"
message: pushPayload
},
function (status, response) {
console.log(status);
console.log(response);
}
);
For more information about push troubleshooting, refer to Mobile Push Troubleshooting.