Smarter ideas worth writing about.

Extending Your Current iOS Applications - Watch Complications

In a previous article, we discussed extending your existing app’s functionality through the use of Today Extensions.  With the release of the Apple Watch and Watch OS2 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, 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 an mockup of my favorite surf forecasting application, Swell Info.  This application is used by surfers to view the surf conditions for the upcoming week.  Conditions such as swell size, swell condition (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, NC:

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 will be building:

In this image, you’ll see we are displaying the current wave condition via an image on the left side of the first row. The image  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) will not 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 give surf break for a given date.  Here is 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 for us to return a CLKComplicationTimelineEntry that you want displayed now.  In our implementation, we call a method on SurfCastService to get the current conditions.  Here is 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 Watch OS 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 will not support backward Time Travel, so here is 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 at array of CLKComplicationTimelineEntry representing upcoming surf forecasts.  Here is 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 is 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 is an example of what we will be rendering:

The method getPlaceholderTemplateForComplication:withHandler is called to create this template. Here is 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 will need to customize the watch face to include the new Complication.  To send a force touch to the emulator, you’ll need change the Force Touch Pressure to Deep Press using the key combination <Shift>-<Command>-2.  <Shift>-<Command>-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:


     
 (Fair and a bit bigger)  (Fair and a still a bit bigger)
 (Clean and close to overhead!)

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.  

What’s Next

In a future entry, we’ll implement the full Apple Watch app that launches when tapping on the Complication.

Share:

About The Author

Enterprise Java/Mobile Solutions Practice Manager
Scott is passionate about Enterprise Application development, mobile web, and native mobile development for iOS and Android.