Displaying Time Offsets in Complications - Apple Watch - iOS 9 Swift Programming Cookbook (2015)

iOS 9 Swift Programming Cookbook (2015)

Chapter 2. Apple Watch

2.9 Displaying Time Offsets in Complications

Problem

The data that you want to present has to be shown as an offset to a specific time. For instance, you want to show the remaining minutes until the next train that the user can take to get home.

Solution

Use the CLKRelativeDateTextProvider to provide your information inside a template. In this example, we are going to use CLKComplicationTemplateModularLargeStandardBody, which is a large and modular template.

Discussion

In this recipe, let’s create a watch app that shows the next available train that the user can take to get home. Trains can have differet properties:

§ Date and time of departure

§ Train operator

§ Type of train (high speed, commuter train, etc.)

§ Service name (as shown on the time table)

In our example, I want the complication to look like Figure 2-42. The complication shows the next train (a Coastal service) and how many minutes away that train departs.

Figure 2-42. Complication shows that the next train leaves in 25 minutes

When you create your watchOS project, enable only the modular large complication in the target settings (see Figure 2-43).

Figure 2-43. Enable only the modular large complication for this example

Now create your data model. It will be similar to what we did in Recipe 2.8, but this time we want to provide train times. For the train type and the train company, create enumerations:

enum TrainType : String{

case HighSpeed = "High Speed"

case Commuter = "Commuter"

case Coastal = "Coastal"

}

enum TrainCompany : String{

case SJ = "SJ"

case Southern = "Souther"

case OldRail = "Old Rail"

}

NOTE

These enumerations are of type String, so you can display them on your UI easily without having to write a switch statement.

Then define a protocol to which your train object will conform. Protocol-oriented programming offers many possibilities (see Recipe 1.12), so let’s do that now:

protocol OnRailable{

var type: TrainType {get}

var company: TrainCompany {get}

var service: String {get}

var departureTime: NSDate {get}

}

struct Train : OnRailable{

let type: TrainType

let company: TrainCompany

let service: String

let departureTime: NSDate

}

As we did in Recipe 2.8, we are going to define a data provider. In this example, we create a few trains that depart at specific times with different types of services and from different operators:

struct DataProvider{

func allTrainsForToday() -> [Train]{

var all = [Train]()

let now = NSDate()

let cal = NSCalendar.currentCalendar()

let units = NSCalendarUnit.Year.union(.Month).union(.Day)

let comps = cal.components(units, fromDate: now)

//first train

comps.hour = 6

comps.minute = 30

comps.second = 0

let date1 = cal.dateFromComponents(comps)!

all.append(Train(type: .Commuter, company: .SJ,

service: "3296", departureTime: date1))

//second train

comps.hour = 9

comps.minute = 57

let date2 = cal.dateFromComponents(comps)!

all.append(Train(type: .HighSpeed, company: .Southern,

service: "2307", departureTime: date2))

//third train

comps.hour = 12

comps.minute = 22

let date3 = cal.dateFromComponents(comps)!

all.append(Train(type: .Coastal, company: .OldRail,

service: "3206", departureTime: date3))

//fourth train

comps.hour = 15

comps.minute = 45

let date4 = cal.dateFromComponents(comps)!

all.append(Train(type: .HighSpeed, company: .SJ,

service: "3703", departureTime: date4))

//fifth train

comps.hour = 18

comps.minute = 19

let date5 = cal.dateFromComponents(comps)!

all.append(Train(type: .Coastal, company: .Southern,

service: "8307", departureTime: date5))

//sixth train

comps.hour = 22

comps.minute = 11

let date6 = cal.dateFromComponents(comps)!

all.append(Train(type: .Commuter, company: .OldRail,

service: "6802", departureTime: date6))

return all

}

}

Move now to the ComplicationController class of your watch extension. Here, you will provide watchOS with the data it needs to display your complication. The first task is to extend CollectionType so that you can find the next train in the array that the allTrainsForToday()function of DataProvider returns:

extension CollectionType where Generator.Element : OnRailable {

func nextTrain() -> Generator.Element?{

let now = NSDate()

for d in self{

if now.compare(d.departureTime) == .OrderedAscending{

return d

}

}

return nil

}

}

And you need a data provider in your complication:

