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

iOS 9 Swift Programming Cookbook (2015)

Chapter 2. Apple Watch

2.12 Displaying Time Intervals in Complications

Problem

You want to display a time interval (start date - end date) on your watchOS UI (see Figure 2-47). Our template is shows today’s meetings on the screen. Right now, it’s brunch time, so the screen shows the description and location of where we are going to have brunch, along with the time interval of the brunch (start - end).

Figure 2-47. Meeting with start and end times

Solution

Use an instance of CLKTimeIntervalTextProvider as your text provider (see Figure 2-47).

NOTE

I will base this recipe on other recipes such as Recipe 2.10 and Recipe 2.11.

Discussion

Let’s say that we want to have an app that shows us all our meetings today. Every meeting has the following properties:

§ Start and end times (the time interval)

§ Name (e.g., “Brunch with Sarah”)

§ Location

Because text providers of type CLKSimpleTextProvider accept a short text in addition to the full text, we also have a short version of the location and the name. For instance, the location can be “Stockholm Central Train Station”, whereas the short version of this could be “Central Station” or even “Centralen” in Swedish, which means the center. So let’s define this meeting object:

protocol Timable{

var name: String {get}

var shortName: String {get}

var location: String {get}

var shortLocation: String {get}

var startDate: NSDate {get}

var endDate: NSDate {get}

var previous: Timable? {get}

}

struct Meeting : Timable{

let name: String

let shortName: String

let location: String

let shortLocation: String

let startDate: NSDate

let endDate: NSDate

let previous: Timable?

}

Create a Swift file in your project called DataProvider. Put all the meetings for today in there and return an array:

struct DataProvider{

func allMeetingsToday() -> [Meeting]{

var all = [Meeting]()

let oneHour: NSTimeInterval = 1 * 60.0 * 60

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

let meeting1 = Meeting(name: "Brunch with Sarah", shortName: "Brunch",

location: "Stockholm Central", shortLocation: "Central",

startDate: comps.date!,

endDate: comps.date!.dateByAddingTimeInterval(oneHour), previous: nil)

all.append(meeting1)

comps.minute = 30

comps.hour = 14

let meeting2 = Meeting(name: "Lunch with Gabriella", shortName: "Lunch",

location: "At home", shortLocation: "Home",

startDate: comps.date!,

endDate: comps.date!.dateByAddingTimeInterval(oneHour),

previous: meeting1)

all.append(meeting2)

comps.minute = 0

comps.hour = 16

let meeting3 = Meeting(name: "Snack with Leif", shortName: "Snack",

location: "Flags Cafe", shortLocation: "Flags",

startDate: comps.date!,

endDate: comps.date!.dateByAddingTimeInterval(oneHour),

previous: meeting2)

all.append(meeting3)

comps.hour = 17

let meeting4 = Meeting(name: "Dinner with Family", shortName: "Dinner",

location: "At home", shortLocation: "Home",

startDate: comps.date!,

endDate: comps.date!.dateByAddingTimeInterval(oneHour),

previous: meeting3)

all.append(meeting4)

return all

}

}

In your complication class, extend CollectionType so that it can return the upcoming meeting:

extension CollectionType where Generator.Element : Timable {

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

let now = NSDate()

for meeting in self{

if now.compare(meeting.startDate) == .OrderedAscending{

return meeting

}

}

return nil

}

}

NOTE

I have extended CollectionType, but only if the items are Timable. I explained this technique in Recipe 1.12.

In your complication handler, create an instance of the data provider:

class ComplicationController: NSObject, CLKComplicationDataSource {

let dataProvider = DataProvider()

...

Our template is of type CLKComplicationTemplateModularLargeStandardBody, which has a few important properties that we set as follows:

headerTextProvider

Shows the time range for the meeting.

body1TextProvider

Shows the name of the meeting.

body2TextProvider

Shows the location of the meeting.

To display the time range of the meeting, instantiate CLKTimeIntervalTextProvider:

func templateForMeeting(meeting: Meeting) -> CLKComplicationTemplate{

let template = CLKComplicationTemplateModularLargeStandardBody()

guard let nextMeeting = dataProvider.allMeetingsToday().nextMeeting() else{

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

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

return template

}

template.headerTextProvider =

CLKTimeIntervalTextProvider(startDate: nextMeeting.startDate,

endDate: nextMeeting.endDate)

template.body1TextProvider =

CLKSimpleTextProvider(text: nextMeeting.name,

shortText: nextMeeting.shortName)

template.body2TextProvider =

CLKSimpleTextProvider(text: nextMeeting.location,

shortText: nextMeeting.shortLocation)

return template

}

Using this method, you can also create timeline entries (date plus template). In this example, I set every new event’s start date to the end date of the previous event (if it is available). That way, as soon as the current ongoing meeting ends, the next meeting shows up on the list:

NOTE

If the event has no previous events, its timeline entry date will be its start date, instead of the end date of the previous event.

func timelineEntryForMeeting(meeting: Meeting) -> CLKComplicationTimelineEntry{

let template = templateForMeeting(meeting)

let date = meeting.previous?.endDate ?? meeting.startDate

return CLKComplicationTimelineEntry(date: date,

complicationTemplate: template)

}

Let’s also participate in time travel and show our content on the lock screen as well:

func getSupportedTimeTravelDirectionsForComplication(

complication: CLKComplication,

withHandler handler: (CLKComplicationTimeTravelDirections) -> Void) {

handler([.Forward, .Backward])

}

func getPrivacyBehaviorForComplication(complication: CLKComplication,

withHandler handler: (CLKComplicationPrivacyBehavior) -> Void) {

handler(.ShowOnLockScreen)

}

Then we have to provide the date range for which we have available meetings. The start of the range is the start date of the first meeting and the end date is the end date of the last meeting:

func getTimelineStartDateForComplication(complication: CLKComplication,

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

handler(dataProvider.allMeetingsToday().first!.startDate)

}

func getTimelineEndDateForComplication(complication: CLKComplication,

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

handler(dataProvider.allMeetingsToday().last!.endDate)

}

We’ll also be asked to provide all the available meetings before a certain date, so let’s do that:

func getTimelineEntriesForComplication(complication: CLKComplication,

beforeDate date: NSDate, limit: Int,

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

let entries = dataProvider.allMeetingsToday().filter{

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

}.map{

self.timelineEntryForMeeting($0)

}

handler(entries)

}

Similarly, we have to provide all our available meetings after a given date:

func getTimelineEntriesForComplication(complication: CLKComplication,

afterDate date: NSDate, limit: Int,

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

let entries = dataProvider.allMeetingsToday().filter{

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

}.map{

self.timelineEntryForMeeting($0)

}

handler(entries)

}

Last but not least, provide your placeholder template, the template for now, and the next time we would like watchOS to ask us for updated information:

func getCurrentTimelineEntryForComplication(complication: CLKComplication,

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

if let meeting = dataProvider.allMeetingsToday().nextMeeting(){

handler(timelineEntryForMeeting(meeting))

} else {

handler(nil)

}

}

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

handler(NSDate().plus10Minutes());

}

func getPlaceholderTemplateForComplication(complication: CLKComplication,

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

if let pause = dataProvider.allMeetingsToday().nextMeeting(){

handler(templateForMeeting(pause))

} else {

handler(nil)

}

}

NOTE

We coded the plus10Minutes() method on NSDate in Recipe 2.10.

See Also