Skip to content

Setting Up Push For iOS

Chidiebere Okwudire edited this page Jan 24, 2017 · 15 revisions

This page describes how to set up push notifications for iOS in a CodenameOne app using push services provided by Parse.

Note: This solution is not compatible with push notifications provided by CodenameOne. Make sure that the includePush build hint is set to false (ios.includePush=false)!

Setting up Push for iOS

Setting up Push notifications via Parse for iOS is not as straightforward as for Android. However, if you carefully follow the steps outlined here, you'd be up and running in no time. This is achieved using cn1 build hints. However, first things first: Your iOS certificates need to be in place.

Backend side

Generating iOS 'resources' (certificates, provisioning profiles, app ID, etc.)

You'll need the following iOS 'resources':

  1. An app ID (Note that wildcard app ids are not allowed for push notifications!)
  2. Apple Push Notification SSL Certificates (.p12 files for development and release builds, respectively)
  3. iOS Development and Distributions certificates (also .p12 files for debug and release but different from the push notification SSL certificates above)
  4. iOS Provisioning Profiles (.mobileprovision files; one for debug and another for release)

Generating iOS certificates manually using Parse's tutorial

Parse also provides a step-by-step tutorial that you can follows here to generate all the required resources manually. These are covered in steps 1 and 2 of the tutorial.

Some things to bear in mind if you follow the above tutorial:

  • Section 1 of the tutorial shows you how to create an App ID (Resource 1) and the Push Notification SSL Certificates (Resource 2). In Section 2, it implicitly assumes that you already have regular iOS Development/Distribution certificates (e.g. see Step 2.6 where reference is made to 'your iOS Development certificate' whose creation was never described!). So you'll need to generate the development and distribution certificates (Resource 3) if you don't already have them.
    • The easiest way is to use the CN1 iOS wizard described below.
    • However, if you still want to generate your ceritificates manually (it's not that hard though you'd need a Mac), bear in mind that CN1 expects you to select both the public and private keys when exporting your .p12 files. See item 2 under section 'iOS Code Signing Fail Checklist' on the CN1 signing page for more details.

Generation (some of) the iOS certificates automatically using CN1's iOS wizard

Of course, there's the CodenameOne iOS wizard that enables automated creation of some of these resources. At the time of writing (January 2017), it is possible to use to use the CN1 iOS wizard to generate Resources 1, 3 and 4 even for free CN1 users. However, you'll need to generate your Push SSL certificates (i.e. Resource 2) yourself if you don't already have them or a CN1 subscription that allows the wizard to generate them for you. For this, you need a Mac and you can follow the Parse iOS tutorial referenced above.

Other options

  • There are numerous resources online for setting up an app for iOS push notifications; just google if you need help.
  • You can also find another interesting tutorial on iOS push in general and set up steps here. Note that this is not Parse-specific! So at least the "A Very Basic App" section is not relevant to you.

Troubleshooting

Here are some useful links for problems that you might face when manually creating your own certificates:

Uploading the Apple Push Notification SSL Certificates to Parse

Follow the instructions in Section 3. 'Configuring the Parse App' of the Parse iOS tutorial to upload your push notification SSL certificate (Resource 2) from the previous section.

  • If using a self-hosted Parse Server, follow the configuration steps here.
  • If using a Parse Server hosting service like back4app or sashido.io, follow the corresponding documentation on how to upload your Push SSL certificate to their backend.

You're now set for the app-side work. Hang on, we're almost there...

App side

Adding the Parse iOS SDK to your app

Thanks to cocoapod support in CodenameOne, updating the Parse iOS SDK can easily be done in the application by adding the following build hints:

ios.pods=Parse ~> x.y.z where x.y.z is the target Parse iOS SDK version, e.g. 1.12.0

ios.pods.platform=7.0

