iOS 8 Today Extension in a MobileFirst application

iOS 8 added support for App Extensions, which lets you extend custom functionality and content outside of your application.

missing_alt

One such extension is the Today Widget. Today Widgets appear in the top drawer of the iOS screen, and show content for a quick glance. An example of a Today Widget is the Calendar Today Widget built-in iOS.

In this example we’ll use the Native iOS Starter Application.

We will be adding a Today Widget to the Starter Application, showing the latest news from the RSS feed in the Today View.

Note: Knowledge of Objective-C or Swift is required to develop your own Today Extension. Objective-C is used for this blog post.


Add a Today Extension Target

missing_alt

In Xcode, while the project is open, go to File > New > Target . In the wizard, choose iOS > Application Extension > Today Extension.

Choose a Product Name (“Today News” in my example) and any other setting required and click Finish. Xcode will generate a new folder in your project and a new target. This will provide a basic skeleton to design and code the extension.

Design the Storyboard

The generated skeleton comes with a simple storyboard (MainInterface.storyboard) and a ViewController (TodayViewController.h). You can design the visual interface just like in any other storyboard. In this example, I kept the storyboard as-is (just a Label centered inside the view). I’ve Control-Dragged the Label to TodayViewController.h to add an outlet.

missing_alt

Because I also want to be able to capture taps on the widget, I’ve added a Tap Gesture Recognizer to my View.

missing_alt

I then linked it to an action in TodayViewController.m. missing_alt

In a real application, you will probably use a more complete design, however this is out of the scope of this article.

Link the MobileFirst (Worklight) Library

Because the Today Extension is a different target from your main application, it lives in its own sandbox - separate from your application sandbox, with its own lifecycle. This has important implications on the way you design and code your widget. You will not be able to share objects living in your application with your widget.

If you plan to share some data between the two, there exist some workarounds which you can find online (http://tapadoo.com/2014/sharing-nsuserdefaults-between-your-app-and-a-today-extension-on-ios-8/).

In this example, to get the latest news from the RSS adapter, I need access to the MobileFirst API from my widget. To setup the target with the MobileFirst API, you need to follow the same instructions provided by the documentation “Developing native applications for iOS”. Make sure the worklight.plist also appears in the Copy Bundle Resources, and don’t forget to set the required Build Settings.

missing_alt

Lifecycle

The generated TodayViewController.m contains a method called widgetPerformUpdateWithCompletionHandler. This method is called by iOS to give your widget an opportunity to update itself. This can happen in the background, or when the user pulls the Today view out.

To see it in action, try to write a simple code that updates the label with the current date.

1
2
3
4
5
6
7
8
- (void)widgetPerformUpdateWithCompletionHandler:(void (^)(NCUpdateResult))completionHandler {</p>
    NSDate *today = [NSDate date];
    NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
    [dateFormatter setTimeStyle:NSDateFormatterMediumStyle];
    NSString *currentTime = [dateFormatter stringFromDate:today];
    [self.label setText:currentTime];
    completionHandler(NCUpdateResultNewData);
}

Look at the generated timestamp every time your open the widget. Also notice the completionHandler call. This important line lets iOS know that you are finished with the updates. A badly behaving widget may be rejected by Apple, or simply may be killed by iOS while running.

While the ViewController also includes lifecycle methods such as viewDidLoad, it is important to note that your widget can have a very short and unpredictable lifecycle, independent from your main application lifecycle. It may be loaded just as the user opens the drawer and be unloaded right after, or it may load in the background for a few seconds.

Invoking Procedure

In this example the update being performed is to fetch new data from the RSS adapter.

1
2
3
4
5
- (void)widgetPerformUpdateWithCompletionHandler:(void (^)(NCUpdateResult))completionHandler {
    WLProcedureInvocationData *myInvocationData = [[WLProcedureInvocationData alloc] initWithAdapterName:@"StarterApplicationAdapter" procedureName:@"getEngadgetFeeds"];
    TodayInvoke *invokeListener = [[TodayInvoke alloc] initWithController:self andCompletionHandler:completionHandler];
    [[WLClient sharedInstance] invokeProcedure:myInvocationData withDelegate:invokeListener];
}

When initializing the invocation listener, I passed both the current ViewController and the completion handler. The ViewController is passed so that the listener is able to update the label in the view once it receives the data. The completion handler is passed so that the listener can inform the operating system when the operation is complete.

1
2
3
4
5
6
7
8
9
10
#import <Foundation/Foundation.h>
#import "WLClient.h"
#import "WLDelegate.h"
#import "TodayViewController.h"
#import <NotificationCenter/NotificationCenter.h>
@interface TodayInvoke : NSObject <WLDelegate>
@property TodayViewController *vc;
@property (nonatomic, copy) void (^completionHandler)(NCUpdateResult completionHandler);
- (id)initWithController: (TodayViewController *)mainView andCompletionHandler:(void (^)(NCUpdateResult))completionHandler;
@end
1
2
3
4
5
6
7
8
- (id)initWithController: (TodayViewController *)mainView andCompletionHandler:(void (^)(NCUpdateResult))completionHandler {
    if ( self = [super init] )
    {
        self.vc = mainView;
        self.completionHandler = completionHandler;
    }
    return self;
}

The onSuccess method will trigger the view updates. To make this example a bit more reactive, I am picking a news item at random instead of showing the truly "latest" news.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
-(void)onSuccess:(WLResponse *)response{
    if(response && response.invocationResult && response.invocationResult.response && response.invocationResult.response[@"items"]){
        //Get random news item
        NSArray *newsItems = response.invocationResult.response[@"items"];
        NSUInteger rnd = arc4random_uniform((int)[newsItems count]);
        NSDictionary *randomObject = [newsItems objectAtIndex:rnd];
        //Update the view
        [self.vc.label setText: randomObject[@"title"]];
        //Cache result
        NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
        [userDefaults setObject:randomObject[@"title"] forKey:@"latestNews"];
        [userDefaults synchronize];
        //Inform operating system
        if(self.completionHandler){
            self.completionHandler(NCUpdateResultNewData);
        }
    } else{
        //No news
        if(self.completionHandler){
            self.completionHandler(NCUpdateResultNoData);
        }
    }
}

You'll also notice that I am using NSUserDefaults to cache the results. This is because if the widget is pulled out of memory, or if the refresh fails, I still want to have the previous news item show, instead of staying empty.

To do so, I've updated viewDidLoad to retrieve the latest cached version just in case.

1
2
3
4
5
6
7
8
- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view from its nib.
    NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
    if([userDefaults stringForKey:@"latestNews"]){
        [self.label setText:[userDefaults stringForKey:@"latestNews"]];
    }
}