class ComplicationController: NSObject, CLKComplicationDataSource {

let dataProvider = DataProvider()

...

For every train, you need to create a template that watchOS can display on the screen. All templates are of type CLKComplicationTemplate, but don’t initialize that class directly. Instead, create a template of type CLKComplicationTemplateModularLargeStandardBody that has a header, two lines of text with the second line being optional, and an optional image. The header will show a constant text (see Figure 2-42), so instantiate it of type CLKSimpleTextProvider. For the first line of text, you want to show how many minutes away the next train is, so that would require a text provider of type CLKRelativeDateTextProvider as we talked about it before.

The initializer for CLKRelativeDateTextProvider takes in a parameter of type CLKRelativeDateStyle that defines the way the given date has to be shown. In our example, we use CLKRelativeDateStyle.Offset:

func templateForTrain(train: Train) -> CLKComplicationTemplate{

let template = CLKComplicationTemplateModularLargeStandardBody()

template.headerTextProvider = CLKSimpleTextProvider(text: "Next train")

template.body1TextProvider =

CLKRelativeDateTextProvider(date: train.departureTime,

style: .Offset,

units: NSCalendarUnit.Hour.union(.Minute))

let secondLine = "\(train.service) - \(train.type)"

template.body2TextProvider = CLKSimpleTextProvider(text: secondLine,

shortText: train.type.rawValue)

return template

}

NOTE

The second line of text we are providing has a shortText alternative. If the watch UI has no space to show our secondLine text, it will show the shortText alternative.

We are going to need to provide timeline entries (date plus template) for every train as well, so let’s create a helper method for that:

func timelineEntryForTrain(train: Train) -> CLKComplicationTimelineEntry{

let template = templateForTrain(train)

return CLKComplicationTimelineEntry(date: train.departureTime,

complicationTemplate: template)

}

When we are asked for the first and the last date of the data we provide, we read our data provider’s array of train and return the first and the last train’s dates, respectively:

func getTimelineStartDateForComplication(complication: CLKComplication,

withHandler handler: (NSDate?) -> Void) {

handler(dataProvider.allTrainsForToday().first!.departureTime)

}

func getTimelineEndDateForComplication(complication: CLKComplication,

withHandler handler: (NSDate?) -> Void) {

handler(dataProvider.allTrainsForToday().last!.departureTime)

}

I want to allow the user to be able to timetravel so that she can see the next train as she changes the time with the digital crown. I also believe our data is not sensitive, so I’ll allow viewing this data on the lock screen:

func getSupportedTimeTravelDirectionsForComplication(

complication: CLKComplication,

withHandler handler: (CLKComplicationTimeTravelDirections) -> Void) {

handler([.Forward, .Backward])

}

func getPrivacyBehaviorForComplication(complication: CLKComplication,

withHandler handler: (CLKComplicationPrivacyBehavior) -> Void) {

handler(.ShowOnLockScreen)

}

Regarding time travel, when asked for trains after and before a certain time, your code should go through all the trains and filter out the times you don’t want displayed, as we did in Recipe 2.8:

UU

func getTimelineEntriesForComplication(complication: CLKComplication,

beforeDate date: NSDate, limit: Int,

withHandler handler: (([CLKComplicationTimelineEntry]?) -> Void)) {

let entries = dataProvider.allTrainsForToday().filter{

date.compare($0.departureTime) == .OrderedDescending

}.map{

self.timelineEntryForTrain($0)

}

handler(entries)

}

func getTimelineEntriesForComplication(complication: CLKComplication,

afterDate date: NSDate, limit: Int,

withHandler handler: (([CLKComplicationTimelineEntry]?) -> Void)) {

let entries = dataProvider.allTrainsForToday().filter{

date.compare($0.departureTime) == .OrderedAscending

}.map{

self.timelineEntryForTrain($0)

}

handler(entries)

}

When the getCurrentTimelineEntryForComplication(_:withHandler:) method is called on our delegate, we get the next train’s timeline entry and return it:

func getCurrentTimelineEntryForComplication(complication: CLKComplication,

withHandler handler: ((CLKComplicationTimelineEntry?) -> Void)) {

if let train = dataProvider.allTrainsForToday().nextTrain(){

handler(timelineEntryForTrain(train))

} else {

handler(nil)

}

}

Since we provide data until the end of today, we ask watchOS to ask us for new data tomorrow:

func getNextRequestedUpdateDateWithHandler(handler: (NSDate?) -> Void) {

handler(NSDate.endOfToday());

}

Last but not least, we provide our placeholder template:

func getPlaceholderTemplateForComplication(complication: CLKComplication,

withHandler handler: (CLKComplicationTemplate?) -> Void) {

if let data = dataProvider.allTrainsForToday().nextTrain(){

handler(templateForTrain(data))

} else {

handler(nil)

}

}

We saw an example of our app showing the next train (see Figure 2-42), but our app can also participate in time travel (see Figure 2-44). The user can use the digital crown on the watch to move forward or backward and see the next available train at the new time.

Figure 2-44. Moving our complication backward in time

See Also