Push notification on iOS was successfully tested using Parse iOS SDK 1.12.0 but should in principle work with newer versions. Note though that updating to the latest SDK shouldn't be needed as parse4cn1 only uses the SDK for installation and push notification-related functionality which is not likely to change quite often. As such, it's advisable to at least begin with this tested and trusted version to limit the risks of introducing additional problems in an already complex process.

Including push entitlements

Certain push-related information contained in the provisioning profile (Resource 4) needs to be added to the Entitlements of your app during compilation. Otherwise, push will not work (on some versions of iOS, the app might not even install).

To accomplish this, add the build hint ios.includePushEnt with value true. This build hint should not be confused with ios.includePush which enables push services from CN1 and must be false since Parse push and CN1 push services are incompatible. All ios.includePushEnt will do is to add the necessary entitlements to your app. See this thread from the CN1 discussion forum for more information.

Initializing the Parse SDK

The Parse SDK must be initialized. This needs to be done in the AppDelegate.m file via build hint injections as described next.

Including the required headers

Add build hint ios.glAppDelegateHeader with value #import "Parse.h" \n #import "com_parse4cn1_nativeinterface_Utils.h" to your app.

Note: The second header is a utility class that is included in parse4cn1 and contains various useful methods. See the source code (.h, .m) and the usage in the subsequent sections. If you don't need it, skip it.

Initializing and registering for push notifications

Add build hint ios.afterFinishLaunching with the following value (after replacing "yourApplicationId", "yourClientKey" and "yourServerUrl" with the right values) and remove the comment lines added to help you understand what's happening.

[Parse initializeWithConfiguration:[ParseClientConfiguration configurationWithBlock:^(id<ParseMutableClientConfiguration> configuration) {
   configuration.applicationId = @"yourApplicationId;
   configuration.clientKey = @"yourClientKey";
   configuration.server = @"yourServerUrl";
}]];

UIUserNotificationType userNotificationTypes = (UIUserNotificationTypeAlert |
                                              UIUserNotificationTypeBadge |
                                              UIUserNotificationTypeSound);

UIUserNotificationSettings *settings = [UIUserNotificationSettings settingsForTypes:userNotificationTypes categories:nil];

[application registerUserNotificationSettings:settings];
[application registerForRemoteNotifications];

// Track app open as described Parse iOS guide
// https://parse.com/docs/ios/guide#push-notifications-tracking-pushes-and-app-opens
if (application.applicationState != UIApplicationStateBackground) {
  // Track an app open here if we launch with a push, unless
  // "content_available" was used to trigger a background push (introduced
  // in iOS 7). In that case, we skip tracking here to avoid double
  // counting the app-open.
  BOOL preBackgroundPush = ![application  respondsToSelector:@selector(backgroundRefreshStatus)];
  BOOL oldPushHandlerOnly = ![self respondsToSelector:@selector(application:didReceiveRemoteNotification:fetchCompletionHandler:)];
  BOOL noPushPayload = ![launchOptions objectForKey:UIApplicationLaunchOptionsRemoteNotificationKey];
  if (preBackgroundPush || oldPushHandlerOnly || noPushPayload) {
    [PFAnalytics trackAppOpenedWithLaunchOptions:launchOptions];
  }

  // Check if app is opened via push and respond accordingly
  NSString *msg;
  NSDictionary *notificationPayload = launchOptions[UIApplicationLaunchOptionsRemoteNotificationKey];
  if (notificationPayload && preBackgroundPush && oldPushHandlerOnly) {
    // Not completely sure if the above checks are completely correct. The intention is that:
    // In the iOS 7+ situations, we don't handle app open via push here because it will be handled in 
    // application:didReceiveRemoteNotification:fetchCompletionHandler.
    msg = @"didFinishLaunchingWithOptions(): App opened via remote notification. Payload=";
    NSString *json = [com_parse4cn1_nativeinterface_Utils dictToJson:notificationPayload];
    msg = [msg stringByAppendingString:json];
    [com_parse4cn1_nativeinterface_Utils handleAppOpenedViaPush:notificationPayload];
} else {
  msg = @"didFinishLaunchingWithOptions(): App opened normally";
}

[com_parse4cn1_nativeinterface_Utils logDebugPlusStateInfo:msg]; // Debug-only. Can be removed.

}

