Tech tutorials Extending Your Current iOS Applications — Watch Complications
By Insight Editor / 7 Jun 2016 , Updated on 21 Aug 2019 / Topics: Application development
By Insight Editor / 7 Jun 2016 , Updated on 21 Aug 2019 / Topics: Application development
An earlier article discussed extending your existing iOS app’s functionality through the use of Today Extensions. With the release of the Apple Watch® and watchOS 2 came yet another avenue for extending the presence of your existing iOS app: complications.
A complication is a widget that can be added to your selected watch face. Each of the red squares in the image below highlight different types of complications available for the Modular watch face:
This JSON structure contains a lot of information — certainly more than is being displayed by the CSWP. The trick, of course, is in figuring out exactly “what” is “where” for purposes of building a paging system.
Thankfully, the CSWP provides us with a relatively easy mechanism for getting at the search result data we care about within the JSON object that’s returned from the call to client.svc. When a Control display template is invoked by the CSWP, it’s passed a context object.
The ctx context object contains a number of useful methods, properties and subordinate objects we can leverage in our attempts to manipulate search results and calculate paging information. We can use ctx to interact directly with the CSWP (which is accessible through the ClientControl property), as well as obtain the search results themselves (and information about them) through the ListData property.
Complications fall into one of five “families”: Modular Small, Modular Large, Utilitarian Small, Utilitarian Large and Circular Small. This article describes building a Modular Large complication that also supports Time Travel to display time-based information for your app. In the above image, the large rectangle in the middle of the screen is a Modular Large complication from the Dark Sky app.
As with the Today Extension, our watch complication will be based on 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:
Our goal for this project is to add a complication to show the current surf conditions, as well as forecast conditions for our default surf break. The following image represents the complication we’ll be building:
In this image, you’ll see we’re displaying the current wave condition via an image on the left side of the first row. represents choppy conditions, represents fair conditions, and represents clean conditions.
Next to the image comes the wave height, air temperature and water temperature. Line two represents wind direction and speed, while line three lists low- and high-tide time values for that day.
Before we can write our complication, we must add a WatchKit app target to our existing project. In the Targets window of your XCode project, click the + button at the bottom left. When the subsequent dialog appears, choose Application > WatchKit App under the watchOS entry as follows:
After clicking "Next," a dialog similar to the following will appear:
For our sample, select only "Include Complication." The other WatchKit options (Notification Scene and Glance Scene) won’t be discussed in this article.
After clicking "Finish," your project should now include the following two additional targets:
You’ll also notice the following new entries in the Project Navigator:
The controller ComplicationController.swift is where we’ll add all of our code.
Upon opening this controller, the first thing you might notice is that it implements the CLKComplicationDataSource protocol. For this sample app, we’ll look at the important protocol methods based on the following logical groupings: Timeline Configuration, Timeline Population and Update Scheduling.
Our app will support Time Travel but only in the forward direction since most surfers don’t care about yesterday’s surf conditions (“You should have been here yesterday!”). To indicate that our app will support forward Time Travel, implement getSupportedTimeTravelDirectionsForComplication:withHandler as follows:
func getPrivacyBehaviorForComplication( complication: CLKComplication, withHandler handler: (CLKComplicationPrivacyBehavior) -> Void) { switch complication.family { case .ModularLarge: handler(.ShowOnLockScreen) default: handler(.HideOnLockScreen) } }
Now that we’ve indicated the date ranges and lock screen behavior, we need to implement three methods that provide past, current and future data for the complication. Past and future data are needed for supporting Time Travel, a feature that allows you to display date-based information for past and future events in your complication.
First, let’s review the model object that represents surf conditions for a surf break for a given date. Here’s our HourForecast struct:
public struct HourForecast{
var date:NSDate
var surfCondition:SurfCondition
var size:String
var windCondition:String
var highTide:NSDate
var lowTide:NSDate
var airTemp:Int
var waterTemp:Int
}
SurfCondition is an enum with the following definition:
enum SurfCondition { case Choppy case Fair case Clean }
Our SurfCastService returns instances of HourForecast. However, the methods for populating the complication require CLKComplicationTimelineEntry objects. The following helper method in our ComplicationController maps HourForecast instances to CLKComplicationTimelineEntry instances:
func createTimeLineEntry(forecast: HourForecast) -> CLKComplicationTimelineEntry { let dateFormatter = NSDateFormatter() dateFormatter.dateFormat = "h:mma" let template = CLKComplicationTemplateModularLargeStandardBody() var wave:UIImage? = nil switch forecast.surfCondition { case .Choppy: wave = UIImage(named: "Complication/choppyImage") break case .Fair: wave = UIImage(named: "Complication/fairImage") break default: wave = UIImage(named: "Complication/cleanImage") } let headerText = "\(forecast.size) ft \ (forecast.airTemp)°/\(forecast.waterTemp)°" template.headerImageProvider = CLKImageProvider(onePieceImage: wave!) template.headerTextProvider = CLKSimpleTextProvider(text: headerText) template.body1TextProvider = CLKSimpleTextProvider(text: forecast.windCondition) template.body2TextProvider = CLKSimpleTextProvider(text: "L:\(dateFormatter.stringFromDate(forecast.lowTide)) H:\(dateFormatter.stringFromDate(forecast.highTide))") let entry = CLKComplicationTimelineEntry(date: forecast.date, complicationTemplate: template) return(entry) }
There’s a fair amount of work going on here. However, it all boils down to this line:
let entry = CLKComplicationTimelineEntry(date: forecast.date, complicationTemplate: template)
The CLKComplicationTimelineEntry object needs an NSDate and a subclass of CLKComplicationTemplate — in our case, a CLKComplicationTemplateModularLargeStandardBody. The rest of the code simply populates the template with values from the HourForecast instance.
The first timeline population methods we need to implement is getCurrentTimelineEntryForComplication:withHandler.
This method asks us to return a CLKComplicationTimelineEntry that we want displayed now. In our implementation, we call a method on SurfCastService to get the current conditions. Here’s the full implementation:
func getCurrentTimelineEntryForComplication( complication: CLKComplication, withHandler handler: ((CLKComplicationTimelineEntry?) -> Void)) { // Call the handler with the current timeline entry if complication.family == .ModularLarge { surfService.getHourForecast( self.surfBreak, forDate: NSDate(), completion: { (forecast, error) in if let hourForecast = forecast { handler(self.createTimeLineEntry(hourForecast)) } else { handler(nil) } }) } else { handler(nil) } }
Now, we have to provide watchOS with CLKComplicationTimelineEntry objects for past and future dates by implementing the methods getTimelineEntriesForComplication:beforeDate:limit:withHandler and getTimelineEntriesForComplication:afterDate:limit:withHandler.
As we said before, our complication won’t support backward Time Travel, so here’s the implementation for getTimelineEntriesForComplication:beforeDate:limit:withHandler:
func getCurrentTimelineEntryForComplication( complication: CLKComplication, withHandler handler: ((CLKComplicationTimelineEntry?) -> Void)) { // Call the handler with the current timeline entry if complication.family == .ModularLarge { surfService.getHourForecast( self.surfBreak, forDate: NSDate(), completion: { (forecast, error) in if let hourForecast = forecast { handler(self.createTimeLineEntry(hourForecast)) } else { handler(nil) } }) } else { handler(nil) } }
However, for getTimelineEntriesForComplication:afterDate:limit:withHandler, we do want to return an array of CLKComplicationTimelineEntry representing upcoming surf forecasts. Here’s that implementation:
func getTimelineEntriesForComplication( complication: CLKComplication, afterDate date: NSDate, limit: Int, withHandler handler: (([CLKComplicationTimelineEntry]?) -> Void)) { var timeLineEntryArray = [CLKComplicationTimelineEntry]() surfService.getHourForecast(self.surfBreak, fromDate: date) { (forecasts, error) in for forecast in forecasts{ let entry = self.createTimeLineEntry(forecast) timeLineEntryArray.append(entry) } handler(timeLineEntryArray) } }
Our final group of methods center around scheduling updates for timeline entries. The first, getNextRequestedUpdateDateWithHandler, returns an NSDate object that indicates when the next update should be take place.
func getNextRequestedUpdateDateWithHandler( handler: (NSDate?) -> Void) { let nextDate = NSDate(timeIntervalSinceNow: 7200) handler(nextDate); }
Our next scheduled update should be in about two hours.
Once an update is scheduled, the method requestedUpdateDidBegin will be called whenever an update begins so that we can reload our timeline. Here’s our implementation:
func requestedUpdateDidBegin() { let server = CLKComplicationServer.sharedInstance() for complication in server.activeComplications! { server.reloadTimelineForComplication(complication) } }
Here, we instruct the complication server to reload the timeline.
The final method we implement is one to create a template to render when customizing your watch face. Here’s an example of what we’ll be rendering:
The method getPlaceholderTemplateForComplication:withHandler is called to create this template. Here’s our implementation:
func getPlaceholderTemplateForComplication( complication: CLKComplication, withHandler handler: (CLKComplicationTemplate?) -> Void) { switch complication.family { case .ModularLarge: let template = CLKComplicationTemplateModularLargeStandardBody() let wave = UIImage(named: "Complication/cleanImage") template.headerImageProvider = CLKImageProvider(onePieceImage: wave!) template.headerTextProvider = CLKSimpleTextProvider(text: "5+ ft 72°/68°") template.body1TextProvider = CLKSimpleTextProvider(text: "N 3 mph") template.body2TextProvider = CLKSimpleTextProvider(text: "L:12:26AM H:12:17AM") handler(template) default: handler(nil) } }
That’s it. Now, we can test our app by launching the following WatchKit complication target in XCode:
Once the Apple Watch emulator launches, you’ll need to customize the watch face to include the new complication. To send a force touch to the emulator, you’ll need to change the Force Touch pressure to Deep Press using the key combination - -2. - -1 will return Force Touch pressure to Shallow Press.
Once you select the SurfCast complication, you should see the Apple Watch emulator display the following screen:
(Current conditions: Choppy)
As you turn the digital crown ahead, you should see the forecast change. Below is a progression of forecast throughout the day:
Again, we were able to quickly and easily enhance our existing app by extending it to the Apple Watch by simply reusing the services from our existing app and adding a few classes.