With all of this, you should be able to view your widget in action.

missing_alt

Tap to open

Again, because the widget lives in a different sandbox, you cannot simply "segue" into your application. You need to open your application just like you would from the another application.

The way to do this is via URL Schemes (Good read: http://iosdevelopertips.com/cocoa/launching-your-own-application-via-a-custom-url-scheme.html).

You first need to register your main application to respond to a specific URL scheme. In the main application's Info.plist, add a URL types property, with a URL scheme and URL identifier. See above link for more details.

missing_alt
1
2
3
4
5
6
7
8
9
10
11
	<key>CFBundleURLTypes</key>
	<array>
		<dict>
			<key>CFBundleURLSchemes</key>
			<array>
				<string>StarterApplication</string>
			</array>
			<key>CFBundleURLName</key>
			<string>com.worklight.StarterApplicationNativeiOS</string>
		</dict>
	</array>

On the other side, from your widget code, you can use the tap action we added earlier to open a URL as defined here.

1
2
3
4
5
- (IBAction)tap:(id)sender {
    NSLog(@"tapped!");
    NSURL *customURL = [NSURL URLWithString:@"StarterApplication://"];
    [self.extensionContext openURL:customURL completionHandler:nil];
}

Authentication Concepts

In some cases, your extension may need to reach protected resources. In general, this is handled by a Challenge Handler, showing a login form or other form of authentication.

While most MobileFirst APIs can work in an extension in theory, extensions don't allow - or at least are not designed for - user inputs. This means you cannot ask your user to enter a username and password. In addition, because the lifecycle of the widget is unpredictable, this would lead to a subpar user experience.

There are workarounds to this issue. I will not go into great details for the scope of this article (stay tune for a follow-up blog post) but here is one possible workaround:

We mentioned that the application and the widget can share a NSUserDefaults object. When the user logs in successfully in the main application, you can generate in the server a custom token and store it in NSUserDefaults. The extension can use this token to handle a challenge. If the token does not exist or is invalid, the extension could open the main application (using openURL) and prompt the user to login.

Hybrid Applications

Those steps can also be used in a MobileFirst Hybrid application. However instead of linking the native API library, you need to link the libraries generated by MobileFirst Studio, including Cordova and all its dependencies. The easiest way is probably to look at the "Build Phases" tab of your main application and try to replicate those settings in your Today target.

Conclusion

A Today Extension can be a very nice addition to your application, and MobileFirst can help you achieve this.

However, keep in mind that a Today widget needs to be useful! A widget that simply opens your applications does not add any real value and can get you bad review (and may even be rejected by Apple). Those widgets are meant to improve the user experience, bringing useful data to the user at a quick glance.

Last modified on June 15, 2016
Share this post: