Home > Articles > Programming

Creating Custom Apple Watch Complications, Part 2

  • Print
  • + Share This
Wei-Meng Lee, author of Learning WatchKit Programming: A Hands-On Guide to Creating Apple Watch Applications, continues his discussion of watch complications and how you can implement Time Travel to display past and future events on the Apple Watch.
From the author of

Creating Custom Apple Watch Complications, Part 2

In Part 1 of this series on creating custom watch complications on the Apple Watch, we examined the different types of complication families that an application can support, and created a project that allows a user to select custom complications on the Apple Watch. In this article, we'll continue working with the project from Part 1, populating the complications with some real data. You'll also learn how to implement Time Travel for your complications.

Populating the Complications with Real Data

Now that you can display the template complication when the user selects your complications, it's time to display some real data, so you can see how complications really work.

In the ComplicationController.swift file (from the Movies project created in Part 1), add the following statements in bold:

import ClockKit
//---multipliers to convert to seconds---
let HOUR: NSTimeInterval = 60 * 60
let MINUTE: NSTimeInterval = 60

struct Movie {
    var movieName: String
    var runningTime: NSTimeInterval  // in seconds
    var runningDate: NSDate
    var rating:Float                 // 1 to 10
}

class ComplicationController: NSObject, CLKComplicationDataSource {