Basically, the code above does the following:

  1. Initialize the Parse SDK
  2. Register for Push
  3. Track app opens and also notify your CN1 app via parse4cn1's ParsePush class's callback interface if the app is opened by clicking a push notification. The latter is delegated to the helper utility class that takes care of things like converting the NSDictionary to a (flattened) JSON string and making sure that callbacks to your CN1 app take place when the app is in a right lifecycle state. Have a look at the utility class and the comments there for more details.

Here's a comment-free, line break-free version of the code above that you can directly add to the build hint (of course, after changing "yourApplicationId", "yourClientKey" and "yourServerUrl" to meaningful values):

[Parse initializeWithConfiguration:[ParseClientConfiguration configurationWithBlock:^(id<ParseMutableClientConfiguration> configuration) { configuration.applicationId = @"yourApplicationId; configuration.clientKey = @"yourClientKey"; configuration.server = @"yourServerUrl"; }]]; UIUserNotificationType userNotificationTypes = (UIUserNotificationTypeAlert | UIUserNotificationTypeBadge | UIUserNotificationTypeSound); UIUserNotificationSettings *settings = [UIUserNotificationSettings settingsForTypes:userNotificationTypes categories:nil]; [application registerUserNotificationSettings:settings]; [application registerForRemoteNotifications]; if (application.applicationState != UIApplicationStateBackground) { BOOL preBackgroundPush = ![application respondsToSelector:@selector(backgroundRefreshStatus)]; BOOL oldPushHandlerOnly = ![self respondsToSelector:@selector(application:didReceiveRemoteNotification:fetchCompletionHandler:)]; BOOL noPushPayload = ![launchOptions objectForKey:UIApplicationLaunchOptionsRemoteNotificationKey]; if (preBackgroundPush || oldPushHandlerOnly || noPushPayload) { [PFAnalytics trackAppOpenedWithLaunchOptions:launchOptions]; } NSString *msg; NSDictionary *notificationPayload = launchOptions[UIApplicationLaunchOptionsRemoteNotificationKey]; if (notificationPayload && preBackgroundPush && oldPushHandlerOnly) { msg = @"didFinishLaunchingWithOptions(): App opened via remote notification. Payload="; NSString *json = [com_parse4cn1_nativeinterface_Utils dictToJson:notificationPayload]; msg = [msg stringByAppendingString:json]; [com_parse4cn1_nativeinterface_Utils handleAppOpenedViaPush:notificationPayload]; } else{ msg = @"didFinishLaunchingWithOptions(): App opened normally"; } [com_parse4cn1_nativeinterface_Utils logDebugPlusStateInfo:msg]; }

Handling push notifications

Handling incoming push messages requires implementing a few iOS app lifecycle methods. These need to be added to your AppDelegate body via build hint ios.glAppDelegateBody. For clarity, each of the methods is explained separately after which a 'squashed' line break and comment-free version of the entire code is provided.

Responding to push notification registration

Upon successful registration for remote notifications (already done above), the following callback is triggered.

- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken { 
  PFInstallation *currentInstallation = [PFInstallation currentInstallation];  
  currentInstallation.deviceToken = @"";
  [currentInstallation setDeviceTokenFromData:deviceToken]; 

  [currentInstallation saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {
    if (succeeded) {
      [com_parse4cn1_nativeinterface_Utils logDebugPlusStateInfo:@"didRegisterForRemoteNotificationsWithDeviceToken(): Installation saved in background"];
      [com_parse4cn1_nativeinterface_Utils notifyPushRegistrationSuccess];
    }

    if (error) {
      [com_parse4cn1_nativeinterface_Utils notifySaveInstallationFailure:error.localizedDescription];
    }
  }];
} 

The above implementation creates the ParseInstallation object. If installation creation fails, the app is notified of the installation update failure. Conversely, successful registration is also reported as push registration success.

If registration for push notification fails, the following callback is triggered. Again, this implementation notifies the app of the failure.

- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error {   
  [com_parse4cn1_nativeinterface_Utils handlePushRegistrationError:error.localizedDescription];  
} 

Handling incoming push messages

Now the fun begins! When a push notification is received, one of the following callbacks is triggered.

According to the official documentation, if both methods are present the application:didReceiveRemoteNotification:fetchCompletionHandler will be called. This version is recommeded especially if you target iOS 7+ because "[u]nlike the application:didReceiveRemoteNotification: method, which is called only when your app is running in the foreground, the system calls this method when your app is running in the foreground or background". In addition, if you enabled the remote notifications background mode [as explained here], "the system launches your app (or wakes it from the suspended state) and puts it in the background state when a remote notification arrives", according to the iOS official documentation. If you need to target devices running earlier versions that iOS 7, your only option is the application:didReceiveRemoteNotification: method. If you're not sure, just include both :)

- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo {
    // Track app open as described Parse iOS guide
    if (application.applicationState == UIApplicationStateInactive) {
      // The application was just brought from the background to the foreground,
      // so we consider the app as having been "opened by a push notification."
      [PFAnalytics trackAppOpenedWithRemoteNotificationPayload:userInfo];
    }
    
    // Handle incoming push notification
    NSString *msg = @"didReceiveRemoteNotification(): userInfo=";
    NSString *json = [com_parse4cn1_nativeinterface_Utils dictToJson:userInfo];
    msg = [msg stringByAppendingString:json];
    [com_parse4cn1_nativeinterface_Utils logDebugPlusStateInfo:msg];
    
    if (![com_parse4cn1_nativeinterface_Utils handlePushReceived:userInfo]) {
      [PFPush handlePush:userInfo]; // Safe as a popup will only be shown if app is in the foreground.
    }
}

Both methods delegate handling of incoming push messages to the utility class that in turn triggers the right callback in your CN1 app depending on the current application state. If you choose not to use the utility class for any reason, feel free to implement your own handling.

- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler {
  // Track app open as described Parse iOS guide
  if (application.applicationState == UIApplicationStateInactive) {
    [PFAnalytics trackAppOpenedWithRemoteNotificationPayload:userInfo];
  }

  // Handle incoming push notification
  NSString *msg = @"didReceiveRemoteNotification_fetchCompletionHandler(): userInfo=";
  NSString *json = [com_parse4cn1_nativeinterface_Utils dictToJson:userInfo];
  msg = [msg stringByAppendingString:json];
  [com_parse4cn1_nativeinterface_Utils logDebugPlusStateInfo:msg];
   
  if (![com_parse4cn1_nativeinterface_Utils handlePushReceived:userInfo]) {
    [PFPush handlePush:userInfo]; // Safe as a popup will only be shown if app is in the foreground.
  }
   
   // By default, we assume no data is fetched. Depending on the use case, this should be replaced with a more elaborate implementation
   completionHandler(UIBackgroundFetchResultNoData);
}

application:didReceiveRemoteNotification:fetchCompletionHandler enables background retrieval. An illustration of a possible use case is provided in the Parse push notification documentation for iOS.

Putting everything together, you get the following blob which needs to be added to build hint ios.glAppDelegateBody

- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {   PFInstallation *currentInstallation = [PFInstallation currentInstallation];    currentInstallation.deviceToken = @"";   [currentInstallation setDeviceTokenFromData:deviceToken];   [currentInstallation saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {     if (succeeded) {       [com_parse4cn1_nativeinterface_Utils logDebugPlusStateInfo:@"didRegisterForRemoteNotificationsWithDeviceToken(): Installation saved in background"];     }      if (error) {         UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Installation not saved" message:error.localizedDescription delegate:nil cancelButtonTitle:@"Ok" otherButtonTitles:nil, nil];         [alert show];         [alert autorelease];     }   }]; } - (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error {     UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Push Registration Error" message:error.localizedDescription delegate:nil cancelButtonTitle:@"Ok" otherButtonTitles:nil, nil];   [alert show];   [alert autorelease];    } - (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo {     if (application.applicationState == UIApplicationStateInactive) {       [PFAnalytics trackAppOpenedWithRemoteNotificationPayload:userInfo];     }         NSString *msg = @"didReceiveRemoteNotification(): userInfo=";     NSString *json = [com_parse4cn1_nativeinterface_Utils dictToJson:userInfo];     msg = [msg stringByAppendingString:json];     [com_parse4cn1_nativeinterface_Utils logDebugPlusStateInfo:msg];         if (![com_parse4cn1_nativeinterface_Utils handlePushReceived:userInfo]) {       [PFPush handlePush:userInfo];     } }  - (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler {   if (application.applicationState == UIApplicationStateInactive) {     [PFAnalytics trackAppOpenedWithRemoteNotificationPayload:userInfo];   }    NSString *msg = @"didReceiveRemoteNotification_fetchCompletionHandler(): userInfo=";   NSString *json = [com_parse4cn1_nativeinterface_Utils dictToJson:userInfo];   msg = [msg stringByAppendingString:json];   [com_parse4cn1_nativeinterface_Utils logDebugPlusStateInfo:msg];      if (![com_parse4cn1_nativeinterface_Utils handlePushReceived:userInfo]) {     [PFPush handlePush:userInfo];   }     completionHandler(UIBackgroundFetchResultNoData); }

Tip: If you use Notepad++, stripping the new lines is pretty easy via menu option Edit >> Blank Operations >> EOL to space

(Optional) Enabling remote notifications background mode

You might be interested in enabling remote notification background mode which effectively means that the OS would be able to wake up your app for it to respond to incoming so-called 'silent' or 'hidden' push messages.

As explained here, this option can be enabled by adding build hint ios.plistInject with value:

<key>UIBackgroundModes</key>
<array>
    <string>remote-notification</string>
</array>

Silent push messages are so named because they are usually oblivious to the user of your app, i.e., no badge, alert or 'status bar' notification is created for such messages. You push payload needs to include the content-available key with value 1 (see the official iOS documentation for more details or simply google for "iOS silent notifications").

Implementing app logic for processing incoming push messages

If all you need is to be able to receive push messages and for your app to be opened when they are clicked, you can safely continue to the next step. However, if you have more advanced needs like processing incoming push data, switching forms based on contents of push messages, handling silent push messages, etc., you should definitely take a moment to review this section of the reference implementation that describes how you can realize such customizations.

Compiling and running

You're done! Compile your app (using your own certificates, of course), deploy to a (real) device, send a push notification via the Parse dashboard push console or using parse4cn1 and you should receive it on your device. If not:

  • Review the steps above for possible mistakes
  • Compare your implementation with that in the parse4cn1 test app (maybe also try compiling and deploying that app which features sending and receiving push notifications).
  • Debug using xcode
  • Check out the troubleshooting and links sections below for more tips

Troubleshooting

1. App installation fails with "no valid 'aps-environment' entitlement string found for application" error

You most likely forgot to enable push entitlements as explained here.

2. Unable to receive push messages

If you are unable to receive push messages, there are a few troubleshooting steps you can take:

  1. Did you enable push entitlements as explained here?
  2. Is your installation being created? Check this on the Parse dashboard.
  3. If the installation is created, confirm that the installation object has non-empty deviceToken and installationId fields and that deviceType=ios. Moreover, fields appIdentifier (=packageName) and appName should be filled with expected values from your CN1 app's settings. If any of the above fields is missing, it is most likely the case that you have a typo in one of your build hints or you skipped some steps. Double- and triple-check your work!
  4. If these don't help, try googling or asking on the Parse discussion forum or if you think it's a parse4cn1 issue, create a ticket in Github.

Useful links

Clone this wiki locally