The Not-So-Complicated Complications

Last week Apple showed us the new ClockKit framework that we can use to integrate our existing Watch app into the standard clock face. Even though the majority of us would prefer creating fully custom clock faces from scratch, complications are quite a powerful piece of UX and you should try to add one to your app.

Due to the buggy nature of the Xcode 7 beta, I would suggest playing with complications in a new project, at least until problems with watchOS 2 migration are fixed. You can start by simply creating new project, and selecting iOS App with WatchKit App as the template. Please make sure to select the Include Complication checkbox as enabling complications manually can be cumbersome and problematic (if you feel brave, check out the “Tips section at the bottom of this post).

After getting the project ready, please take a while before compiling and running the app, because strange things can happen if we don’t pass the data first.

What’s the source of the data for the complication?

CLKComplicationDataSource, easy as that. This protocol specifies 9 methods that your class has to implement in order to provide data to the complication on the Watch face. Alongside those nine, we will be creating a couple of helper functions that will help us implement the rest. Let’s start with the first one that we have to implement:

/// When your extension is installed, this method will be called once per supported complication, and the results will be cached.
/// If you pass back nil, we will use the default placeholder template (which is a combination of your icon and app name).
func getPlaceholderTemplateForComplication(complication: CLKComplication, withHandler handler: (CLKComplicationTemplate?) -> Void)

Please note the “when your extension is installed” part. That’s right: if you ever want to change the placeholder template, you’ll have to reinstall the app in the simulator. So let’s implement this method using a helper function that will be responsible for the generation of the templates everywhere in the ComplicationController.swift:

func getPlaceholderTemplateForComplication(complication: CLKComplication, withHandler handler: (CLKComplicationTemplate?) -> Void)) { 
    handler(defaultTemplate()) 
}

func defaultTemplate() -> CLKComplicationTemplateModularLargeStandardBody { 
    let placeholder = CLKComplicationTemplateModularLargeStandardBody() placeholder.headerTextProvider = CLKSimpleTextProvider(text: "Transit")
    placeholder.body1TextProvider = CLKSimpleTextProvider(text: "Next bus in:") 
    placeholder.body2TextProvider = CLKSimpleTextProvider(text: "") 
    return placeholder 
}

As you can see from the snippet, we will be building a complication displaying public transit schedules. For this purpose, I have chosen CLKComplicationTemplateModularLargeStandardBody, one of the biggest complication templates available, but there’s a lot more to choose from, check out the CLKComplicationTemplate.h header for a list of available template styles. Please note, though, that most of the templates work only with select watch faces. If we run our app at this stage, after the install process finishes we should be able to select our complication from the watch face customisation screen: Pretty, isn’t it? But I guess the complication would be much more useful if it showed some real data instead of just a pretty placeholder. The method responsible for populating the current timeline entry is called thusly:

/// Provide the entry that should currently be displayed. 
/// If you pass back nil, we will conclude you have no content loaded and will stop talking to you until you next call -reloadTimelineForComplication:.
func getCurrentTimelineEntryForComplication(complication: CLKComplication, withHandler handler: (CLKComplicationTimelineEntry?) -> Void)

As you can see, we are given the CLKComplication object and a handler that we have to execute with our timeline entry as an argument. First off, we will start with the actual data: usually you will get the data to display from somewhere outside the complication class, but let’s imagine that we live in a utopian city where public transport has a fixed schedule and is always on time:

let timeTable = [7, 18, 29, 32, 38, 49, 59]

By using this fixed data, we can easily calculate the upcoming arrival time using a helper function:

func getNextBusArrivalDate(fromDate date: NSDate) -> NSDate { 
    let calendar = NSCalendar.currentCalendar() 
    let components = calendar.components([.Minute, .Hour], fromDate: date) 
    if let nextBusArrivalMinute = timeTable.filter({ return $0 > components.minute }).first { 
        return calendar.dateBySettingHour(components.hour, minute: nextBusArrivalMinute, second: 0, ofDate: date, options: .MatchFirst)! 
    } else { 
        let date = calendar.dateBySettingHour(components.hour, minute: timeTable.first!, second: 0, ofDate: date, options: .MatchFirst)! return date.dateByAddingTimeInterval(NSTimeInterval(3600)) 
    } 
}

Now we can finally implement the getCurrentTimelineEntryForComplication method:

func getCurrentTimelineEntryForComplication(complication: CLKComplication, withHandler handler: ((CLKComplicationTimelineEntry?) -> Void)) {
    let template = defaultTemplate()
    let currentDate = NSDate()

    template.body1TextProvider = CLKSimpleTextProvider(text: "Next bus in:")
    template.body2TextProvider = CLKRelativeDateTextProvider(date: getNextBusArrivalDate(fromDate: currentDate), style: .Offset, units: .Minute)

    let entry = CLKComplicationTimelineEntry(date: currentDate, complicationTemplate: template)

    handler(entry)
}

Finaly! Now I’ll never be late to work!

And what if I told you Time Travel is possible?