    //---in real life, the movies can be loaded from a web service or file
    // system---
    let movies = [
        Movie(movieName: "Terminator 2: Judgement Day",
            runningTime: 137 * MINUTE,
            runningDate: NSDate(timeIntervalSinceNow: -360 * MINUTE),
            rating:8),
        Movie(movieName: "World War Z ",
            runningTime: 116 * MINUTE,
            runningDate: NSDate(timeIntervalSinceNow: -120 * MINUTE),
            rating:7),
        Movie(movieName: "Secondhand Lions",
            runningTime: 90 * MINUTE,
            runningDate: NSDate(timeIntervalSinceNow: 10 * MINUTE),
            rating:8),
        Movie(movieName: "The Dark Knight ",
            runningTime: 152 * MINUTE,
            runningDate: NSDate(timeIntervalSinceNow: 120 * MINUTE),
            rating:9),
        Movie(movieName: "The Prestige",
            runningTime: 130 * MINUTE,
            runningDate: NSDate(timeIntervalSinceNow: 360 * MINUTE),
            rating:8),
    ]

These statements create a structure to store movie information, and the movies array stores a list of movies. Figure 1 shows the timeline of each movie and their corresponding play times.

Figure 1 Visualization of the timelines of the various movies.

The next method to implement is the getCurrentTimelineEntryForComplication:withHandler: method. This method is called to display the timeline entry you want to display now. Add the following statements in bold to the ComplicationController.swift file:

func getCurrentTimelineEntryForComplication(
    complication: CLKComplication,
    withHandler handler: ((CLKComplicationTimelineEntry?) -> Void)) {
// Call the handler with the current timeline entry

    // handler(nil)
    for movie in movies {
        //---display the movie that is currently playing or the next one
        // that is coming up---
        if (abs(movie.runningDate.timeIntervalSinceNow) <
            movie.runningTime) {
            switch complication.family {
            case .ModularSmall:
                let modularSmallTemplate =
                    CLKComplicationTemplateModularSmallRingText()
                modularSmallTemplate.textProvider =
                    CLKSimpleTextProvider(text: "\(Int(movie.rating))")
                modularSmallTemplate.fillFraction = movie.rating / 10
                modularSmallTemplate.ringStyle =
                    CLKComplicationRingStyle.Closed
                let entry = CLKComplicationTimelineEntry(
                    date:NSDate(),
                    complicationTemplate: modularSmallTemplate)
                handler(entry)
            case .ModularLarge:
                let modularLargeTemplate =
                    CLKComplicationTemplateModularLargeStandardBody()
                modularLargeTemplate.headerTextProvider =
                    CLKTimeIntervalTextProvider(
                        startDate: movie.runningDate,
                        endDate: NSDate(
                            timeInterval: movie.runningTime,
                            sinceDate: movie.runningDate))
                modularLargeTemplate.body1TextProvider =
                    CLKSimpleTextProvider(
                        text: movie.movieName,
                        shortText: movie.movieName)
                modularLargeTemplate.body2TextProvider =
                    CLKSimpleTextProvider(
                        text: "\(movie.runningTime / MINUTE) mins",
                        shortText: nil)
                let entry = CLKComplicationTimelineEntry(
                    date:NSDate(),
                    complicationTemplate: modularLargeTemplate)
                handler(entry)
            case .UtilitarianSmall:
                handler(nil)
            case .UtilitarianLarge:
                handler(nil)
            case .CircularSmall:
                handler(nil)
            }
        }
    }
}

When the getCurrentTimelineEntryForComplication:withHandler: method is fired, we want to display the next upcoming movie. So we iterate through each movie and check whether the difference between the movie's running date and the current time is greater than or equal to 0 (movies that started playing before the current time have a negative value for the timeIntervalSinceNow property):

for movie in movies {
    //---display the next movie that is coming up---
    if (movie.runningDate.timeIntervalSinceNow >= 0) {

Once the first movie is located, we'll create a CLKComplicationTimelineEntry object to display the complication.

To test the application, deploy the iOS application onto the iPhone 6 Simulator. On the Apple Watch Simulator, customize the watch face to display another complication and then change it to display the MOVIES WATCHKIT APP complications again (this will force the complications to clear the watch's cache). You should see the watch face as shown in Figure 2. Notice the name of the movie and the rating of 8.

Figure 2 The complications display the name and rating of the upcoming movie.

Time Travel

One of the cool new features in watchOS 2 is Time Travel. Time Travel allows your application to display time-sensitive information on watch faces with complications. Let's modify our application so that it can display information for movies that were shown previously, as well as movies that are coming up soon.

First, the getSupportedTimeTravelDirectionsForComplication:withHandler: method is already implemented by default:

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

This means that your application will display complication data for past events as well as future events.

Next, add the following statements in bold to the getTimelineStartDateForComplication:withHandler: method:

func getTimelineStartDateForComplication(
    complication: CLKComplication,
    withHandler handler: (NSDate?) -> Void) {

    // handler(nil)
    //---the earliest date your complication can display data
    // is 6 hours ago---
    handler(NSDate(timeIntervalSinceNow: -6 * HOUR))
}

This means that you want to display complication data for events that happened up to six hours ago.

Add the following statements in bold to the getTimelineEndDateForComplication:withHandler: method:

func getTimelineEndDateForComplication(
    complication: CLKComplication,
    withHandler handler: (NSDate?) -> Void) {

    // handler(nil)
    //---the latest date your complication can display data
    // is 12 hours from now---
    handler(NSDate(timeIntervalSinceNow: 12 * HOUR))
}

This means that you want to display complication data for events that will happen in the next 12 hours.

Next, add the following statements in bold to the getTimelineEntriesForComplication:beforeDate:limit:withHandler: method. This method is fired to obtain an array of CLKComplicationTimelineEntry objects, which will contain all the movies screened before the specified date (passed as an argument through the method).

func getTimelineEntriesForComplication(complication: CLKComplication,
        beforeDate date: NSDate, limit: Int,
        withHandler handler: (([CLKComplicationTimelineEntry]?) -> Void)) {
        // Call the handler with the timeline entries prior to the given
        // date

        // handler(nil)
        var timelineEntries: [CLKComplicationTimelineEntry] = []
        //---find all movies before the current date---
        for movie in movies {
            if timelineEntries.count < limit &&
            movie.runningDate.timeIntervalSinceDate(date) < 0 {
                switch complication.family {
                case .ModularSmall:
                    let modularSmallTemplate =
                    CLKComplicationTemplateModularSmallRingText()
                    modularSmallTemplate.textProvider =
                        CLKSimpleTextProvider(text: "\(Int(movie.rating))")
                    modularSmallTemplate.fillFraction = movie.rating / 10
                    modularSmallTemplate.ringStyle =
                        CLKComplicationRingStyle.Closed
                    let entry = CLKComplicationTimelineEntry(
                        date:NSDate(timeInterval: 0 * MINUTE,
                            sinceDate: movie.runningDate),
                        complicationTemplate: modularSmallTemplate)
                    timelineEntries.append(entry)
                case .ModularLarge:
                    let modularLargeTemplate =
                        CLKComplicationTemplateModularLargeStandardBody()
                    modularLargeTemplate.headerTextProvider =
                        CLKTimeIntervalTextProvider(
                            startDate: movie.runningDate,
                            endDate: NSDate(
                                timeInterval: movie.runningTime,
                                sinceDate: movie.runningDate))
                    modularLargeTemplate.body1TextProvider =
                        CLKSimpleTextProvider(
                            text: movie.movieName,
                            shortText: movie.movieName)
                    modularLargeTemplate.body2TextProvider =
                        CLKSimpleTextProvider(
                            text: "\(movie.runningTime / MINUTE) mins",
                            shortText: nil)
                    let entry = CLKComplicationTimelineEntry(
                        date:NSDate(
                            timeInterval: 0 * MINUTE,
                            sinceDate: movie.runningDate),
                        complicationTemplate: modularLargeTemplate)
                    timelineEntries.append(entry)
                case .UtilitarianSmall:
                    break
                case .UtilitarianLarge:
                    break
                case .CircularSmall:
                    break
                }
            }
        }
        handler(timelineEntries)
    }

The limit argument of the method indicates the maximum number of entries to provide. For efficiency, don't exceed the number of entries as specified.

Likewise, add the following statements in bold to the getTimelineEntriesForComplication:afterDate:limit:withHandler: method. This method is fired to obtain an array of CLKComplicationTimelineEntry objects, which will contain all the movies screened after the specified date (passed as an argument through the method).

func getTimelineEntriesForComplication(complication: CLKComplication,
    afterDate date: NSDate, limit: Int,
    withHandler handler: (([CLKComplicationTimelineEntry]?) -> Void)) {
    // Call the handler with the timeline entries after the given
    // date

    // handler(nil)
    var timelineEntries: [CLKComplicationTimelineEntry] = []
    //---find all movies after the current date---
    for movie in movies {
        if timelineEntries.count < limit &&
        movie.runningDate.timeIntervalSinceDate(date) > 0 {
            switch complication.family {
            case .ModularSmall:
                let modularSmallTemplate =
                    CLKComplicationTemplateModularSmallRingText()
                modularSmallTemplate.textProvider =
                    CLKSimpleTextProvider(text: "\(Int(movie.rating))")
                modularSmallTemplate.fillFraction = movie.rating / 10
                modularSmallTemplate.ringStyle =
                    CLKComplicationRingStyle.Closed
                let entry = CLKComplicationTimelineEntry(
                    date:NSDate(timeInterval: 0 * MINUTE,
                    sinceDate: movie.runningDate),
                    complicationTemplate: modularSmallTemplate)
                timelineEntries.append(entry)
            case .ModularLarge:
                let modularLargeTemplate =
                    CLKComplicationTemplateModularLargeStandardBody()
                modularLargeTemplate.headerTextProvider =
                    CLKTimeIntervalTextProvider(
                        startDate: movie.runningDate,
                        endDate: NSDate(timeInterval: movie.runningTime,
                            sinceDate: movie.runningDate))
                modularLargeTemplate.body1TextProvider =
                    CLKSimpleTextProvider(text: movie.movieName,
                        shortText: movie.movieName)
                modularLargeTemplate.body2TextProvider =
                    CLKSimpleTextProvider(
                        text: "\(movie.runningTime / MINUTE) mins",
                        shortText: nil)
                let entry =
                CLKComplicationTimelineEntry(
                    date:NSDate(
                        timeInterval: 0 * MINUTE,
                        sinceDate: movie.runningDate),
                    complicationTemplate: modularLargeTemplate)
                timelineEntries.append(entry)
            case .UtilitarianSmall:
                break
            case .UtilitarianLarge:
                break
            case .CircularSmall:
                break
            }
        }
    }
    handler(timelineEntries)
}

That's it! Deploy the application onto the iPhone 6 Simulator again. Also, remember to clear the complications and set it again.

This time, turn the scroll wheel on your mouse to activate Time Travel. Turning back about two hours will display the World War Z movie, while turning back six hours will display the Terminator 2 movie (see the top of Figure 3).

Moving the time forward about two hours, you'll see the The Dark Knight movie, and moving forward about six hours will display the The Prestige movie (shown at the bottom of Figure 3).

Figure 3 Timeline for the various movies, past and future.

Setting Refresh Frequency

So how often does the complication data get refreshed? You can programmatically specify how often ClockKit wakes up your application to request some data, by implementing the getNextRequestedUpdateDateWithHandler: method:

func getNextRequestedUpdateDateWithHandler(handler: (NSDate?) -> Void) {
    // Call the handler with the date when you would next like to be
    // given the opportunity to update your complication content

    // handler(nil);
    //---update in the next 1 hour---
    handler(NSDate(timeIntervalSinceNow: HOUR))
}

In this statement, I've indicated that the complication data should be requested every hour. However, this is entirely up to ClockKit to decide, and hence it's not guaranteed to happen as planned. Apple recommends that you refresh your complication data either hourly or daily, and each time you refresh you should fetch as much information as you need for each fetch cycle.

If your WatchKit Extension app notices that the complication data is stale, you can also force a manual refresh by using the CLKComplicationServer class:

import ClockKit
...

        let complicationServer = CLKComplicationServer.sharedInstance()
        for complication in complicationServer.activeComplications {
            complicationServer.reloadTimelineForComplication(complication)
        }

Summary

In this article, you displayed watch complications using real data and also implemented Time Travel so that the complications can display movies past and future. Complications is one of the killer features of watchOS 2. Let me know (in the comments or by email) what complications you've implemented for your apps!

  • + Share This
  • 🔖 Save To Your Account