iOS 8 - Widgets

  • Tutorial

With the release of iOS 8, developers have the opportunity to create their own widgets for the Today screen. So far, the API has not finally settled down, there is a Known Issue and many moments not described in the documentation. But if you still want to make your widget, then I ask for a cut (note, Swift is used in the examples).



Extensions


IOS introduced a new concept - extensions. Extensions allow you to make available some of the content and functionality outside the application.

The part of the system that supports extensions is called extension point. The following extension point is available for iOS:
  • Today (Notification Center) - quickly perform an action or get information through the Today screen in Notification Center
  • Share - share content with friends or in a feed on any site
  • Action - view or manage content within the context of another application
  • Photo Editing - edit photos or videos inside the Photos app
  • Storage Provider - select a document from the set of documents available to the current application
  • Custom Keyboard - replace your native iOS keyboard with yours for use in all applications

Extensions can only be distributed in the extension container, which is the bundle of a regular application. One container can contain several extensions.

It should be noted that extensions are a special type of binary files. This is not an application!
Unfortunately, extensions do not support App to App IPC (pipes, sockets, ...), and therefore you need to use the familiar [UIApplication openURL:] (now it does not work for extensions, see the Known Issue ) or, for example, the App Group .

Each extension runs in a separate process. Thus, the same extensions in the context of different applications are different processes, and you do not have to worry about synchronization problems.

Extension documentation is here.

Widgets


Widgets are called extensions that display information in the Notification Center on the Today screen and, therefore, are designed to show the information that is important at the moment. When a user opens Today, he expects that the information he is interested in will be instantly available.

The widget becomes available after the user installs the application containing the widget (now it happens that the widget does not install after the first launch of the application, after all, this is still beta). To add a widget, you need to open the Today screen in the Notification Center, click the Edit button and add the desired widget.

The connection between the container and the widget is through NotificationCenter.framework .

Essentially, a widget is a UIViewController that is familiar to any iOS programmer. Accordingly, when creating widgets, you can use previously accumulated knowledge. For example, if you need to perform some action before displaying the widget, you should override viewWillAppear, etc.

To make the widget always look up to date, iOS sometimes makes widgets snapshots. When the widget becomes visible again, the last snapshot is shown first, and only then the real widget window. In order for the widget to update its state before the snapshot, the NCWidgetProviding protocol is used .
protocol NCWidgetProviding : NSObjectProtocol {
// Called to allow the client to update its state prior to a snapshot being taken, or possibly other operations.// Clients should call the argument block when the work is complete, passing the appropriate 'NCUpdateResult'.@optional func widgetPerformUpdateWithCompletionHandler(completionHandler: ((NCUpdateResult) -> Void)!)
// Clients wishing to customize the default margin insets can return their preferred values.// Clients that choose not to implement this method will receive the default margin insets.@optional func widgetMarginInsetsForProposedMarginInsets(defaultMarginInsets: UIEdgeInsets) -> UIEdgeInsets
}

When a widgetPerformUpdateWithCompletionHandler is called on a widget, it must update its window and then call the completionHandler block with an argument equal to one of the following constants:
  • NCUpdateResultNewData - new content requires window updates
  • NCUpdateResultNoData - widget does not need updating
  • NCUpdateResultFailed - an error occurred during the update process

Since users are waiting for an instant reaction from Notification Center, and the system performs snapshots, the widget is simply obliged to store its previous state, that is, to cache the data it needs to work.

Notification Center determines the width of the widget, while the widget itself determines its height. To determine the height, the widget can use the Auto Layout or the preferedContentSize property of the UIViewController instance.
override func viewDidLoad() {
    super.viewDidLoad()     
    self.preferredContentSize.height = 350
 }

It turns out that there are the following requirements for widgets:
  • ensure that the content displayed is up to date
  • respond appropriately to user actions
  • consume as few resources as possible (iOS can kill a widget if it consumes a lot of memory)

Please note that the widget UI has the following limitations:
  • do not show keyboard
  • you cannot use controls that work with gestures (for example, UIDatePicker)
  • maps cannot be displayed at this time (now this item appears in Known Issue - Mapviews do not load tiles in widgets.)

Since the keyboard is not available, the user must be able to configure the widget in the container application.

Widget interaction


To update the information displayed by the widget, there is an NCWidgetController class .
An instance of this class has a single setHasContent: forWidgetWithBundleIdentifier: method, which sends a message to the widget stating that it should update the information.
Used as follows:
NCWidgetController.widgetController().setHasContent(true,
 forWidgetWithBundleIdentifier: "com.e-legion.Traffic.Widget")

This class can be used from the widget and container application.

Exchange data with container application


