To support Time Travel, you need to modify your ComplicationController in NomNomNom to support it. Just as you need to provide the current complication before the user raises her wrist, so that the data can be there immediately, you need to provide the past and future complications before the user activates Time Travel, so she can smoothly transition from present to past and future. It all begins with getSupportedTimeTravelDirectionsForComplication(_:withHandler:). This method determines whether your complication can support going forward or backward in time (or both). For NomNomNom, since you’re looking at calendar data, you can support both. Other apps may be a judgment call: for instance, should a weather complication support the past so you can see what the temperature was an hour ago? Then there’s the obvious joke: figure out how to make a stock market complication support the future, and retire a billionaire. For now, let’s implement this method on NomNomNom:
| func getSupportedTimeTravelDirectionsForComplication( |
| complication: CLKComplication, |
| withHandler handler: (CLKComplicationTimeTravelDirections) -> Void) { |
| handler([.Forward, .Backward]) |
| } |
Since you support both directions, you pass them both to handler. Now the system will try to display timeline entries for the past and future. Complications that don’t support Time Travel in a given direction are grayed out during Time Travel to indicate to the user that they’re unavailable.
Next, you need to tell the system the range of dates for which Time Travel is available. If the user advances Time Travel beyond the end date you’ve specified, your complication will be grayed out to indicate that there’s no more data. For you to know the appropriate dates to provide, you’re going to need to know when the first and last calendar events are inside the maximum range of Time Travel. Let’s build this out, piece by piece.
You need to find the range of dates that Time Travel supports. No matter what data you provide, the user can only use Time Travel to go from midnight at the beginning of the previous day to midnight at the end of the next day. This gives you a three-day window to work with: yesterday, today, and tomorrow. To represent those dates as NSDate instances, you need to refactor your date math from the last chapter. Open DateMath.swift and modify it as follows:
| private let calendar = NSCalendar.currentCalendar() |
| |
| extension NSDate { |
| private var componentsAtMidnight: NSDateComponents { |
| let components = calendar.components([.Year, .Month, .Day, .Hour, .Minute, |
| .Second], fromDate: self) |
| |
| components.hour = 0 |
| components.minute = 0 |
| components.second = 0 |
| |
| return components |
| } |
| |
| var midnightBefore: NSDate { |
| return calendar.dateFromComponents(componentsAtMidnight)! |
| } |
| |
| func midnightBeforeWithDayOffset(offset: Int) -> NSDate { |
| let dayOffsetComponents = NSDateComponents() |
| dayOffsetComponents.day = offset |
| |
| return calendar.dateByAddingComponents(dayOffsetComponents, |
| toDate: midnightBefore, |
| options: NSCalendarOptions())! |
| } |
| |
| var midnightAfter: NSDate { |
| return midnightBeforeWithDayOffset(1) |
| } |
| } |
In this code, you pull out the bit that creates NSDateComponents for a date at midnight into its own computed property, componentsAtMidnight. The midnightBefore computed property becomes simple: just create a date from those components. midnightAfter is now implemented in terms of midnightBeforeWithDayOffset(_:), which will add (or subtract) days to the midnight components. Using this method, you’ll be able to compute the dates for Time Travel.
Back in ComplicationController.swift, let’s use this method to find the minimum and maximum dates that Time Travel will cover:
| var timeTravelBeginDate: NSDate { |
| return NSDate().midnightBeforeWithDayOffset(-1) |
| } |
| |
| var timeTravelEndDate: NSDate { |
| return NSDate().midnightBeforeWithDayOffset(2) |
| } |
You use -1 for the beginning offset to subtract one day from midnight of the current day and 2 to add two days to it, giving you the correct dates for Time Travel. Next, let’s use those dates to filter your calendar events, so you can find just the calendar events that the user can use Time Travel to get to.
Much like your date math, you’re going to refactor your EventKit extension to allow you to filter meal events by date. You’ll make a new method, getMealEventsBetweenDate(_:andDate:completion:), which you’ll use to filter your events. Refactor the extension as follows:
| extension EKEventStore { |
| func getMealEventsBetweenDate(startDate: NSDate, andDate endDate: NSDate, |
| completion: [EKEvent] -> Void) { |
| requestAccessToEntityType(.Event) { (success, error) in |
| if success { |
| let timePredicate = self.predicateForEventsWithStartDate( |
| startDate, |
| endDate: endDate, |
| calendars: nil) |
| |
| let mealPredicates = ["breakfast", "brunch", "lunch", "supper", |
| "dinner", "snack", "coffee", "tea", "happy hour", "drinks"] |
| .map { NSPredicate(format: "title contains[cd] %@", $0) } |
| |
| let mealMatchingPredicate = NSCompoundPredicate( |
| orPredicateWithSubpredicates: mealPredicates) |
| |
| let events = self.eventsMatchingPredicate(timePredicate) |
| .filter { mealMatchingPredicate.evaluateWithObject($0) } |
| |
| completion(events) |
| } |
| else { |
| completion([]) |
| } |
| } |
| } |
| |
| func requestNextMealEvent(forDate date: NSDate = NSDate(), |
| handler: EKEvent? -> Void) { |
| getMealEventsBetweenDate(date, andDate: date.midnightAfter) { events in |
| |
| let nextEvent = events |
| .sort { $0.startDate.compare($1.startDate) == .OrderedAscending} |
| .first |
| |
| handler(nextEvent) |
| } |
| } |
| } |
All of the logic in getMealEventsBetweenDate(_:andDate:completion:) comes from requestNextMealEvent(forDate:handler:)—events are filtered based on the dates that are passed in and then on the event’s title. In requestNextMealEvent(forDate:handler:), you leave the logic for when to filter—between now and midnight—as well as the logic to simply return the earliest event, if there is one.
With this method, you can now filter the user’s calendar to find all of the events that should show up using Time Travel:
| func getCalendarEvents(completion: [EKEvent] -> Void) { |
| let minimumDate = timeTravelBeginDate |
| let maximumDate = timeTravelEndDate |
| |
| eventStore.getMealEventsBetweenDate(minimumDate, andDate: maximumDate, |
| completion: completion) |
| } |
The getCalendarEvents(_:) method computes the beginning and ending dates, filters the events in the calendar to be between them, and then calls completion with the matching events. Even if there are no matching events, you’ll still get an array; it’ll just be empty. Finally, now that you know the range of events you’re supporting, you can implement your complication data source methods to tell ClockKit when Time Travel is supported:
| func getTimelineStartDateForComplication(complication: CLKComplication, |
| withHandler handler: (NSDate?) -> Void) { |
| getCalendarEvents { events in |
| let earliestDate = events.reduce(NSDate()) { (date, event) in |
| return date.earlierDate(event.startDate) |
| } |
| |
| handler(earliestDate) |
| } |
| } |
| |
| func getTimelineEndDateForComplication(complication: CLKComplication, |
| withHandler handler: (NSDate?) -> Void) { |
| getCalendarEvents { events in |
| let latestDate = events.reduce(NSDate()) { (date, event) in |
| return date.laterDate(event.endDate) |
| } |
| |
| handler(latestDate) |
| } |
| } |
These methods are similar, just mirror images of one another. The first finds the earliest date of all of the events’ start dates, and the second finds the latest date of all of the events’ end dates. They use reduce(_:combine:) to iterate through the events’ dates, using earlierDate(_:) and laterDate(_:) to find the earlier and later dates of each pair. Now that you have these dates, you can construct new timeline entries for each event!
Time Travel works by moving along a timeline of events, ranging from the beginning date to the end date. As the user moves through events, the next event in the timeline is displayed to her. To implement Time Travel, all you need to do is to return CLKComplicationTimelineEntry instances for each event. Let’s work on the past first:
| func getTimelineEntriesForComplication(complication: CLKComplication, |
| beforeDate date: NSDate, |
| limit: Int, |
| withHandler handler: (([CLKComplicationTimelineEntry]?) -> Void)) { |
| eventStore.getMealEventsBetweenDate(timeTravelBeginDate, |
| andDate: date) { events in |
| let timelineEntries = events |
| .flatMap { event -> CLKComplicationTimelineEntry? in |
| switch complication.family { |
| case .UtilitarianLarge: |
| return self.utilitarianLargeTimelineEntry( |
| forEvent: event) |
| case .ModularLarge: |
| return self.modularLargeTimelineEntry( |
| forEvent: event) |
| default: |
| return nil |
| } |
| } |
| |
| handler(timelineEntries) |
| } |
| } |
This method is pretty dense, so let’s break it down. First, you filter your calendar between the timeline beginning date and the date passed in to this method. This will give you every event before date. When you have those, you use flatMap(_:) to convert the array of EKEvent events into CLKComplicationTimelineEntry classes using the methods you wrote before. Once you have an array of timeline entries, you pass it into the handler, and your Time Travel is ready to go! For future events, you do the same thing:
| func getTimelineEntriesForComplication(complication: CLKComplication, |
| afterDate date: NSDate, |
| limit: Int, |
| withHandler handler: (([CLKComplicationTimelineEntry]?) -> Void)) { |
| eventStore.getMealEventsBetweenDate(date, |
| andDate: timeTravelEndDate) { events in |
| let timelineEntries = events |
| .flatMap { event -> CLKComplicationTimelineEntry? in |
| switch complication.family { |
| case .UtilitarianLarge: |
| return self.utilitarianLargeTimelineEntry( |
| forEvent: event) |
| case .ModularLarge: |
| return self.modularLargeTimelineEntry( |
| forEvent: event) |
| default: |
| return nil |
| } |
| } |
| |
| handler(timelineEntries) |
| } |
| } |
The difference here is simply the order of the dates passed in; this time, you’ll go from the given date to the timeline end date. With these two methods in place, you’re finished with Time Travel! Build and run, and you can see for yourself. Either manipulate the Digital Crown on a physical device or scroll your trackpad on the simulator to activate Time Travel.
This screenshot shows the same complication in the past, present, and future. As you scroll through time, however, you might notice that the complication behaves slightly differently from the built-in system calendar complication. Whereas the system calendar complication includes an animation while it transitions from event to event, your complication just changes in place; the labels update to reveal their new contents but don’t animate at all. You can specify the animation behavior of your complication to solve this problem.
For your complication, you’d like to have an animated transition between any two timeline entries. To achieve this, you need to implement another callback in your complication controller:
| func getTimelineAnimationBehaviorForComplication(complication: CLKComplication, |
| withHandler handler: (CLKComplicationTimelineAnimationBehavior) -> Void) { |
| handler(CLKComplicationTimelineAnimationBehavior.Always) |
| } |
By simply calling the handler with .Always, you tell ClockKit to always animate between timeline entries. If you build and run the app, you’ll see the behavior as one entry is swapped out for the next during Time Travel. Perfect! If this isn’t quite what you want, there are other options available. Passing .Never instead of .Always will remove all animations from the complication. In between these two extremes is .Grouped. This value causes ClockKit to look at the timelineAnimationGroup property of the CLKComplicationTimelineEntry instances on the timeline. If two entries next to each other in the timeline have different animation groups set, or if one is nil, then the system will use an animation. If the animation groups are the same, then no animation will be performed. This is a great way to get custom behavior out of your timeline.
With these APIs in place, you’ve completed adding Time Travel support to NomNomNom. Users can see their food calendar for three days at a time right on their watch. Time Travel is real, and you can use it today! Great Scott!
18.227.24.74