iOS 9 Swift Programming Cookbook (2015)
Chapter 2. Apple Watch
2.10 Displaying Dates in Complications
Problem
You want to display NSDate instances on your complications.
Solution
Use an instance of the CLKDateTextProvider class, which is a subclass of CLKTextProvider, as your text provider.
NOTE
I am going to use CLKComplicationTemplateModularLargeColumns (a modular large template) for this recipe. So configure your watch target to provide only large modular templates (see Figure 2-43).
Discussion
Let’s develop a modular large complication that provides us with the name and the date of the next three public holidays (see Figure 2-45). We are not formatting the date ourselves. We leave it to watchOS to decide how to display the date by using an instance of CLKDateTextProvider.
Figure 2-45. The next 3 public holidays, with their names and dates
Just as in Recipe 2.8 and Recipe 2.9, we are going to add a new class to our watch app called DataProvider. In there, we are going to program all the holidays this year. Let’s start off by defining what a holiday object looks like:
protocol Holidayable{
var date: NSDate {get}
var name: String {get}
}
struct Holiday : Holidayable{
let date: NSDate
let name: String
}
In our data provider class, we start off by defining some holiday names:
struct DataProvider{
private let holidayNames = [
"Father's Day",
"Mother's Day",
"Bank Holiday",
"Nobel Day",
"Man Day",
"Woman Day",
"Boyfriend Day",
"Girlfriend Day",
"Dog Day",
"Cat Day",
"Mouse Day",
"Cow Day",
]
private func randomDay() -> Int{
return Int(arc4random_uniform(20) + 1)
}
...
Then we move on to providing our instances of Holiday:
func allHolidays() -> [Holiday]{
var all = [Holiday]()
let now = NSDate()
let cal = NSCalendar.currentCalendar()
let units = NSCalendarUnit.Year.union(.Month).union(.Day)
let comps = cal.components(units, fromDate: now)
var dates = [NSDate]()
for month in 1...12{
comps.day = randomDay()
comps.month = month
dates.append(cal.dateFromComponents(comps)!)
}
var i = 0
for date in dates{
all.append(Holiday(date: date, name: holidayNames[i++]))
}
return all
}
It’s worth noting that the allHolidays() function we just wrote simply goes through all months inside this year, and sets the day of the month to a random day. So we will get 12 holidays, one in each month, at a random day inside that month.
Over to our ComplicationController. When we get asked later when we would like to provide more data or updated data to watchOS, we are going to ask for 10 minutes in the future. So if our data changes, watchOS will have a chance to ask us for updated information:
extension NSDate{
func plus10Minutes() -> NSDate{
return self.dateByAddingTimeInterval(10 * 60)
}
}
Because the template we are going to provide allows a maximum of three items, I would like to have methods on Array to return the second and the third items inside the array, just like the prebuilt first property that the class offers:
extension Array{
var second : Generator.Element?{
return self.count >= 1 ? self[1] : nil
}
var third : Generator.Element?{
return self.count >= 2 ? self[2] : nil
}
}
DataProvider’s allHolidays() method returns 12 holidays. How about extending the built-in array type to alawys give us the next 3 holidays? It would have to read today’s date, go through the items in our array, compare the dates, and give us just the upcoming 3 holidays:
extension CollectionType where Generator.Element : Holidayable {
//may contain less than 3 holidays
func nextThreeHolidays() -> Array<Self.Generator.Element>{
let now = NSDate()
let orderedArray = Array(self.filter{
now.compare($0.date) == .OrderedAscending
})
let result = Array(orderedArray[0..<min(orderedArray.count , 3)])
return result
}
}
now we start defining our complication:
class ComplicationController: NSObject, CLKComplicationDataSource {
let dataProvider = DataProvider()
...
We need a method that can take in a Holiday object and give us a template of type CLKComplicationTemplate for that. Our specific template for this recipe is of type CLKComplicationTemplateModularLargeColumns. This template is like a 3x3 table. It has 3 rows and 3 columns (see Figure 2-45). If we are at the end of the year and we have no more holidays, we return a template that is of type CLKComplicationTemplateModularLargeStandardBody and tell the user that there are no more upcoming holidays. Note that both templates have the words “ModularLarge” in their name. Because we have specified in our target setting that we support only modular large templates (see Figure 2-43), this example can return only templates that have those words in their name.
func templateForHoliday(holiday: Holiday) -> CLKComplicationTemplate{
let next3Holidays = dataProvider.allHolidays().nextThreeHolidays()
let headerTitle = "Next 3 Holidays"
guard next3Holidays.count > 0 else{
let template = CLKComplicationTemplateModularLargeStandardBody()
template.headerTextProvider = CLKSimpleTextProvider(text: headerTitle)
template.body1TextProvider = CLKSimpleTextProvider(text: "Sorry!")
return template
}
let dateUnits = NSCalendarUnit.Month.union(.Day)
let template = CLKComplicationTemplateModularLargeColumns()
//first holiday
if let firstHoliday = next3Holidays.first{
template.row1Column1TextProvider =
CLKSimpleTextProvider(text: firstHoliday.name)
template.row1Column2TextProvider =
CLKDateTextProvider(date: firstHoliday.date, units: dateUnits)
}
//second holiday
if let secondHoliday = next3Holidays.second{
template.row2Column1TextProvider =
CLKSimpleTextProvider(text: secondHoliday.name)
template.row2Column2TextProvider =
CLKDateTextProvider(date: secondHoliday.date, units: dateUnits)
}
//third holiday
if let thirdHoliday = next3Holidays.third{
template.row3Column1TextProvider =
CLKSimpleTextProvider(text: thirdHoliday.name)
template.row3Column2TextProvider =
CLKDateTextProvider(date: thirdHoliday.date, units: dateUnits)
}
return template
}
You need to provide a timeline entry (date plus template) for your holidays as well:
func timelineEntryForHoliday(holiday: Holiday) ->
CLKComplicationTimelineEntry{
let template = templateForHoliday(holiday)
return CLKComplicationTimelineEntry(date: holiday.date,
complicationTemplate: template)
}
Also provide the first and the last holidays:
func getTimelineStartDateForComplication(complication: CLKComplication,
withHandler handler: (NSDate?) -> Void) {
handler(dataProvider.allHolidays().first!.date)
}
func getTimelineEndDateForComplication(complication: CLKComplication,
withHandler handler: (NSDate?) -> Void) {
handler(dataProvider.allHolidays().last!.date)
}
Also support time travel and provide your content on the lock screen, because it is not private:
func getSupportedTimeTravelDirectionsForComplication(
complication: CLKComplication,
withHandler handler: (CLKComplicationTimeTravelDirections) -> Void) {
handler([.Forward, .Backward])
}
func getPrivacyBehaviorForComplication(complication: CLKComplication,
withHandler handler: (CLKComplicationPrivacyBehavior) -> Void) {
handler(.ShowOnLockScreen)
}
Now let’s give watchOS information about previous and upcoming holidays:
func getTimelineEntriesForComplication(complication: CLKComplication,
beforeDate date: NSDate, limit: Int,
withHandler handler: (([CLKComplicationTimelineEntry]?) -> Void)) {
let entries = dataProvider.allHolidays().filter{
date.compare($0.date) == .OrderedDescending
}.map{
self.timelineEntryForHoliday($0)
}
handler(entries)
}
func getTimelineEntriesForComplication(complication: CLKComplication,
afterDate date: NSDate, limit: Int,
withHandler handler: (([CLKComplicationTimelineEntry]?) -> Void)) {
let entries = dataProvider.allHolidays().filter{
date.compare($0.date) == .OrderedAscending
}.map{
self.timelineEntryForHoliday($0)
}
handler(entries)
}
Last but not least, provide the upcoming 3 holidays when you are asked to provide them now:
func getCurrentTimelineEntryForComplication(complication: CLKComplication,
withHandler handler: ((CLKComplicationTimelineEntry?) -> Void)) {
if let first = dataProvider.allHolidays().nextThreeHolidays().first{
handler(timelineEntryForHoliday(first))
} else {
handler(nil)
}
}
func getNextRequestedUpdateDateWithHandler(handler: (NSDate?) -> Void) {
handler(NSDate().plus10Minutes());
}
func getPlaceholderTemplateForComplication(complication: CLKComplication,
withHandler handler: (CLKComplicationTemplate?) -> Void) {
if let holiday = dataProvider.allHolidays().nextThreeHolidays().first{
handler(templateForHoliday(holiday))
} else {
handler(nil)
}
}
See Also