Blog Archives

Creating your first “Today” widget on iOS 8.0

Widgets? On iOS?!

At last us developers have been empowered by iOS to make our own widgets. This means that there are now additional available points of contact where we can interact with our users and allow them to use our app. Today we’re going to talk about one of these “extension points” – the “Today” extension.

The “Today” extensions are the ones available through the drop-down notification center drawer (that’s the one the comes down from the top). You’ll notice that in iOS 8.0, there’s a new “Edit” option there – it will open up a list of available extensions to add/remove to this “Today” view:

Screen Shot 2014-10-11 at 2.24.01 PM

I’m going to show you how to add a widget to your application that will be available in this list – this means that it will be available to your users once it’s added by them to their “Today” view.

Some things you should know about “Today” widgets

  • They are compiled separately as a standalone binary. This means that they can’t interact directly with your app or access the same memory.
  • They are essentially view controllers, meaning you can do anything you would do in a view controller within your widget. Keep in mind that the controls you put in there need to be accessed by the user in the notification center context so for instance UITextFields aren’t suitable for this controller (though they are technically¬†addable).
  • They are required to be efficient. The basic guidelines for efficient data fetch, caching and proper multithreading is crucial here. Your app (and widget) will be rejected if your “Today” widget is unresponsive and interferes with the overall awesomeness of the notification center.

Ladies and gentlemen, this is your widget speaking

Time to create our widget.

What we’re going to do today is create a flight info widget that acts as a companion widget for a flight check-in app. It offers up-to-date information about your flight in terms of boarding, takeoff and landing times as well as an inline option to transfer to your application for the actual online checkin process (which we won’t actually implement due to laziness).

First things first, let’s actually create a new widget. In order to do this, go to your project details in XCode 6 by selecting the project file in the navigator and click the “+” on the bottom left in order to add a new target. Next, select “Application Extension” under iOS. You’ll see the following menu drop down:

 

Select “Today Extension” and click “Next”. Now you’ll notice a dialog that looks very similar to the one you get when opening a new project. Remember when I said it’s a separate binary?

OK, now let’s get down to business. You’ll notice a new folder in your project which will contain a designated storyboard and a single view controller class. The storyboard is there to help you define¬†your widget layout as you would any controller. In our demo application, we’re going to add some static and dynamic labels, some image views with preset icons, 2 buttons, a title label and an activity indicator, so it would look something like this:

 

Now let’s write some code.

Updating your data

The underlying controller is where the magic happens. First thing you will notice is that your controller is pre populated with a¬†widgetPerformUpdateWithCompletionHandler: method. This method requires you to perform your updates and notify the OS when you’re done with the following possible outcomes:

  • NCUpdateResultNewData – New data is available. The widget might need to update layout.
  • NCUpdateResultNoData – There was no new data available since the last update.
  • NCUpdateResultFailed – We’ve failed the attempt to update.

For the sake of simplicity, we’re going to work with a simulated fetch of our data. This is going to be performed by calling an asynchronous method with a block callback:

- (void)fetchFlightInfoWithCompletion:(FlightInfoCompletion)completion
{
    //Preset dummy info
    static NSString *boardingTime = @"12:05";
    static NSString *takeoffTime = @"12:35";
    static NSString *landingTime = @"18:10";
    static BOOL checkedIn = YES;

    //Simulate asynchronous fetch
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        sleep(5);

        //Update data
        self.lastKnownTakeoffTime = takeoffTime;
        self.lastKnownBoardingTime = boardingTime;
        self.lastKnownLandingTime = landingTime;
        self.lastKnownCheckinStatus = checkedIn;
        self.dataLoaded = YES;

        //Call completion on main thread
        dispatch_async(dispatch_get_main_queue(), ^{
            if (completion)
            {
                completion(YES,boardingTime,takeoffTime,landingTime,YES,nil);
            }
        });
    });
}

Every time the operating system is ready for updating our widget, it’s going to call the following widgetPerformUpdateWithCompletionHandler: method

- (void)widgetPerformUpdateWithCompletionHandler:(void (^)(NCUpdateResult))completionHandler {
    if (!self.dataLoaded)
    {
        //Start fetch
        [self.aiLoading startAnimating];
        [self fetchFlightInfoWithCompletion:^(BOOL newData, NSString *boardingTime, NSString *takeoffTime, NSString *landingTime, BOOL checkedIn, NSError *err) {
            [self.aiLoading stopAnimating];
            if (err) //An error occured - make sure to notify the OS
            {
                completionHandler(NCUpdateResultFailed);
            }
            else if (newData) //New data is available - update UI and notify OS there's new data
            {
                self.lblBoarding.text = boardingTime;
                self.lblTakeoff.text = takeoffTime;
                self.lblLanding.text = landingTime;
                [self updateCheckinStatusUI:checkedIn];
                completionHandler(NCUpdateResultNewData);
            }
            else //Nothing new - no data to update
            {
                completionHandler(NCUpdateResultNoData);
            }
        }];
    }
    else
    {
        //refresh for next time and force refresh
        [self fetchFlightInfoWithCompletion:nil];
        self.dataLoaded = NO;

        //update UI
        self.lblBoarding.text = self.lastKnownBoardingTime;
        self.lblTakeoff.text = self.lastKnownTakeoffTime;
        self.lblLanding.text = self.lastKnownLandingTime;
        [self updateCheckinStatusUI:self.lastKnownCheckinStatus];

        //The data fetched is not new, make sure to notify the OS
        completionHandler(NCUpdateResultNoData);
    }
}

In order to make sure we’re updated before this is called, a good option would be to call the update method from viewWillAppear:

- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];

    //Make sure we're up to date next time we're viewed
    [self fetchFlightInfoWithCompletion:nil];
}

Layout

In terms of layout, we need to make sure to notify about our preferred content size. This is ideal to do in awakeFromNib:

- (void)awakeFromNib
{
    [super awakeFromNib];

    [self setPreferredContentSize:CGSizeMake(kMyFlightWidgetWidth, kMyFlightWidgetHeightWhenNotCheckedIn)];
}

User interactions and referring to your app

Now let’s take care of what happens when tapping “Check in”. What we want to do is open our app with additional data to handle. The best way to do this is with openURL (and of course implementing handleOpenURL in the target application). We’re not going to elaborate about this but make sure you register your app for the proper URL scheme so that it will be launched when calling openURL.

- (IBAction)checkInPressed:(id)sender
{
    [[self extensionContext] openURL:[NSURL URLWithString:@"myflight://check-in"] completionHandler:^(BOOL success) {

    }];
}

When the user wants to manually refresh the data, this is called:

- (IBAction)refreshPressed:(id)sender
{
    [self.aiLoading startAnimating];
    [self fetchFlightInfoWithCompletion:^(BOOL newData, NSString *boardingTime, NSString *takeoffTime, NSString *landingTime,BOOL checkedIn,NSError *err) {
        [self.aiLoading stopAnimating];
        if (newData)
        {
            self.lblBoarding.text = boardingTime;
            self.lblTakeoff.text = takeoffTime;
            self.lblLanding.text = landingTime;
            [self updateCheckinStatusUI:checkedIn];
        }
    }];
}

That’s all folks!

This is how our little flight widget appears in the notification center:


That’s about it – we now have a functioning widget available when your application is installed. You can try it by running its target as you would any other app. The dummy project and the widget itself can be downloaded here.

Icons used in the demo project were taken from icons8

 

Advertisements