MobileFirst tweet notifications on Apple Watch (Swift)

In this blog we will do the following:
  1. Create an iOS app and use MobileFirst native API
  2. Use a MobileFirst HTTP adapter
  3. Implement tag-based push notifications to notify users when new tweets are posted
  4. Register a Twitter app and use the Twitter API to obtain a list of recent tweets
  5. Use an event source to periodically check for new tweets
  6. Create a WatchKit app that displays the list of tweets and handles notifications
  7. Pass data between the iOS and WatchKit app
missing_alt

Creating the iOS App

Start by creating an iOS project with a Single View Application. Choose Swift for the language. We want to display tweets that will get updated frequently, so I've decided to show tweets that contain '#AppleWatch' since that's both relevant to the project and a very active hashtag right now. In our iOS app, we'll use a TableView to display the list of tweets. We'll also have a refresh button for this table. We want to allow the user to subscribe and unsubscribe from the push notifications, so we''ll have a button for that too. So, add those 3 items to the storyboard. We're also going to have a label for the table. missing_alt Create IBActions for the buttons and IBOutlets for the buttons and table view:
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
class ViewController: UIViewController {
    @IBOutlet weak var refreshButton: UIButton!
    @IBOutlet weak var tweetsTable: UITableView!
    @IBOutlet weak var toggleSubscriptionButton: UIButton!
    override func viewDidLoad() {
        super.viewDidLoad()
    }
    @IBAction func refreshButtonClicked(sender: AnyObject) {
    }
    @IBAction func toggleSubscription(sender: AnyObject) {
    }
[/code]
Since the subscription button will both subscribe and unsubscribe, its text will change to show that. We want to have a flag to indicated whether or not the user is currently subscribed. The flag will be false initially, but will change every time the button is tapped.
[code title="ViewController.swift"]
class ViewController: UIViewController {
    ...
    @IBOutlet weak var toggleSubscriptionButton: UIButton!
    var isSubscribed = false
    override func viewDidLoad() {
        super.viewDidLoad()
        if (isSubscribed){
            toggleSubscriptionButton.setTitle("Stop receiving notifications", forState: UIControlState.Normal)
        }
        else{
            toggleSubscriptionButton.setTitle("Start receiving notifications", forState: UIControlState.Normal)
        }
    }
    ...
    @IBAction func toggleSubscription(sender: AnyObject) {
        if (!isSubscribed){
            isSubscribed = true
            toggleSubscriptionButton.setTitle("Stop receiving notifications", forState: UIControlState.Normal)
        }
        else{
            isSubscribed = false
            toggleSubscriptionButton.setTitle("Start receiving notifications", forState: UIControlState.Normal)
        }
    }
At this point we can run the app on the iPhone simulator and see that tapping the button multiple times changes the text back and forth from "Start receiving..." to "Stop receiving..."

Adding the MobileFirst Adapter

Now we’ve got to set up the MobileFirst portion of the application. Using MobileFirst Studio, create a MobileFirst Project with a Native iOS API. Then create a MobileFirst HTTP Adapter: missing_alt You should now have the following project structure in Studio: missing_alt Now we're going to integrate the MobileFirst SDK into the iOS application:
  1. Copy the WorklightAPI folder and worklight.plist file from the MobileFirst project into the Xcode project.
  2. Inside the Xcode project Build Settings, in the Swift Compiler - Code Generation section, set Objective-C Bridging Header to "WorklightAPI/include/WLSwiftBridgingHeader.h"
  3. In the Linking section, set Other Linker Flags to "-ObjC"
  4. Inside Build Phases, in the Link Binary With Libraries section, add the following frameworks: missing_alt
Now it's time for the fun stuff! The next step is to establish a connection to the server. To do this, we need our ViewController to implement WLDelegate protocol. So, add that to the class definition:
1
2
3
class ViewController: UIViewController, WLDelegate {
...
}
Now we will get an error saying that ViewController does not conform to protocol 'WLDelegate' because we need to implement certain methods that are required for WLDelegate. Command+Click on WLDelegate and we can see the method definitions that we need to add to our ViewController:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
 * @ingroup main
 *
 * A protocol that defines methods that a delegate for the WLClient invokeProcedure method should implement,
 * to receive notifications about the success or failure of the method call.
 */
@protocol WLDelegate <NSObject>
/**
 *
 * This method will be called upon a successful call to WLCLient invokeProcedure with the WLResponse containing the
 * results from the server, along with any invocation context object and status.
 *
 * @param response contains the results from the server, along with any invocation context object and status.
 **/
-(void)onSuccess:(WLResponse *)response;
/**
 *
 * This method will be called if any kind of failure occurred during the execution of WLCLient invokeProcedure.
 *
 * @param response contains the error code and error message, and optionally the results from the server,along with any invocation context object and status.
 **/
-(void)onFailure:(WLFailResponse *)response;
@end
Now we can add those methods in Swift. We'll also print to the console in both methods just to show where we are:
1
2
3
4
5
6
func onSuccess(response: WLResponse!) {
    NSLog("onSuccess")
}
func onFailure(response: WLFailResponse!) {
    NSLog("onFailure")
}
Now that we conform to the WLDelegate protocol, we can use the WLClient API to connect to the server. Add the following line to the viewDidLoad() method:

1
2
3
4
5
6
7
8
9
10
override func viewDidLoad() {
        super.viewDidLoad()
        WLClient.sharedInstance().wlConnectWithDelegate(self)
        if (isSubscribed){
            toggleSubscriptionButton.setTitle("Stop receiving notifications", forState: UIControlState.Normal)
        }
        else{
            toggleSubscriptionButton.setTitle("Start receiving notifications", forState: UIControlState.Normal)
        }
}
Let's test what we have so far and make sure we can connect to the server:
  1. In the MobileFirst Studio project, right-click the app name and select Run As->Deploy native API missing_alt
  2. Right-click the adapter name and select Run As->Deploy MobileFirst Adapter missing_alt
  3. Run the Xcode app.
You should see a lot of output in the console including the "onSuccess" log.

Push Notifications

Now we can implement the tag subscription. Push notifications in iOS requires an APNS p12 certificate to be added to the MobileFirst project.

missing_alt Inside the application-descriptor.xml file, provide the certificate password and the subscription tag:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<nativeIOSApp id="TwitterFeeds" platformVersion="7.0.0.00.20150402-2001" bundleId="com.TwitterFeeds"
    version="1.0" xmlns="http://www.worklight.com/native-ios-descriptor">
    <displayName>TwitterFeeds>/displayName>
    <description>TwitterFeeds>/description>
    <accessTokenExpiration>3600>/accessTokenExpiration>
    <userIdentityRealms>>/userIdentityRealms>
        <tags>
        <tag>
            <name>sample-tag1>/name>
            <description>A sample tag 1>/description>
        </tag>
    </tags>
<pushSender password="password"/>
</nativeIOSApp>
Next is implementing the iOS portion of the subscription. In the same way we had to use WLDelegate to connect to the server, we'll need to use WLOnReadyToSubscribeListener to subscribe to the tag. To conform to its protocol, we have to implement the OnReadyToSubscribe() method. When we reach that method, we'll know that the application is ready to subscribe, so we can enable the subscribe button in there. Prior to that, it should be disabled. We will also need to initialize the WLPush instance inside the viewDidLoad() method:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class ViewController: UIViewController, WLDelegate, WLOnReadyToSubscribeListener {
    ...
    override func viewDidLoad() {
        super.viewDidLoad()
        toggleSubscriptionButton.enabled = false
        WLClient.sharedInstance().wlConnectWithDelegate(self)
        WLPush.sharedInstance().onReadyToSubscribeListener = self
        if (isSubscribed){
            toggleSubscriptionButton.setTitle("Stop receiving notifications", forState: UIControlState.Normal)
        }
        else{
            toggleSubscriptionButton.setTitle("Start receiving notifications", forState: UIControlState.Normal)
        }
    }
    ...
    func OnReadyToSubscribe() {
        NSLog("ready to subscribe")
        toggleSubscriptionButton.enabled = true
    }
    ...
}
We also have to make changes to the AppDelegate. Inside the didFinishLaunchingWithOptions method, we need to initialize the push instance. We also need to implement the didRegisterForRemoteNotificationsWithDeviceToken method to set the device token:

1
2
3
4
5
6
7
8
9
10
11
class AppDelegate: UIResponder, UIApplicationDelegate {
    var window: UIWindow?
   func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject : AnyObject]?) -> Bool {
        WLPush.sharedInstance()
        return true
    }
    func application(application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: NSData) {
        WLPush.sharedInstance().tokenFromClient = deviceToken.description
    }
    ...
}
To test that the app is ready to subscribe, run the app on a device, and make sure the subscribe button becomes enabled. Also make sure the console contains the "ready to subscribe" log.

Next, we need to implement the actual subscribing. Inside the toggleSubscription method, we'll use the WLPush API to subscribe to (and unsubscribe from) the tag:

1
2
3
4
5
6
7
8
9
10
11
12
@IBAction func toggleSubscription(sender: AnyObject) {
    if (!isSubscribed){
        WLPush.sharedInstance().subscribeTag("sample-tag1", nil, self)
        isSubscribed = true
        toggleSubscriptionButton.setTitle("Stop receiving notifications", forState: UIControlState.Normal)
   }
    else{
        WLPush.sharedInstance().unsubscribeTag("sample-tag1", self)
        isSubscribed = false
        toggleSubscriptionButton.setTitle("Start receiving notifications", forState: UIControlState.Normal)
    }
}
If we run the app on a device and click the subscribe button, we'll see a log message saying "Successfully subscribed to tag sample-tag1" in the console. If we click again, we'll see "Successfully unsubscribed from tag sample-tag1" as well.

Using the Twitter API

Visit the Twitter Application Management site to set up a Twitter app (you must have a Twitter account to do this). In Application-only authentication, Follow Step 1 under Issuing application-only requests to obtain your encoded credentials. Add them to the worklight.properties file in your MobileFirst project:

1
my.twitter.app.credentials=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
The rest of the work will be done in the adapter. Following Step 2, we're going to obtain an access token. Inside TwitterAdapter-impl.js, we'll have an access token variable that will be null initially and then updated:

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
var accessToken = null;
function getTwitterAccessToken(){
    WL.Logger.info("getTwitterAccessToken");
    if (accessToken != null){
        WL.Logger.info("getTwitterAccessToken :: returning existing accessToken");
        return accessToken;
    }
    WL.Logger.info("getTwitterAccessToken :: getting a new accessToken");
    var requestOptions = {
            method:"POST",
            headers:{
                Authorization: "Basic " + WL.Server.configuration["my.twitter.app.credentials"],
            },
            path:"oauth2/token",
            body:{
                content:"grant_type=client_credentials",
                contentType:"application/x-www-form-urlencoded;charset=UTF-8"
            }
    };
    var response = WL.Server.invokeHttp(requestOptions);
    if (response.statusCode == 200){
        WL.Logger.info("getTwitterAccessToken :: got a new accessToken");
        accessToken = response.access_token;
    } else {
        WL.Logger.info("getTwitterAccessToken :: failed to get a new accessToken");
        accessToken = null;
    }
    return accessToken ? "Bearer " + accessToken : accessToken;
}
For Step 3, we're going to make a request using the value of the Bearer token as the Authorization header. The path will specify that we only want tweets containing #AppleWatch. We're going to get only the 10 most recent tweets:

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
function getTweets(count){
    count = count || 10;
    function getTweetsInternal(shouldRetry){
        var requestOptions = {
                method: "GET",
                headers:{
                    Authorization: getTwitterAccessToken()
                },
                path:"/1.1/search/tweets.json?q=apple%20watch&count=" + count
        };
        var response = WL.Server.invokeHttp(requestOptions);
        if (response.statusCode == 200){
            return response.statuses;
        }
        if (shouldRetry){
            accessToken = null;
            return getTweetsInternal(false);
        } else {
            return [];
        }
    }
    return {
        tweets: getTweetsInternal(true)
    };
}

