An iOS 9 Today Extension Widget Tutorial
Previous | Table of Contents | Next |
An Introduction to Extensions in iOS 9 | Creating an iOS 9 Photo Editing Extension |
Learn SwiftUI and take your iOS Development to the Next Level |
With the basic concepts of extensions covered in the previous chapter, this chapter will work step-by-step through the creation of an example iOS 9 extension widget that will appear within the Today view of the Notifications panel. In the process of creating the example app, key areas of the Today extension implementation process will be covered in detail.
About the Example Extension Widget
The purpose of the extension created in this tutorial is to display the longitude and latitude of the user’s current location within the Today view of the iOS Notification panel. The steps to achieve this will involve the addition of an extension target to an existing container app, the design of the widget user interface and the implementation of the code to obtain, display and update the appropriate location data.
As previously outlined, Apple states that the container app for an extension must itself perform some useful function in addition to serving as the delivery vehicle for an extension. In recognition of this requirement, the tutorial is intended to be implemented as an extension to the Location application created in the chapter of this book entitled An Example iOS 9 Location Application. The steps outlined in the chapter may still be followed, however, regardless of whether or not you have completed the location based chapter.
Creating the Example Project
If you previously completed the Location tutorial as outlined in the An Example iOS 9 Location Application chapter of this book, locate the project and load it into Xcode. If, on the other hand, you have yet to complete this tutorial, download the sample code for the examples in this book from the following URL, and locate and load the completed Location example project into Xcode:
http://www.ebookfrenzy.com/code/iOS9BookSamples.zip
Adding the Extension to the Project
With the Location project loaded into Xcode, the next step is to add an extension target to the project using one of the extension templates provided by Xcode. From the Xcode menu, select the File -> New -> Target… menu option. In the resulting panel, select the Application Extension category listed under iOS in the left hand panel, and the Today Extension template from the main panel as shown in Figure 85-1:
Figure 85-1
Click on the Next button and, on the options panel, enter MyLocation into the Product Name field, leaving the remaining settings unchanged from the default values provided by Xcode. Click on Finish to create the new extension target.
As soon as the target has been created, a new panel will appear requesting permission to activate the new scheme for the extension target. Every target within an Xcode project has associated with it a scheme which defines how that target is to be built. When an extension target is added to a project, Xcode automatically creates a corresponding scheme so that the extension can be built and run. Activate this scheme now by clicking on the Activate button in the request panel.
The Today extension can be tested using the default template settings simply by setting the extension scheme (MyLocation) as the active scheme from the Xcode toolbar as shown in Figure 85-2 , selecting a suitable device or simulator target and clicking on the run button.
Figure 85-2
When an extension is launched it must do so within the context of a host app. Xcode will therefore request that a host app be selected for the extension. Since this is a Today extension, select the Today application from the list of options (Figure 85-3) prior to clicking on the Run button.
Figure 85-3
The default user interface for the Today template consists of a single label displaying text which reads “Hello World”. Assuming a successful launch of the extension, the Today view will appear in the Notification panel with the widget displayed as illustrated in Figure 85-4:
Figure 85-4
If the widget does not appear in the view, it may need to be enabled. Scroll down the Today view and select the Edit button when it comes into view. On the edit screen, locate the MyLocation widget and tap the green + button located next to it to add it to the view before selecting Done:
Figure 85-5
Reviewing the Extension Files
With the extension added to the project it is worth taking time to gain familiarity with the files which have been added to the project. Within the project navigator panel a new folder will have been created entitled MyLocation. It is within this folder that all of the files associated with the extension are contained (Figure 85-6):
Figure 85-6
The files that make up a Today extension are as follows:
- TodayViewController.swift - Contains the source code for the View Controller representing the widget in the Today view.
- MainInterface.storyboard – The storyboard file containing the user interface of the widget as it will appear within the Today view.
- Info.plist – The information property list for the extension.
Learn SwiftUI and take your iOS Development to the Next Level |
Designing the Widget User Interface
The Today extension template provided the project with a simple storyboard layout containing a single label view. For the purposes of this tutorial, two labels will be required to display both the longitude and latitude of the user’s current location. Within the Xcode project navigator panel, locate and select the MyLocation -> MainInterface.storyboard file to load it into the Interface Builder environment.
Begin by selecting and deleting the “Hello World” label from the view leaving a clean canvas on which to work. By default, the template has configured the widget view with a height suitable to accommodate a single label. In order to fit two labels onto the widget this height property will need to be increased. Click on the gray background of the widget to select the view and display the Size Inspector. Within the inspector panel, change the Height property from 37 to 50 as shown in Figure 85-7:
Figure 85-7
With the height increased, drag and drop two Label views from the object palette onto the view canvas, stretch the labels to fill approximately half of the width of the widget view and use the Attributes Inspector panel to change the foreground colors of the labels to Light Text Color so that the layout resembles that of Figure 85-8:
Figure 85-8
If the extension were to be launched on a device or simulator at this point, the labels would not be visible. The reason for this is that the Today view relies on Auto Layout settings within the widget layout, or specific preferred content size settings in the view controller code to decide on the size at which the widget should appear. Since no Auto Layout constraints have been configured, and no code has been added to set the preferred content size, the widget content would appear at zero height.
Select the uppermost Label and display the Auto Layout Pin menu (Figure 85-9). Enable the “Spacing to nearest neighbor” constraints on the top and left edges of the label by enabling the red constraint bars and using the current values and with the Constrain to margins option disabled.
Figure 85-9
Drop down the menu on the Width constraint setting and select the Use current canvas value option. Once the spacing and width constraints have been set, click on the Add 3 Constraints button to implement the changes.
Select the bottom Label view and repeat the above steps, this time setting spacing constraints on the left, top and bottom edges of the view, once again using the current spacing values.
Run the extension and verify that the widget layout appears correctly within the Today view, returning to the storyboard file if necessary to make adjustments to the layout.
Once the layout work is complete, display the Assistant Editor panel and make sure that it is showing the source code contained in the TodayViewController.swift file. Ctrl-click on the upper Label view and drag the resulting line to a position beneath the class declaration line in the Assistance Editor panel. Release the line and, in the resulting connection dialog, establish an outlet connection named latitudeLabel. Repeat this sequence of steps for the bottom label, this time creating an outlet named longitudeLabel.
Setting the Preferred Content Size in Code
Although not necessary in this particular instance (since Auto Layout is being used to influence the height of the widget), it is worth noting that the height of a widget can be set in code using the setPreferredContentSize method of the extension view controller instance. For example, the following code changes the height of the widget to 200 before it is displayed to the user:
override func viewWillAppear(animated: Bool) { var currentSize: CGSize = self.preferredContentSize currentSize.height = 200.0 self.preferredContentSize = currentSize }
This technique is particularly useful when it is necessary to dynamically change the size of a widget at runtime. A widget might, for example, display some initial information to the user and provide a “More” button to display more detailed information. In this scenario the “More” button would simply change the preferred content size to make additional views visible.
Modifying the Widget View Controller
The view controller class for the Today extension widget now needs to be implemented such that it obtains the user’s current location and updates the labels in the widget accordingly. Select and edit the TodayViewController.swift file to import the CoreLocation Framework, declare an optional variable in which to store the current location and to create and initialize a location manager instance. Note also that the TodayViewController class declaration has been modified to indicate that it now implements the CLLocationManagerDelegate protocol:
import UIKit import NotificationCenter import CoreLocation class TodayViewController: UIViewController, NCWidgetProviding, CLLocationManagerDelegate { @IBOutlet weak var latitudeLabel: UILabel! @IBOutlet weak var longitudeLabel: UILabel! var locationManager: CLLocationManager = CLLocationManager() var currentLocation: CLLocation? override func viewDidLoad() { super.viewDidLoad() locationManager.desiredAccuracy = kCLLocationAccuracyBest locationManager.delegate = self locationManager.requestLocation() } . . . }
Learn SwiftUI and take your iOS Development to the Next Level |
func locationManager(manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { currentLocation = locations[0] } func locationManager(manager: CLLocationManager, didFailWithError error: NSError) { print(error.description) }
Next, a convenience function needs to be written to update the labels with the latest location data:
func performWidgetUpdate() { if currentLocation != nil { let latitudeText = String(format: "Lat: %.4f", currentLocation!.coordinate.latitude) let longitudeText = String(format: "Lon: %.4f", currentLocation!.coordinate.longitude) latitudeLabel.text = latitudeText longitudeLabel.text = longitudeText } }
This function verifies that the currentLocation optional variable contains a value and, if so, constructs String objects containing the current longitude and latitude values before displaying them on the corresponding widget labels.
The last modification to the widget view controller is to ensure that the performWidgetUpdate function is called at the appropriate times so that the user sees the latest location information each time the widget is displayed in the Today view. To make sure the function is called prior to the widget being displayed, the viewWillAppear method of the view controller needs to be overridden as follows:
override func viewWillAppear(animated: Bool) { performWidgetUpdate() }
From time to time, the system will take snapshots of the widget so that information can be presented quickly to the user when the Today view is displayed. To make sure that the latest information is displayed when the system takes snapshots of the widget, it is also necessary to update the widget within the widgetPerformUpdateWithCompletionHandler method, a template of which has been provided by Xcode in the TodayViewController.swift file. All that needs to be added to this method is a call to our performWidgetUpdate method:
func widgetPerformUpdateWithCompletionHandler(completionHandler: ((NCUpdateResult) -> Void)!) { performWidgetUpdate() completionHandler(NCUpdateResult.NewData) }
Testing the Extension
Compile and run the extension using the extension’s scheme and select the Today view as the host app when prompted. Once the Today view is visible, use the Edit button to enable the widget (if it is not already displayed), at which point it should appear as shown in Figure 85-10:
Figure 85-10
Opening the Containing App from the Extension
When developing extensions it may be useful to provide the user with the ability to open the containing application from within the extension. The MyLocation Today widget, for example, only displays a subset of the data available within the containing Location app.
Every extension has associated with it an extension context object, a reference to which can be accessed via the extensionContext property of the extension’s view controller instance. Among the methods available to be called on the extension context object is the openURL method which can be used to launch other, suitably configured applications.
In order for an application to be launchable using the openURL method, the Info.plist file for that application must define a custom URL scheme. This essentially declares the URL name by which the application is identified (represented by the CFBundleURLName key and typically set using a reverse domain name identifier such as com.ebookfrenzy.location) and the schemes it supports (via an array of string values assigned to the CFBundleURLSchemes key).
For the purposes of this example, the Info.plist file for the Location application will be modified to specify a CFBundleURLName value of com.ebookfrenzy.location and a URL scheme named location. Whilst this can be achieved using the Xcode property list editor, it is actually quicker in this instance to directly edit the XML source of the Info.plist file.
Learn SwiftUI and take your iOS Development to the Next Level |
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>CFBundleURLTypes</key> <array> <dict> <key>CFBundleURLName</key> <string>com.ebookfrenzy.location</string> <key>CFBundleURLSchemes</key> <array> <string>location</string> </array> </dict> </array> <key>CFBundleDisplayName</key> <string></string> <key>NSLocationUsageDescription</key> <string>The application uses this to display your location in the Today view</string> . . . </dict> </plist>
The user interface layout for the MyLocation widget now needs to be modified to include a Button view which, when selected, will open the Location application. Select the MainInterface.storyboard file to load it into Interface Builder and add a Button which reads “Open” positioned as shown in Figure 85-11:
Figure 85-11
Display the Assistant Editor and verify that it is listing the source code for the TodayViewController.swift file. Ctrl-click and drag from the newly added Button view to the location within the Swift file where you would like the action method to be placed and release the line. In the resulting panel, change the connection type to Action and name the action openApp before clicking on the Connect button.
Edit the openApp method to construct the URL (which is referenced by the CFBundleURLSchemes location string) and to call the openURL method of the extension context:
@IBAction func openApp(sender: AnyObject) { let url: NSURL? = NSURL(string: "location:")! if let appurl = url { self.extensionContext!.openURL(appurl, completionHandler: nil) } }
In order to test this new functionality it will be necessary to build and re-install both the Location containing app and the MyLocation extension. Begin by selecting the Location scheme from the Xcode toolbar and build and run the application on the target device or simulator. Next, change the scheme to MyLocation and build and run the extension using the Today view as the host app. Once the extension is visible in the Today view, touch the Open button to launch the Location container app.
Summary
The Today extension allows widgets to appear within the Today view of the iOS Notification panel. Today widgets are essentially view controllers with the user interface of the widget contained within a storyboard file. It is important when designing a widget to make sure that it is small and lightweight and that either Auto Layout constraints or preferred content size method calls are made to ensure that the widget is sized correctly within the Today view. The system will call the widgetPerformUpdateWithCompletionHandler delegate method of the extension view controller at regular intervals in an effort to ensure that recent data is available to be displayed next time the view appears. A widget may provide the option to launch another app from within the Today view using the openURL method of the extension context instance.
Learn SwiftUI and take your iOS Development to the Next Level |
Previous | Table of Contents | Next |
An Introduction to Extensions in iOS 9 | Creating an iOS 9 Photo Editing Extension |