Try on Bluemix and migrate to on-prem MobileFirst Platform

Contributed By : Chethan Kumar SN (chethankumar.sn@in.ibm.com) and Vittal Pai (vittalpai@in.ibm.com) With the release of MobileFirst Platform v7.1, one can now migrate any existing iOS app built for MobileServices on Bluemix to MobileFirst Platform with just a handful of simple steps. To elucidate the process, lets look at how to migrate a simple Bluemix iOS app.

Existing Bluemix Server Application

The Bluemix app has the following functionality:
  • On the client side, the application stores a list of items and provides a way to add more items to the list. Each item can able to store Name, Store, Price and image of the product. The App's are protected by Custom Authenticator via AMA security service provided by bluemix.
  • On the server side, the App contains a JAX-RS class to store and manipulate the data. It also contains the server side AMA security implementation.
On BlueMix we have application with the following configuration:
  • Liberty Runtime : which used to run JAX-RS application on Bluemix
  • Advance Mobile Access service : which gives mobile application security and monitoring functionality
  • Push Service for iOS 8 : which provides the capability to use iOS Push features

Liberty Runtime

  • Liberty contains two projects with JAX-RS service (i.e Custom-oauth-java for Custom Authentication and LocalstoreAdapter for storing items). The service include the protected resource and the custom identity provider code. The liberty server is configured with TAI.
  • Trust Association Interface (TAI) is a service provider API that enables the integration of third-party security services with a Liberty profile server. For more info on TAI : click here
  • The custom identity provider authenticates a user by sending challenges to the client. However, custom identity providers do not communicate directly with clients. They send challenges and receive responses to the challenges by means of the Advanced Mobile Access service. When a custom identity provider successfully authenticates the user, it provides the user identity information to Advanced Mobile Access. For more information on custom authentication refer bluemix documentation : click here The custom identity provider code is defined by two http API: /startAutorization and /handleChallengeAnswer
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    
            @POST
        	@Consumes ("application/json")
        	@Path("/{tenantId}/customAuthRealm_3/startAuthorization")
        	@Produces(MediaType.APPLICATION_JSON)
        	public JSONObject startAuthorization(String payload,
        			@PathParam("tenantId") String deviceId,
        			@PathParam("realmName") String realmName) throws Exception {
        		JSONObject returnJson = (JSONObject) JSON.parse(CHALLENGE_JSON);
        		return returnJson;
        	}
        	@POST
        	@Consumes ("application/json")
        	@Path("/{tenantId}/customAuthRealm_3/handleChallengeAnswer")
        	@Produces(MediaType.APPLICATION_JSON)
        	public JSONObject handleChllengeAnswer(String payload,
        			@PathParam("tenantId") String deviceId,
        			@PathParam("realmName") String realmName) throws Exception {
        		JSONObject userStoreJson = (JSONObject) JSON.parse(USER_STORE_JSON);
        		JSONObject failedResponseJson = (JSONObject) JSON.parse(FAILURE_JSON);
        		if(payload == null || payload.isEmpty()) {
        			return failedResponseJson;
        		}
        		JSONObject payloadJson = (JSONObject) JSON.parse(payload);
        		JSONObject challengeAnswer = (JSONObject) payloadJson.get("challengeAnswer");
        		if (challengeAnswer == null ) {
        			return failedResponseJson;
        		}
        		String userName = (String) challengeAnswer.get("userName");
        		String password = (String) challengeAnswer.get("password");
        		if (userName == null || userName.isEmpty() || password == null || password.isEmpty()) {
        			return failedResponseJson;
        		}
        		if (userStoreJson.containsKey(userName)) {
        			JSONObject userInfoJson = (JSONObject) userStoreJson.get(userName);
        			String userPassword = (String) userInfoJson.get("password");
        			String userDisplayName = (String) userInfoJson.get("displayName");
        			if (password.equals(userPassword)) {
        				JSONObject returnJson = new JSONObject();
        				JSONObject userIdentityJson = new JSONObject();
        				userIdentityJson.put("userName", userName);
        				userIdentityJson.put("displayName", userDisplayName);
        				returnJson.put("status", "success");
        				returnJson.put("userIdentity", userIdentityJson);
        				return returnJson;
        			}
        		}
        		return failedResponseJson;
        	}
    
            
    The Localstore adapter contains few http API's to perform some basic operations like Add, Update, Create and Delete in client application.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    
    @GET
    	@Path("/getAllItems")
    	public String getAllItems() throws IOException{
    		init();
    		JsonArray jsonArray = new JsonArray();
    		for(Object key : props.keySet()){
    			jsonArray.add(parser.parse(props.getProperty((String) key)).getAsJsonObject());
    		}
    		return jsonArray.toString();
    	}
    	@PUT
    	@Path("/addItem")
    	public void addItem(String itemJson)
    			throws IOException, URISyntaxException{
    		try{
    			init();
    			int newKey = props.keySet().size()+1;
    			props.put(String.valueOf(newKey), itemJson);
    			URL url = this.getClass().getClassLoader().getResource("data.properties");
    			File file = new File(url.toURI().getPath());
    			FileOutputStream foStream = new FileOutputStream(file);
    			props.store(foStream, "saving new item");
    			foStream.close();
    		}catch(IOException ioe){
    			ioe.printStackTrace();
    		}
    	}
    	@POST
    	@Path("/addAllItems")
    	public String addAllItems(String itemsJson)
    			throws  URISyntaxException, IOException{
    		try{
    			init();
    			clearAllData();
    			JsonArray jsonArr = parser.parse(itemsJson).getAsJsonArray();
    			for(int i=0;i<jsonArr.size();i++){
    				props.put(String.valueOf(i+1), jsonArr.get(i).toString());
    			}
    			URL url = this.getClass().getClassLoader().getResource(&"data.properties");
    			File file = new File(url.toURI().getPath());
    			FileOutputStream foStream = new FileOutputStream(file);
    			props.store(foStream, "saving new item");
    			foStream.close();
    			return "true";
    		}catch(IOException ioe){
    			ioe.printStackTrace();
    		}
    		return "false";
    	}
    	@DELETE
    	@Path("/clearAll")
    	public String clearAllData()
    			throws MissingConfigurationOptionException, URISyntaxException, IOException{
    			init();
    			props.clear();
    			System.out.println("Size : "+props.size());
    			URL url = this.getClass().getClassLoader().getResource("data.properties");
    			File file = new File(url.toURI().getPath());
    			FileOutputStream foStream = new FileOutputStream(file);
    			props.store(foStream, "clearing all data");
    			foStream.close();
    			return "cleared";
    	}
  • Add TAI Extension in the following path of server directory server/usr/extensions TAI Extension Link : Download the extension.zip from here
  • Add TAI Security constraint in web.xml file for both the projects.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    
    <security-constraint>
       	<web-resource-collection>
          	   <web-resource-name>LocalstoreApplication</web-resource-name>
          	   <url-pattern>/apps/*</url-pattern>
       	</web-resource-collection>
          	<auth-constraint>
               <role-name>TAIUserRole</role-name>
          	</auth-constraint>
    </security-constraint>
    <security-role id="SecurityRole_TAIUserRole" >
           <role-name>TAIUserRole</role-name>
    </security-role>
  • Add OAuthTai feature in server.xml
    1
    
    <feature>usr:OAuthTai-1.0</feature>
  • Protect the Url's using TAI by adding following code in server.xml
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    
    <usr_OAuthTAI id="myOAuthTAI" realmName="imfRealm">
    		<securityConstraint httpMethods="GET, POST" securedURLs="/LocalstoreAdapter/*"/>
    		<securityConstraint httpMethods="GET, POST" securedURLs="/custom-oauth-java/*"/>
    	</usr_OAuthTAI>
        <webApplication id="custom-oauth-java" location="custom-oauth-java.war" name="custom-oauth-java">
         <application-bnd>
    		<security-role name="TAIUserRole">
    			<special-subject type="ALL_AUTHENTICATED_USERS"/>
    		</security-role>
    	</application-bnd>
    	</webApplication>
    	  <webApplication id="LocalstoreAdapter" location="LocalstoreAdapter.war" name="LocalstoreAdapter">
            <application-bnd>
    		<security-role name="TAIUserRole">
    			<special-subject type="ALL_AUTHENTICATED_USERS"/>
    		</security-role>
    	</application-bnd>
    	</webApplication>
        
  • Specify the IMF Auth Url inside Server.env file in liberty.
    1
    
    imfServiceUrl=https://imf-authserver.ng.bluemix.net/imf-authserver
  • Create a server package which contains above two applications using following command.
    ./server package ${server_name} --include=usr
  • Push the newly created server package to bluemix using following command.
    cf push ${app_name} -p ${path_to_server_package_zip}

Advance Mobile Access service

  • Bind the pushed application to Advance Mobile Access Service. missing_alt
  • Register your client application in AMA dashboard. For more info refer documentation : click here missing_alt
  • AMA provides Facebook, Google, or a custom identity provider to authenticate access to protected resources. Add Custom identity provider feature as it can be migrated to MFPF and specify the corresponding jax-rs custom authentication application url and realm name. missing_alt
  • Add the following code inside didFinishLaunchingWithOptions function in Appdelegate of client application which will register the realm and initialize connection with Bluemix Application.
    1
    2
    
    IMFClient.sharedInstance().registerAuthenticationDelegate(customAuthDelegate, forRealm: "customAuthRealm_3")
    IMFClient.sharedInstance().initializeWithBackendRoute("https://parkstore.mybluemix.net", backendGUID: "5e3ad88d-dd48-469d-b46f-2c4ad66b5345")
  • The following is the sample code to invoke the Rest url's in client application.
    1
    2
    3
    
            var request: IMFResourceRequest = IMFResourceRequest(path: "https://parkstore.mybluemix.net/LocalstoreAdapter/apps/5e3ad88d-dd48-469d-b46f-2c4ad66b5345/localstore/getAllItems", method: "GET")
            request.sendWithCompletionHandler { (wlResponse:IMFResponse!, err:NSError!) -> Void in
        

Push Service for iOS 8

  • Bind the application with Push Service for iOS 8 missing_alt
  • Configure Apple Push Notification service (APNs) which requires Apple Developer Account and Generate pl2 certificates. Documentation link : click here
  • Upload the generated pl2 certificate in Push service dashboard missing_alt
  • Add the following code inside didFinishLaunchingWithOptions function in Appdelegate of client application which will register notifications in client app.
    1
    2
    3
    4
    5
    
            let notificationTypes: UIUserNotificationType = UIUserNotificationType.Badge | UIUserNotificationType.Alert | UIUserNotificationType.Sound
            let notificationSettings: UIUserNotificationSettings = UIUserNotificationSettings(forTypes: notificationTypes, categories: nil)
            application.registerUserNotificationSettings(notificationSettings)
            application.registerForRemoteNotifications()
        
  • Add the following code inside didRegisterForRemoteNotificationsWithDeviceToken function in Appdelegate of client application which will register pushclient and subscribe to tag in client app.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    
    IMFPushClient.sharedInstance().registerDeviceToken(deviceToken, completionHandler: { (response, error) -> Void in
                if error != nil {
                    println("Error during device registration \(error.description)")
                }
                else {
                    println("Response during device registration json: \(response.responseJson.description)")
                    var tags = ["parkstore"]
                    IMFPushClient.sharedInstance().subscribeToTags(tags, completionHandler: { (response:IMFResponse!, err:NSError!) -> Void in
                        if err != nil {
                            println("There was an error while subscribing to tag")
                        }else{
                            println("Successfully subscribe to tag parkstore")
                        }
                    })
                }
            
  • Add the following function inside Appdelegate which triggers when push notification arrived in client app.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    
            func application(application: UIApplication, didReceiveRemoteNotification userInfo: [NSObject : AnyObject]) {
                println("Got remote Notification. Data : \(userInfo.description)")
                let info = userInfo as NSDictionary
                let data = info.objectForKey("aps")?.objectForKey("alert") as! NSDictionary
                let userData = data.objectForKey("body") as! String
                let alertView = UIAlertView(title: "WishList!", message: "\(userData)", delegate: nil, cancelButtonTitle: "OK")
                alertView.show()
               }
            }
            

Existing Bluemix Client Application

Add the following Code snippets to the existing Bluemix Client Application and name the application with same name which you have registered in Advance Mobile Access Dashboard.

  • Add the following code inside didFinishLaunchingWithOptions function in Appdelegate of client application which will register the realm and initialize connection with Bluemix Application.
    1
    2
    
            IMFClient.sharedInstance().registerAuthenticationDelegate(customAuthDelegate, forRealm: "customAuthRealm_3")
    IMFClient.sharedInstance().initializeWithBackendRoute("https://parkstore.mybluemix.net", backendGUID: "5e3ad88d-dd48-469d-b46f-2c4ad66b5345")
  • The following is the sample code to invoke the Rest url's in client application.
    1
    2
    3
    
    var request: IMFResourceRequest = IMFResourceRequest(path: "https://parkstore.mybluemix.net/LocalstoreAdapter/apps/5e3ad88d-dd48-469d-b46f-2c4ad66b5345/localstore/getAllItems", method: "GET")
            request.sendWithCompletionHandler { (wlResponse:IMFResponse!, err:NSError!) -> Void in
                
  • Add the following code inside didFinishLaunchingWithOptions function in Appdelegate of client application which will register notifications in client app.
    1
    2
    3
    4
    5
    
    let notificationTypes: UIUserNotificationType = UIUserNotificationType.Badge | UIUserNotificationType.Alert | UIUserNotificationType.Sound
            let notificationSettings: UIUserNotificationSettings = UIUserNotificationSettings(forTypes: notificationTypes, categories: nil)
            application.registerUserNotificationSettings(notificationSettings)
            application.registerForRemoteNotifications()
            
  • Add the following code inside didRegisterForRemoteNotificationsWithDeviceToken function in Appdelegate of client application which will register pushclient and subscribe to tag in client app.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    
    IMFPushClient.sharedInstance().registerDeviceToken(deviceToken, completionHandler: { (response, error) -> Void in
                if error != nil {
                    println("Error during device registration \(error.description)")
                }
                else {
                    println("Response during device registration json: \(response.responseJson.description)")
                    var tags = ["parkstore"]
                    IMFPushClient.sharedInstance().subscribeToTags(tags, completionHandler: { (response:IMFResponse!, err:NSError!) -> Void in
                        if err != nil {
                            println("There was an error while subscribing to tag")
                        }else{
                            println("Successfully subscribe to tag parkstore")
                        }
                    })
                }
                
  • Add the following function inside Appdelegate which triggers when push notification arrived in client app.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    func application(application: UIApplication, didReceiveRemoteNotification userInfo: [NSObject : AnyObject]) {
            println("Got remote Notification. Data : \(userInfo.description)")
            let info = userInfo as NSDictionary
            let data = info.objectForKey("aps")?.objectForKey("alert") as! NSDictionary
            let userData = data.objectForKey("body") as! String
            let alertView = UIAlertView(title: "WishList!", message: "\(userData)", delegate: nil, cancelButtonTitle: "OK")
            alertView.show()
           }
    }
  • The following are the screenshots of client application. missing_alt missing_alt missing_alt missing_alt missing_alt

Migration to On-Prem

Migration of Client Application

Migration of Client Application includes following two steps
  • Configuring Cocoapods
  • Client App Migration

Configuring Cocoapods

If CocoaPods has not been installed on a specific computer:
  • Follow the "Getting Started" guide for CocoaPods installation: http://guides.cocoapods.org/using/getting-started.html
  • Open "Terminal" at the installation location and run the "pod init" command

The following steps assume that the client application is working with CocoPods. If not, follow this "Using CocoaPods" documentation : click here.

In both cases, the instructions below explain how to edit the "Podfile" file.

  1. Open the "Podfile" file located in the root of your XCode project in a favourite text editor.
  2. Comment out or remove the existing content.
  3. Add the following lines:
    1
    2
    
            source 'https://github.rtp.raleigh.ibm.com/imflocalsdks/imf-client-sdk-specs.git'
    pod 'IMFCompatibility'
  4. Open "Terminal" at the location of "Podfile".
  5. Verify that the XCode project is closed.
  6. Run the "pod install" command.

Open the [MyProject].xcworkspace file in XCode. This file is located side by side with [MyProject].xcodeproj.
An usual CocoaPods-based project is managed as a workspace containing the application (the executable) and the library (all project dependencies brought by the CocoaPods manager).
In Xcode's Build Settings, search for "Other Linker Flags" and insert ${inherited} (if -ObjC is defined in this field, you can just delete it, since it is configured in the CocoaPod project).

Client App Migration

  1. Search for bluemix dependency imports like
    1
    2
    
    #import <IMFCore/IMFCore.h>
    #import <IMFPush/IMFPush.h>
    Replace the above imports with
    1
    
    #import <IMFCompatibility/IMFCompatibility.h>
  2. Look for a call to the initializeWithBackendRoute method and replace the route URL with your on-premise server URL. For example:
     IMFClient.sharedInstance().initializeWithBackendRoute("https://parkstore.mybluemix.net", backendGUID: "5e3ad88d-dd48-469d-b46f-2c4ad66b5345"
             
    should be replaced with your on-premise MFP server URL:
    IMFClient.sharedInstance().initializeWithBackendRoute("http://localhost:10080/ParkStoreMFP", backendGUID: "5e3ad88d-dd48-469d-b46f-2c4ad66b5345"
    Note, that backendGUID parameter is ignored and can be empty. Look for all instantiations of IMFResourceRequest class and update it
  3. Look for all instantiations of IMFResourceRequest class and update the request URL with absolute or relative path to the resource. For example:
    1
    2
    
     var request: IMFResourceRequest = IMFResourceRequest(path: "https://parkstore.mybluemix.net/LocalstoreAdapter/apps/5e3ad88d-dd48-469d-b46f-2c4ad66b5345/localstore/getAllItems", method: "GET")
     
    should be replaced with
    1
    
    var request: IMFResourceRequest = IMFResourceRequest(path: "http://localhost:10080/ParkStoreMFP/adapters/LocalstoreAdapter/localstore/getAllItems", method: "GET")
  4. Add the following code inside didRegisterForRemoteNotificationsWithDeviceToken function in Appdelegate of Client application.
    1
    
     WLPush.sharedInstance().tokenFromClient = deviceToken.description
  5. All on-premise applications require the "worklight.plist" file to be present in the application resources. In the IBMMobileFirstPlatformFoundationNativeSDK pod we supply a file named sample.worklight.plist.
    • Locate the "sample.worklight.plist" file in the ‘IBMMobileFirstPlatformFoundationNativeSDK’ pod.
    • Copy this file to the parent (application) project and rename it to "worklight.plist".
    • Edit the "worklight.plist" file by setting the "application id" key to the name of your application deployed to the on-premise MFPF server

Migration of JAX-RS Application to JAVA Adapter

  1. To migrate JAX-RS application to on-prem (MobileFirst Foundation) server we need to do the following steps for server: Create MobileFirst Project --> Create native API app for iOS ​​ missing_alt missing_alt missing_alt
  2. Add two adapters for Custom Authentication and Localstore and migrate the JAX-RS code as shown in the following example.

Copy the JAX-RS BlueMix code and paste it in the newly created Localstore Java adapter JAX-RS file. Add and remove the following changes in your adapter code.

  • remove /{tenantId}/
  • remove the @PathParam -> PathParam("tenantId") String deviceId and @PathParam("realmName") String realmName
  • Add scope to the all http api resource @OAuthSecurity (scope="customAuthRealm_3")
The code looks like the following:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
	@GET
	@OAuthSecurity (scope="customAuthRealm_3")
	@Path("/getAllItems")
	public String getAllItems() throws MissingConfigurationOptionException{
		init();
		JsonArray jsonArray = new JsonArray();
		for(Object key : props.keySet()){
			jsonArray.add(parser.parse(props.getProperty((String) key)).getAsJsonObject());
		}
		return jsonArray.toString();
	}
	@PUT
	@OAuthSecurity (scope="customAuthRealm_3")
	@Path("/addItem")
	public void addItem(String itemJson)
			throws MissingConfigurationOptionException, URISyntaxException, IOException{
		try{
			init();
			int newKey = props.keySet().size()+1;
			props.put(String.valueOf(newKey), itemJson);
			URL url = this.getClass().getClassLoader().getResource("data.properties");
			File file = new File(url.toURI().getPath());
			FileOutputStream foStream = new FileOutputStream(file);
			props.store(foStream, "saving new item");
			foStream.close();
		}catch(IOException ioe){
			ioe.printStackTrace();
		}
	}
	@POST
	@OAuthSecurity (scope="customAuthRealm_3")
	@Path("/addAllItems")
	public String addAllItems(String itemsJson)
			throws MissingConfigurationOptionException, URISyntaxException, IOException{
		try{
			init();
			clearAllData();
			JsonArray jsonArr = parser.parse(itemsJson).getAsJsonArray();
			for(int i=0;i<jsonArr.size();i++){
				props.put(String.valueOf(i+1), jsonArr.get(i).toString());
			}
			URL url = this.getClass().getClassLoader().getResource("data.properties");
			File file = new File(url.toURI().getPath());
			FileOutputStream foStream = new FileOutputStream(file);
			props.store(foStream, "saving new item");
			foStream.close();
			return &amp;"true";
		}catch(IOException ioe){
			ioe.printStackTrace();
		}
		return &amp;"false&amp;";
	}
	@DELETE
	@OAuthSecurity(enabled=false)
	@Path("/clearAll")
	public String clearAllData()
			throws MissingConfigurationOptionException, URISyntaxException, IOException{
			init();
			props.clear();
			System.out.println("Size : "+props.size());
			URL url = this.getClass().getClassLoader().getResource("data.properties");
			File file = new File(url.toURI().getPath());
			FileOutputStream foStream = new FileOutputStream(file);
			props.store(foStream, "clearing all data");
			foStream.close();
			return "cleared";
	}

Configuring Custom-OAuth

  • Add realm with same name you had on BlueMix and login module to the authenticationConfig.xml.
    1
    2
    3
    4
    5
    6
    7
    8
    
        <realm name="customAuthRealm_3" loginModule="customAuthLoginModule_3">
        <className>com.worklight.core.auth.ext.CustomIdentityAuthenticator</className>
        <parameter name="providerUrl" value="http://localhost:10080/ParkStoreMFP/adapters/Customauth"/>
        </realm>
        <loginModule name="customAuthLoginModule_3" expirationInSeconds="3600">
        <className>com.worklight.core.auth.ext.CustomIdentityLoginModule</className>
        </loginModule>
        
  • Add Custom-oauth Realm in userIdentityRealms in Application Descriptor file of iOS Native API
    1
    2
    
        <userIdentityRealms>customAuthRealm_3</userIdentityRealms>
        

Configuring Push Capability

  • Add apns p12 certificate which is generated from Apple Developer Account under iOS Native API Folder missing_alt
  • Add Push configuration in Application Descriptor file of iOS Native API and include the password of added apns certificate.
    1
    2
    3
    4
    5
    6
    
    <pushSender password="password"/>
    <tags>
      <tag>
        <name>parkstore</name>
      </tag>
    </tags>
  • Create HTTP Push Adapter with following function code which will send the user push notification to the devices which is subscribed to tag "parkstore".
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
        function sendTagNotification(notificationText) {
            var notificationOptions = {};
            notificationOptions.message = {};
            notificationOptions.target = {};
            notificationOptions.message.alert = notificationText;
            notificationOptions.target.tagNames = ["parkstore"];
            WL.Server.sendMessage("ParkStoreMFP", notificationOptions);
            return {
                result : "Notification sent to users subscribed to the tag parkstore."
            };
        }

By performing above steps one can easily run iOS app built for Bluemix on MobileFirst Platform and following are the links to samples.

Sample and Source Code

Bluemix Server : Parkstore bluemix server
Bluemix Client : Parkstore bluemix
MFP Server : Parkstore mfp server
MFP Client : Parkstore mfp
Inclusive terminology note: The Mobile First Platform team is making changes to support the IBM® initiative to replace racially biased and other discriminatory language in our code and content with more inclusive language. While IBM values the use of inclusive language, terms that are outside of IBM's direct influence are sometimes required for the sake of maintaining user understanding. As other industry leaders join IBM in embracing the use of inclusive language, IBM will continue to update the documentation to reflect those changes.
Last modified on July 06, 2017