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

iOS 9 Swift Programming Cookbook (2015)

Chapter 2. Apple Watch

2.11 Displaying Times in Complications

Problem

You want to display a time on your watch UI and want it to look good regardless of available space on the watch.

Solution

Provide your time (in form of NSDate) to an instance of CLKTimeTextProvider and use it inside a template (see Figure 2-46). Our large and modular complication on the center of the screen is showing the next pause that we can take at work, which happens to be a coffee pause.

Figure 2-46. The time is displayed on the screen using an instance of CLKTimeTextProvider

NOTE

In this recipe, we are going to rely a lot on what we have learned in Recipe 2.8 and other complication recipes in this chapter. I suggest reading Recipe 2.8 at least to get an idea of how our data provider works. Otherwise, you will still be able to read this recipe; however, I will skip over some details that I’ve already explained in Recipe 2.8.

Discussion

This recipe uses a large modular template, so make sure that your project is set up for that (see Figure 2-43). Here, I want to build an app that shows the different breaks or pauses that I can take at work, and when they occur: for instance, when the first pause is after I get to work, when lunch happens, when the next pause between lunch and dinner is, and if I want to have dinner as well, when that should happen.

So we have breaks at work and we need to define them. Create a Swift file in your watch extension and call it DataProvider. In there, define your break:

import Foundation

protocol Pausable{

var name: String {get}

var date: NSDate {get}

}

struct PauseAtWork : Pausable{

let name: String

let date: NSDate

}

Now in your DataProvider, create 4 pauses that we can take at work at different times and provide them as an array:

struct DataProvider{

func allPausesToday() -> [PauseAtWork]{

var all = [PauseAtWork]()

let now = NSDate()

let cal = NSCalendar.currentCalendar()

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

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

comps.calendar = cal

comps.minute = 30

comps.hour = 11

all.append(PauseAtWork(name: "Coffee", date: comps.date!))

comps.minute = 30

comps.hour = 14

all.append(PauseAtWork(name: "Lunch", date: comps.date!))

comps.minute = 0

comps.hour = 16

all.append(PauseAtWork(name: "Tea", date: comps.date!))

comps.hour = 17

all.append(PauseAtWork(name: "Dinner", date: comps.date!))

return all

}

}

Here we have just obtaied the date and time of today and then gone from coffee break in the morning to dinner in the evening, adding each pause to the array. The method is called allPausesToday(). and we are going to invoke it from our watch complication.

Before, we created a protocol called Pausable and now we have all our pauses in an array. When we are asked to provide a template for the next pause to show in the complication, we have to get the current time and find the pause whose time is after the current time. So let’s bundle that up by extending CollectionType like we have done in other recipes in this chapter:

extension CollectionType where Generator.Element : Pausable {

func nextPause() -> Self.Generator.Element?{

let now = NSDate()

for pause in self{

if now.compare(pause.date) == .OrderedAscending{

return pause

}

}

return nil

}

}

In our complication now, we instantiate our data provider:

class ComplicationController: NSObject, CLKComplicationDataSource {

let dataProvider = DataProvider()

...

For every pause that we want to display to the user (see Figure 2-46), we need to provide a template of type CLKComplicationTemplate to the runtime. We never instantiate that class directly. Instead, we return an instance of a subclass of that class. In this particular example, we display an instance of CLKComplicationTemplateModularLargeTallBody. However, if there are no more pauses to take at work (e.g., if time is 21:00 and we are no longer at work), we display a placeholder to the user to tell her there are no more pauses. The template for that is of typeCLKComplicationTemplateModularLargeStandardBody. The difference between the two templates is visible if you read their names. We set the time on our template by setting the bodyTextProvider property of our CLKComplicationTemplateModularLargeTallBodyinstance.

func templateForPause(pause: PauseAtWork) -> CLKComplicationTemplate{

guard let nextPause = dataProvider.allPausesToday().nextPause() else{

let template = CLKComplicationTemplateModularLargeStandardBody()

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

template.body1TextProvider = CLKSimpleTextProvider(text: "None")

return template

}

let template = CLKComplicationTemplateModularLargeTallBody()

template.headerTextProvider = CLKSimpleTextProvider(text: nextPause.name)

template.bodyTextProvider = CLKTimeTextProvider(date: nextPause.date)

return template

}

We also have to provide some of the other delegate methods of CLKComplicationDataSource, such as the timeline entry (date plus template) for every pause that we can take at work. We also need to support timetravel for this example. On top of that, our information is not sensitive, so when asked whether we want to display our complication on the lock screen, we happily say yes.

func timelineEntryForPause(pause: PauseAtWork) ->

CLKComplicationTimelineEntry{

let template = templateForPause(pause)

return CLKComplicationTimelineEntry(date: pause.date,

complicationTemplate: template)

}

func getSupportedTimeTravelDirectionsForComplication(

complication: CLKComplication,

withHandler handler: (CLKComplicationTimeTravelDirections) -> Void) {

handler([.Forward, .Backward])

}

func getPrivacyBehaviorForComplication(complication: CLKComplication,

withHandler handler: (CLKComplicationPrivacyBehavior) -> Void) {

handler(.ShowOnLockScreen)

}

When asked the beginning and the end range of dates for our complications, we will return the dates for the first and the last pause that we want to take at work today. Remember, in this complication, we will return all the pauses that we can take at work today. When the time comes to display the pauses to take at work tomorrow, we will provide a whole set of new pauses:

func getTimelineStartDateForComplication(complication: CLKComplication,

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

handler(dataProvider.allPausesToday().first!.date)

}

func getTimelineEndDateForComplication(complication: CLKComplication,

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

handler(dataProvider.allPausesToday().last!.date)

}

When the runtime calls the getTimelineEntriesForComplication(_:beforeDate:limit:withHandler:) method, provide all the pauses that are available before the given date.

func getTimelineEntriesForComplication(complication: CLKComplication,

beforeDate date: NSDate, limit: Int,

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

let entries = dataProvider.allPausesToday().filter{

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

}.map{

self.timelineEntryForPause($0)

}

handler(entries)

}

Similarly, when the getTimelineEntriesForComplication(_:afterDate:limit:withHandler:) method is called, return all the available pauses after the given date:

func getTimelineEntriesForComplication(complication: CLKComplication,

afterDate date: NSDate, limit: Int,

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

let entries = dataProvider.allPausesToday().filter{

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

}.map{

self.timelineEntryForPause($0)

}

handler(entries)

}

In the getCurrentTimelineEntryForComplication(_:withHandler:) method, you will be asked to provide the template for the current data (the next pause) to show on screen. We already have a method on CollectionType called nextPause(), so let’s use that to provide a template to watchOS:

func getCurrentTimelineEntryForComplication(complication: CLKComplication,

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

if let pause = dataProvider.allPausesToday().nextPause(){

handler(timelineEntryForPause(pause))

} else {

handler(nil)

}

}

Because, in a typical watch app, our data would probably come from a backend, we would like the runtime to task us for up-to-date information as soon as possible, but not too soon. So let’s do that after 10 minutes:

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

handler(NSDate().plus10Minutes());

}

Last but not least, we also need to provide a placeholder template when the user is adding our complication to her watch face:

func getPlaceholderTemplateForComplication(complication: CLKComplication,

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

if let pause = dataProvider.allPausesToday().nextPause(){

handler(templateForPause(pause))

} else {

handler(nil)

}

}

See Also