To communicate with the container, the NSExtensionContext object is used, accessible via the extensionContext property owned by the UIViewController.
classNSExtensionContext: NSObject {
    // The list of input NSExtensionItems associated with the context. If the context has no input items, this array will be empty.var inputItems: AnyObject[]! { get }
    // Signals the host to complete the app extension request with the supplied result items. The completion handler optionally contains any work which the extension may need to perform after the request has been completed, as a background-priority task. The `expired` parameter will be YES if the system decides to prematurely terminate a previous non-expiration invocation of the completionHandler. Note: calling this method will eventually dismiss the associated view controller.
    func completeRequestReturningItems(items: AnyObject[]!, completionHandler: ((Bool) -> Void)!)
    // Signals the host to cancel the app extension request, with the supplied error, which should be non-nil. The userInfo of the NSError will contain a key NSExtensionItemsAndErrorsKey which will have as its value a dictionary of NSExtensionItems and associated NSError instances.
    func cancelRequestWithError(error: NSError!)
    // Asks the host to open an URL on the extension's behalf
    func openURL(URL: NSURL!, completionHandler: ((Bool) -> Void)!)
}

That is, to open the container application from the widget, the container must register the scheme (for example, "traffic: //"), and you need to add to the widget code
self.extensionContext.openURL(NSURL(string: "traffic://"), completionHandler: nil)
.
By default, iOS security prevents the exchange of data between the container application and the extension. To enable data exchange, you need to add the container target and extensions to the same App Group .

As a result, the entitlements file will have the following contents:
<?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"><plistversion="1.0"><dict><key>com.apple.security.application-groups</key><array><string>group.96GT47C53G.traffic</string></array></dict></plist>

Pay attention to 96GT47C53G. This is Development Team ID. You can see it in your profile . To run on the simulator, you can use any value, for example, group.traffic, ...

Now, using the containerURLForSecurityApplicationGroupIdentifier method, you can get the path to the shared folder and store application-specific data there.
NSFileManager.defaultManager().containerURLForSecurityApplicationGroupIdentifier("group.96GT47C53G.traffic")

Example


So, let's try to create a widget that will show a map with traffic jams. We will receive traffic information in the form of a picture using the Yandex.Maps API .

An example of a link to get a picture: http://static-maps.yandex.ru/1.x/?ll=30.35,59.9690273&spn=0.01,0.2&size=300,250&l=map,trf
Actually, lat and lon are the center of the map , and spn is the extent of the map display area in degrees.

The source code for the project is available on GitHub .

Create a container application


The task of the container is to give the user the ability to configure the widget, namely to select an area on the map. Thus, the application will contain a MapView for selecting an area and a “Set frame” button, which will transfer this area to the widget.


The application should enable the user to select lat, lon, spn parameters and pass them to the widget. The following code does this:
@IBAction func updateWidgetButtonTapped(sender : AnyObject) {
    var dict : NSMutableDictionary = NSMutableDictionary()
    dict["spn"] = self.mapView.region.span.latitudeDelta
    dict["lat"] = self.mapView.region.center.latitude
    dict["lon"] = self.mapView.region.center.longitude
    var dictUrl : NSURL = NSFileManager.defaultManager().containerURLForSecurityApplicationGroupIdentifier("group.96GT47C53G.traffic").URLByAppendingPathComponent("settings.dict")
    dict.writeToFile(dictUrl.path, atomically: true)
    NCWidgetController.widgetController().setHasContent(true, forWidgetWithBundleIdentifier: "com.e-legion.Traffic.Widget")
}

Add widget


Adding a widget comes down to adding a new target.
Click File-> New-> Target and select iOS-> Application Extension-> Today Extension.



Now a stub for the widget has been added to the project. But if you try to use your widget now, then nothing will work for you. The widget will crash. To fix this, add the following method to TodayViewController:
init(coder aDecoder: NSCoder!) {
    super.init(coder: aDecoder)
}

Pay attention to the Info.plist file from the template. It contains the NSExtension key, which defines some widget parameters.
<key>NSExtension</key><dict><key>NSExtensionMainStoryboard</key><string>MainInterface</string><key>NSExtensionPointIdentifier</key><string>com.apple.widget-extension</string></dict>

NSExtensionMainStoryboard stores the name of the storyboard that stores the controller for the widget. The controller can be specified explicitly by replacing the NSExtensionMainStoryboard key with NSExtensionPrincipalClass and using the name of the controller as the value.

Each widget has a slight left shift. If you want to get rid of it, then you need to return the desired UIEdgeInsets in the widgetMarginInsetsForProposedMarginInsets method.
 func widgetMarginInsetsForProposedMarginInsets(defaultMarginInsets: UIEdgeInsets) -> UIEdgeInsets {
    returnUIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
 }

The widget code is quite simple. There is an updateMap method that updates the map. Updating occurs when the widget starts displaying (viewWillLoad), clicking on the button and calling widgetPerformUpdateWithCompletionHandler. The widget receives data about the displayed area through containerURLForSecurityApplicationGroupIdentifier .

Conclusion


Widgets are a very cool thing, but so far everything is damp. Sometimes crashes occur, sometimes the widget is not visible, etc. But most of all there is a lack of documentation. Soon all this will be fixed and it will be possible to enrich your applications with new functionality, but for now you can play around with what is.

Also popular now: