Smarter ideas worth writing about.

Extending Your Current iOS Applications - Today Extension

iOS 8 provides an easy method for extending the presence of your existing applications.  App Extensions were introduced as a way to extend the functionality and content of your application to other applications or the system.  This article describes how easily a particular type of App Extension, Today Extension, can be incorporated into an existing application, giving the application new life.  Note, Swift will be the language used in code samples in this article.

Existing Application
The existing application used in this article is a mockup of my favorite surf forecasting application, Swell Info.  This application is used by surfers to view the surf conditions for the upcoming week.  Conditions such as swell size, swell condition (clean, choppy, fair), wind speed and direction, and water temperature for your favorite surf breaks are listed in a simple table view.  Below is a screenshot of the conditions for my home break, Wrightsville Beach, NC:



 

Swell conditions are shown as colors:  green == clean, red == choppy, blue == fair.  I'm constantly checking this app for the right conditions for a session.  Wouldn't it be nice to have some of this info on the lock screen in the Today view?  

Common Code
The first step in adding a Today extension is to extract common code used by the main application and the extension into a framework.  Adding a framework is done by adding a new target to your existing application.  To add a framework in Xcode, select File -> New -> Target from the top menu.  The following popup menu should display:



Select Framework & Library -> Cocoa Touch Framework.  The following dialog should appear:


For the Product Name field, enter SurfcastServices. In the navigation section in Xcode, you should now see a new folder with the title SurfcastServices. Now, you can add classes for performing activities common to both the main application and the extension.  In this case, I've extracted classes that perform REST service calls to Swell Info as well as a few domain classes to contain serialized JSON data. I won't go into details of these classes other than to say the REST services are called using Alamofire (https://github.com/Alamofire/Alamofire), a Swift based networking framework written by the creator of AFNetworking. Expect a blog post on Alamofire soon. Here is a screenshot of the SurfcastServices folder once these common classes have been added:

Today Extension
Now that our framework is in place, it's time to add our Today extension.  Similar to how the framework was added, an app extension is added via a new target.  To add this new target, select File -> New -> Target from the top menu.  This time, select Application Extension -> Today Extension.  Below is how this selection should appear:

Clicking Next will present the following dialog:

This time for Product Name, enter Surf Cast.  This name will be used as the name of the app in the Today view.  You may get a follow-up dialog asking you to activate this scheme.  Make sure you select Activate.

If all goes well, you should now see the following folder in Xcode:



Two files have been added: TodayViewController.swift and MainInterface.storyboard.  As you may have guessed, TodayViewController.swift is the backing controller for the MainInterface.storyboard.  Let's take a look at TodayViewController.swift.

import UIKit
import NotificationCenter

class TodayViewController: UIViewController, NCWidgetProviding {
        
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view from its nib.
    }
    
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
    
    func widgetPerformUpdateWithCompletionHandler(
completionHandler: ((NCUpdateResult) -> Void)!) {
        // Perform any setup necessary in order to update the view.\

        // If an error is encountered, use NCUpdateResult.Failed
        // If there's no update required, use NCUpdateResult.NoData
        // If there's an update, use NCUpdateResult.NewData

        completionHandler(NCUpdateResult.NewData)
    } 
}

As you can see, not much is going on here.  TodayViewController extends UIViewController, so the familiar lifecycle methods, such as viewDidLoad, are available.  Also notice that TodayViewController also implements the NCWidgetProviding protocol.  This protocol provides the method widgetPerformUpdateWithCompletionHandler.  This method is called by the system, either in the background or in Notification Center, to allow the extension to updates its contents. All updates should be done asynchronously in this method.   When complete, this method should call the completionHandler block passing in the appropriate NCUpdateResult (.Failed if the update failed, .NoData if no new data was gathered, or .NewData if new data was gathered).  Later, we will add calls to our SurfcastServices framework classes to this method.

Now, open MainInterface.storyboard.  You should see the following empty storyboard:

Let's run our extension.  To do so, first set Surf Cast as the active scheme at the top of Xcode as follows:

Next, click the run button which should display a dialog similar to the following:


Select Today and click Run.  If all goes according to plan, the emulator should start and in the Notification pulldown, you should see your extension as follows:

There is your first Today Extension.  Since the UI is controlled by a storyboard, almost all of the UI controls for your extension are at your disposal.  For Surf Cast, I've added some images, labels, and outlets just like when creating a normal storyboard for the main application.  My new storyboard looks like the following:

As with other storyboards, it's best to use autolayout to define the appropriate constraints.  Most of the elements in this storyboard are standard UIImageVIew and UILabel components.  The black boxes, however, are subclasses of UIView called ForecastView.  This special subclass is passed a condition and swell size.  Given these two values, the view will create a color gradient representing the surf conditions (green==clean, red==choppy, blue==fair) and a numeric indicator of the swell size.  The implementation for this class isn't discussed here, but we'll see it in action a bit later.

 I've also added outlets to the backing view controller, TodayViewController, for all labels and views I want to update.  Here are the outlets now define at the top of TodayViewController:

@IBOutlet weak var surfLocation: UILabel!
    @IBOutlet weak var airTemp: UILabel!
    @IBOutlet weak var waterTemp: UILabel!
    @IBOutlet weak var swellInfo: UILabel!
    @IBOutlet weak var highTide: UILabel!
    @IBOutlet weak var lowTide: UILabel!
    @IBOutlet weak var todayAMView: ForecastView!
    @IBOutlet weak var todayPMView: ForecastView!
    @IBOutlet weak var tomorrowPMView: ForecastView!
    @IBOutlet weak var tomorrowAMView: ForecastView!

Now that the controls are wired up to the view controller, let's add the call to the framework SurfCastService class used to retrieve data from Swell Info.  I've added the following function to TodayViewController:

  func loadForecast(completion:(error:NSError?) -> Void){
        
        var service:SurfCastService = SurfCastService();
        var serviceError:NSError? = nil;
        
        service.getForecast("WB", completion: 
{ (forecast:SurfForecast, error:NSError?) -> Void in
            
            if error == nil{
                let df:NSDateFormatter = NSDateFormatter()
                df.dateStyle = .NoStyle
                df.timeStyle = .ShortStyle
                
                self.surfLocation.text = forecast.beachName
                self.airTemp.text = "\(forecast.airTemp)°"
                self.waterTemp.text = "\(forecast.waterTemp)°"
                self.swellInfo.text = "\(forecast.swellSize) ft @ \
(forecast.swellPeriod) sec"
                self.lowTide.text = df.stringFromDate(forecast.lowTide)
                self.highTide.text = df.stringFromDate(forecast.highTide)
                
                var todaysForecast:DayForecast = 
forecast.getTodaysForecast()!;
                var tomorrowsForecast:DayForecast = 
forecast.getTomorrowsForecast()!
                
                self.todayAMView.setForecast(todaysForecast.amCondition, 
size:todaysForecast.amSize)
                self.todayPMView.setForecast(todaysForecast.pmCondition, 
size:todaysForecast.pmSize)
                self.tomorrowAMView.setForecast(
tomorrowsForecast.amCondition,
size:tomorrowsForecast.amSize)
                self.tomorrowPMView.setForecast(
tomorrowsForecast.pmCondition, 
size:tomorrowsForecast.pmSize)
            }
            serviceError = error;
        })
        completion(error:serviceError)
    }


Note: make sure the SurfCastServices.framework is included in the Linked Frameworks and Libraries section of the Surf Cast target General properties. Those properties should look similar to the following:

The getForecast method returns a SurfForeCast instance for the given surf break, in this case Wrightsville Beach ("WB").  The values for this instance are then used to populate the labels and ForecastViews in the storyboard.  

To invoke this method, simply add a call to it in widgetPerformUpdateWithCompletionHandler as follows:

 func widgetPerformUpdateWithCompletionHandler(
completionHandler: ((NCUpdateResult) -> Void)!) {
        
        // Perform any setup necessary in order to update the view.


        // If an error is encountered, use NCUpdateResult.Failed
        // If there's no update required, use NCUpdateResult.NoData
        // If there's an update, use NCUpdateResult.NewData


         self.loadForecast(
            { (error:NSError?) -> Void in
                if error == nil{
                    completionHandler(NCUpdateResult.NewData)
                }
                else{
                 completionHandler(NCUpdateResult.Failed)
                }
            })
    }

Also, add the same call in viewDidLoad.  Now, if all goes well you should see the following now in the Today View of Notification Center:




One last thing...we want to be able to launch the full application from within our Today Extension.  Launching your application is done using the openURL:completionHandler: method  from within the TodayViewController.  However, before we can call this method, we must first register our application with a custom URL.  The URL is added to the info.plist in your main application.  Below are the entries required to add "surfcast://" as a URL for our application:

Now that the URL is registered, add the following a method to invoke the URL to TodayViewController:

@IBAction func handleTouch(sender: AnyObject) {
        let url:NSURL! = NSURL(string: "surfcast://")
        self.extensionContext?.openURL(url, completionHandler: nil)
    }

Almost there.  Next, something needs to be added to the Today View interface to invoke this new method.  This invocation will be done via a TapGestureRecognizer.  In the MainInterface.storyboard file, drag a TapGestureRecognizer from the Object Library to your storyboard as follows:

Next, we need to wire up the TapGestureRecognizer to our main view in our MainInterface.storyboard.  Right click on the View under Today View Controller, Control-drag a connection from the gestureRecognizers Outlet to the TapGestureRecognizer that was just added.  If done correctly, you should see the following:


Lastly, add an outlet from the TapGestureRecognizer to the handleTouch: method previously added to the TodayViewContoller.  Right click on the Tap Gesture Recognizer in MainInterface.storyboard, Control-drag a connection from the Selector under Sent Actions to the Today View Controller.  A popup should appear, allowing you to choose handleTouch:  If done correctly, you should see the following:

Now, when you run your Today Extension, tapping anywhere in the Surf Cast view should take you to the full application.

That was actually pretty easy!  With very little effort, you can extend the life and presence of your existing iOS applications.  The Today Extension is but one of several extension types provided in iOS 8.  The other extension types can be found on the Apple developer library document "App Extension Programming Guide".

What's Next
In an upcoming entry, we'll further extend our Surf Cast application to our wrist using WatchKit and Apple Watch.  




Share:

About The Author

Enterprise Java/Mobile Solutions Practice Manager
Scott is passionate about Enterprise Application development, mobile web, and native mobile development for iOS and Android.