It won’t be a surprise to anyone that even with our life-changing complication, citizens of The Only City in the World Where Public Transport Works As Intended still somehow managed to miss their buses. They also started complaining that our complication doesn’t allow them to simply ignore the upcoming bus and take the next one. Well, let’s fix that with Time Travel! And no, we won’t need flux capacitors.

Apple allows us to fill the complication with both future and past data and to easily scroll through it using the Digital Crown. To do so, we will need to implement 3 more methods from the protocol, refactoring some code we already have.

First off, we will specify that our complication supports Time Travel in both directions by adding the getSupportedTimeTravelDirectionsForComplication method:

func getSupportedTimeTravelDirectionsForComplication(complication: CLKComplication, withHandler handler:    (CLKComplicationTimeTravelDirections) -> Void) { 
    handler([.Forward, .Backward]) 
}

This uses the new OptionSet protocol which allows us to do bitwise operations much easier than before. Instead of ANDs and ORs we can simply pass an array of options and the system will take care of the rest. As we will have to provide data for the arbitrary date, let’s create a new helper method that will create the timeline entry for us based on the passed date:

func timelineEntryForDate(date: NSDate, timeTravel: Bool = false) -> CLKComplicationTimelineEntry { let template = defaultTemplate()
    template.body1TextProvider = CLKSimpleTextProvider(text: "Next bus in:")
    template.body2TextProvider = CLKRelativeDateTextProvider(date: getNextBusArrivalDate(fromDate: date), style: .Offset, units: .Minute)

    return CLKComplicationTimelineEntry(date: date, complicationTemplate: template)
}

Having that, we can refactor the getCurrentTimelineEntryForComplication method:

func getCurrentTimelineEntryForComplication(complication: CLKComplication, withHandler handler: ((CLKComplicationTimelineEntry?) -> Void)) { 
    handler(timelineEntryForDate(NSDate())) 
}

Because we will traverse the timeline in both directions, we should add another helper method to get the last bus arrival date before the given date: we can easily do that by slightly changing the getNextBusArrivalDate

func getLastBusArrivalDate(fromDate date: NSDate) -> NSDate { 
    let calendar = NSCalendar.currentCalendar() let components = calendar.components([.Minute, .Hour], fromDate: date) 
    if let lastBusArrivalMinute = timeTable.reverse().filter({ return $0 < components.minute }).first { 
        return calendar.dateBySettingHour(components.hour, minute: lastBusArrivalMinute, second: 0, ofDate: date, options: .MatchFirst)! 
    } else { 
        let date = calendar.dateBySettingHour(components.hour, minute: timeTable.last!, second: 0, ofDate: date, options: .MatchFirst)! 
        return date.dateByAddingTimeInterval(NSTimeInterval(-3600)) 
    } 
}

And now we can finally implement the time traversing methods:

func getTimelineEntriesForComplication(complication: CLKComplication, beforeDate date: NSDate, limit: Int, withHandler handler: (([CLKComplicationTimelineEntry]?) -> Void)) {
        var entries: [CLKComplicationTimelineEntry] = []
        var prevDate = date
        for _ in 0..<limit {
            prevDate = getLastBusArrivalDate(fromDate: prevDate)
            let entry = timelineEntryForDate(prevDate)
            entries.append(entry)
        }
        handler(entries)
    }

func getTimelineEntriesForComplication(complication: CLKComplication, afterDate date: NSDate, limit: Int, withHandler handler: (([CLKComplicationTimelineEntry]?) -> Void)) {
        var entries: [CLKComplicationTimelineEntry] = []
        var nextDate = date
        for _ in 0..<limit {
            nextDate = getNextBusArrivalDate(fromDate: nextDate)
            let entry = timelineEntryForDate(nextDate)
            entries.append(entry)
        }
        handler(entries)
}

After reinstall of the app we can use the Digital Crown to traverse the timeline:

As you can see, implementing both a simple Complication and Time Travel is not as hard as it may have seemed; nevertheless, there’s a chance that at some point in time you will yell:

My complication doesn't work!

Well, I could just say: get used to it, as right now most developers are encountering similar problems. As I mentioned earlier, the complication support is far from flawless and can quite often simply decide to stop working altogether. Here’s a handful of tips that I either got from Apple engineers at WWDC lab sessions or figured out by myself:

  • If you're using Swift, make sure that the Data Source Class field in the project setting has the $(PRODUCT_MODULE_NAME) suffix
  • If you can't even get your template to show up, use the Watch app on the iOS simulator to uninstall and reinstall your Watch app apply the same process if your complication won’t update
  • Sometimes the only way to get the complication running again is resetting the contents and settings of both simulators
  • Make sure you're using the correct template with the correct watch face. Some of the templates work only with a select few or even just one of the many watch faces you can choose from.
  • The easiest way to force a reload of the complication data (and to trigger the debugger breakpoints) is to Force Touch the watch face, tap "Customize", change the selected complication to any other one, swipe to the left, swipe back, and select the complication you want.
  • The complication target that gets created automatically unfortunately does nothing at this moment.

Improving Notification Center
Cross-Platform Libraries