The shouldRetry boolean is to prevent the code from retrying to get tweets more than once if it is unsuccessful. Without this, we could get an infinite loop.
We want to notify the user when a new tweet arrives. So we need to keep track of the latest tweet, get a new one, and compare the two. If they're different, we send a push notification to our tag and update the latest tweet:

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
var accessToken = null;
var storedLatestTweetId = 0;
...
function getLatestTweetId(){
    var tweetsObj = getTweets(1);
    if (tweetsObj.tweets.length < 1){
        return null;
    } else {
        return tweetsObj.tweets[0].id;
    }
}

function checkNewTweets(){
    WL.Logger.error("checkNewTweets");
    var currentLatestTweetId = getLatestTweetId();
    WL.Logger.error("checkNewTweets :: currentLatestTweetId :: "+ currentLatestTweetId);
    if (currentLatestTweetId != storedLatestTweetId){
        // send push notification
        var notificationOptions = {
                message:{
                    alert:"New #AppleWatch tweet!"
                },
                target:{
                    tagNames:["sample-tag1"]
                }
        };
        WL.Server.sendMessage("TwitterFeeds", notificationOptions)
        storedLatestTweetId = currentLatestTweetId;
    }
}

Creating an Event Source

We want to be able to check for new tweets periodically, so we're going to this with an event source. For testing purposes, we'll use an interval of 30 seconds. A more practical interval would be 30 minutes (1800 seconds):

