Extending Your Current iOS Applications — Watch Complications

Published 11/28/2018 10:50 AM   |    Updated 12/03/2018 07:55 AM
This article originally appeared on June 7, 2016.
 
An earlier article discussed extending your existing 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:
 
 
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.
 

Existing application

 
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.
 

WatchKit app

 
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.
 

ComplicationController

 
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.
 

Timeline Configuration

 
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)
        }
    }
 

Timeline Population

 
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.
 
Current Data
 
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)
        }
    }
 
Past and Future Data
 
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)
        	}
    }
 

Update Scheduling

 
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.
 
Placeholder Template
 
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.
 

Is this answer helpful?