Extending Your Current iOS Applications — Today Extension

Published 11/27/2018 03:32 PM   |    Updated 12/03/2018 08:13 AM
This article originally appeared on April 28, 2015.

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, the Today Extension, can be incorporated into an existing application, giving the application new life. Note: Swift will be the language used in our code samples.
:

Existing application


The existing application used in this article is a mock-up of my favorite surf forecasting application, Swellinfo. This application is used by surfers to view the surf conditions for the upcoming week. Conditions such as swell size, swell (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, North Carolina:
 
 
 

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 pop-up 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. 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 Swellinfo, 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. Here’s 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 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’ll 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 pull-down, you should see your extension as follows:
 
 
There’s 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 SurfCast, 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 to the view controller, let's add the call to the framework SurfCastService class used to retrieve data from Swellinfo. 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 SurfCast 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 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 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 the TapGestureRecognizer to our main view in our MainInterface.storyboard. Right-click on the View under Today View Controller and Control-drag a connection from the gestureRecognizers Outlet to the TapGestureRecognizer that was just added. If it was 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 and Control-drag a connection from the Selector under Sent Actions to the Today View Controller. A pop-up should appear, allowing you to choose handleTouch. If it was done correctly, you should see the following:
 
 
Now, when you run your Today Extension, tapping anywhere in the SurfCast 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."

Is this answer helpful?