1
2
3
4
5
6
7
8
9
var accessToken = null;
var storedLatestTweetId = 0;
WL.Server.createEventSource({
    name:"TwitterEventSource",
    poll:{
        interval:30,
        onPoll:"checkNewTweets"
    }
});

That's everything we need to do in the adapter. Add a tag for each procedure in the adapter XML file:

1
2
3
4
5
6
7
8
<wl:adapter name="TwitterAdapter"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:wl="http://www.ibm.com/mfp/integration"
    xmlns:http="http://www.ibm.com/mfp/integration/http">
    ...
    <procedure name="getTweets"/>
    <procedure name="checkNewTweets"/>
</wl:adapter>

To check that this all works, visit this URL in your browser:
http://{server}:{port}/context/invoke?adapter=TwitterAdapter&procedure=getTweets¶meters=["10"] You should see a ton of JSON output, but if you look closely you will see that the tweets are there. You can put the output into a JSON prettifier and see it more clearly.
The next step is to get the tweets into the TableView in our iOS app in the ViewController. We're going to add variables for a cell identifier and a tweets array:

1
2
3
4
5
6
@IBOutlet weak var refreshButton: UIButton!
@IBOutlet weak var tweetsTable: UITableView!
@IBOutlet weak var toggleSubscriptionButton: UIButton!
var isSubscribed = false
let textCellIdentifier = "cell"
var tweets = []

To use the TableView, we need to conform to the UITableViewDelegate and UITableViewDataSource protocols. For the cellForRowAtIndexPath method, we're going to display only the main content of the tweet, which has the key "text" within the tweets array (you can see this key in the JSON output from invoking the adapter procedure).

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
class ViewController: UIViewController, WLDelegate, WLOnReadyToSubscribeListener, UITableViewDelegate, UITableViewDataSource {
    ...
    let textCellIdentifier = "cell"
    var tweets = []
    override func viewDidLoad() {
        super.viewDidLoad()
        toggleSubscriptionButton.enabled = false
        tweetsTable.delegate = self
        tweetsTable.dataSource = self
        ...
    }
    ...
    func numberOfSectionsInTableView(tableView: UITableView) -> Int {
        return 1
    }
    func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return self.tweets.count
    }
    func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCellWithIdentifier(textCellIdentifier, forIndexPath: indexPath) as! UITableViewCell
        let row = indexPath.row
        let tweet = self.tweets.objectAtIndex(row) as! NSDictionary;
        let title = tweet.objectForKey("text") as! NSString
        cell.textLabel?.text = title as String
        return cell
    }
}

Now to actually populate the table - we want to do this both in our iOS app and on the Apple Watch.

Creating the WatchKit App

Add a WatchKit App target to the iOS project. On the watch, we're only going to show the table of tweets. We don't need any buttons, but we do want to put a label inside the table to display the tweet:

missing_alt

Add a new class to the WatchKit Extension folder called FeedRowController that is a subclass of NSObject. This class will hold the content for each row of the table. In the storyboard, select the TableRowController from the Interface Controller Scene and set its class and identifier to FeedRowController:

missing_alt

Add an IBOutlet for the label. You will need to add the import WatchKit statement.

1
2
3
4
import WatchKit
class FeedRowController: NSObject {
    @IBOutlet weak var labelText: WKInterfaceLabel!
}

In the InterfaceController, add an IBOutlet for the table.

1
2
3
class InterfaceController: NSObject {
    @IBOutlet weak var tweetsTable: WKInterfaceTable!
}

Passing Data Between the Two Apps

We want to keep all of the logic that retrieves the tweets in one place. To keep things simple, we will do all of this in a separate class. Add a new class to the application folder called RequestManager that is a subclass of NSObject. This class will make the request to the adapter to get the tweets and send the response with a completion handler. We will send this data back and forth between the iOS app and the WatchKit app by using WatchKit's openParentApplication and handleWatchKitExtensionRequest methods.

Since we are making an adapter request, the RequestManager class will need to conform to the WLDelegate protocol and implement onSuccess and onFailure methods. We aren't going to do anything inside these methods except print to the console so we know that we've reached those methods.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class RequestManager: NSObject, WLDelegate {
    class func retrieveTweets(completionHandler:(AnyObject)->Void){
        var url = NSURL(string: "/adapters/TwitterAdapter/getTweets")
        var request = WLResourceRequest(URL: url, method: "GET")
        request.setQueryParameterValue("['10']", forName: "params")
        request.sendWithCompletionHandler { (WLResponse response, NSError error) -> Void in
            if(error != nil){
                // error
                var emptyDict = Dictionary<String, String>()
                completionHandler(emptyDict)
            }
            else if(response != nil){
                completionHandler(response.getResponseJson())
            }
        }
    }
    func onSuccess(response: WLResponse!) {
        NSLog("onSuccess")
    }
    func onFailure(response: WLFailResponse!) {
        NSLog("onFailure")
    }
}

The openParentApplication method will be called inside willActivate. We will create a dictionary specifying the action we want to take which will be passed to the iOS app. The tweets will be received in the reply block and used to populate the table.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class InterfaceController: NSObject {
    @IBOutlet weak var tweetsTable: WKInterfaceTable!
    ...
    override func willActivate() {
        super.willActivate()
        var actionDictionary = ["action":"getTweets"]
        WKInterfaceController.openParentApplication(actionDictionary) { (reply, error) -> Void in
            if (error != nil){
                NSLog("Failed getting tweets. Error :: %@", error)
            } else {
                NSLog("Got tweets. Rebuilding table")
                var tweets = reply["tweets"] as! NSArray
                self.tweetsTable.setNumberOfRows(tweets.count, withRowType: "FeedRowController")
                for (index, tweet) in enumerate(tweets){
                    let row = self.tweetsTable.rowControllerAtIndex(index) as! FeedRowController
                    let title = tweet.objectForKey("text") as! String
                    row.labelText.setText(title)
                    NSLog("tweet %d : %a", index, title)
                }
            }
        }
    }
}

The handleWatchKitExtensionRequest method will be implemented inside the AppDelegate. This method is going to identify the action we specified in the dictionary that is passed through the userInfo parameter and respond accordingly by calling the retreiveTweets method and sending the results to the WatchKit app through the reply block.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func application(application: UIApplication, handleWatchKitExtensionRequest userInfo: [NSObject : AnyObject]?, reply: (([ NSObject : AnyObject]!) -> Void)!){
    let userInfoDict = userInfo as? Dictionary<String, String>
    let action = userInfoDict?["action"]
    if (action == "getTweets") {
        var watchKitHandler = UIBackgroundTaskInvalid
        watchKitHandler = UIApplication.sharedApplication().beginBackgroundTaskWithName("backgroundTask", expirationHandler: { () -> Void in
            watchKitHandler = UIBackgroundTaskInvalid
        });
        RequestManager.retrieveTweets { (tweets) -> Void in
            reply(tweets as! Dictionary)
        }
    } else {
        reply(Dictionary<String, String>())
    }
}

For our purposes we are only performing one action, which is getting the tweets. If we wanted to handle other actions, we would specify those in the actionDictionary we pass to openParentApplication and process them with additional if statements in handleWatchKitExtensionRequest.

We're going to add a loadTweets method in ViewController that will call the retreiveTweets method and reload the table data. This method will be called on two occasions: when a successful connection is established and when the refresh button is tapped:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func loadTweets(){
    RequestManager.retrieveTweets { (retrievedTweets) -> Void in
        self.tweets = retrievedTweets["tweets"] as! NSArray
        NSLog("%@", self.tweets)
        self.tweetsTable.reloadData()
    }
}
@IBAction func refreshButtonClicked(sender: AnyObject) {
    loadTweets()
}
...
func onSuccess(response: WLResponse!) {
    NSLog("onSuccess")
    loadTweets()
}

The last thing we need to do is ensure that our WatchKit app can handle receiving notifications. To do this, we just have to uncomment the didReceiveRemoteNotification method inside NotificationController:

1
2
3
4
5
6
7
8
override func didReceiveRemoteNotification(remoteNotification: [NSObject : AnyObject], withCompletion completionHandler: ((WKUserNotificationInterfaceType) -> Void)) {
    // This method is called when a remote notification needs to be presented.
    // Implement it if you use a dynamic notification interface.
    // Populate your dynamic notification interface as quickly as possible.
    //
    // After populating your dynamic notification interface call the completion block.
    completionHandler(.Custom)
}

We're done! To test that the notifications are received on the Watch, run the app on an iPhone (with the MobileFirst native API and adapter deployed), then close the app and lock the iPhone. Put the Watch on (the Watch needs to be worn so that the wearer gets notification there and not the phone) and wait the 10 seconds or so for a new tweet to arrive. This doesn't require you to do any manual checking since we check for new tweets with the event source. When you get the notification on the watch, tap the app icon to launch the WatchKit app.

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 May 01, 2016