iOS 9 Swift Programming Cookbook (2015)
Chapter 2. Apple Watch
2.8 Constructing Small Complications with Text and Images
Problem
You want to construct a small-modular complication and provide the user with past, present and future data. In this example, a small modular complication (Figure 2-39, bottom left) shows the current hour with a ring swallowing it. The ring is divided into 24 sections and increments for every 1 hour in the day. At the end of the day, the ring will be completely filled and the number inside the ring will show 24.
Figure 2-39. Small modular complication (bottom left) showing the current surrounded by a ring
Solution
Follow these steps:
§ Create your main iOS project with a watch target and make sure your watch target has a complication.
§ In your complication, implement the getSupportedTimeTravelDirectionsForComplication(_:withHandler:) method of the CLKComplicationDataSource protocol. In this method, return your supported time travel directions (more on this later). The directions are of typeCLKComplicationTimeTravelDirections.
§ Implement the getTimelineStartDateForComplication(_:withHandler:) method inside your complication class and call the given handler with an NSDate that indicates the start date of your available data.
§ Implement the getTimelineEndDateForComplication(_:withHandler:) method of your complication and call the handler with the last date for which your data is valid.
§ Implement the getTimelineEntriesForComplication(_:beforeDate:limit:withHandler:) method of your complication, create an array of type CLKComplicationTimelineEntry, and send that array into the given handler object. These will be the timeline entries before the given date that you would want to return to the watch (more on this later).
§ Implement the getTimelineEntriesForComplication(_:afterDate:limit:withHandler:) method of your complication and return all the events that your complication supports, after the given date.
§ Implement the getNextRequestedUpdateDateWithHandler(_:) method of your complication and let watchOS know when it has to ask you next for more content.
Discussion
When providing complications, you are expected to provide data to the watchOS as the time changes. In our example, for every hour in the day, we want to change our complication. So each day we’ll return 24 events to the runtime.
With the digital crown on the watch, the user can scroll up and down while on the watch face to engage in a feature called “time travel”. This allows the user to change the time known to the watch just so she can see how various components on screen change with the new time. For instance, if you provide a complication to the user that shows all football match results of the day, the user can then go back in time a few hours to see the results of a match she has just missed. Similarly, in the context of a complication that shows the next fast train time to the city where the user lives, she can scroll forward, with the digital crown on the watch face, to see the future times that the train leaves from the current station.
The time is an absolute value on any watch, so let’s say that you want to provide the time of the next football match in your complication. Let’s say it’s 14:00 rightnow and the football match starts at 15:00. If you give 15:00 as the start of that event to your complication, watchOS will show the football match (or the data that you provide for that match to your user through your complication) to the user at 15:00, not before. That is a bit useless, if you ask me. You want to provide that information to the user before the match starts so she knows what to look forward to, and when. So keep that in mind when providing a starting date for your events.
watchOS complications conform to the CLKComplicationDataSource protocol. They get a lot of delegate messages from this protocol calling methods that you have to implement even if you don’t want to return any data. For instance, in thegetNextRequestedUpdateDateWithHandler(_:) method, you get a handler as a parameter that you must call with an NSDate object, specifying when you want to be asked for more data next time. If you don’t want to be asked for any more data, you have to still call this handler object but with a nil date. You’ll find out soon that most of these handlers ask for optional values, so you can call them with nil if you want to.
While working with complications, you can tell watchOS which directions of time travel you support, or if you support time travel at all. If you don’t support it, your complication returns only data for the current time. And if the user scrolls the watch face with the digital crown, your complication won’t update its information. I don’t suggest you opting out of time travel unless your complication really cannot provide relevant data to the user. Certainly, if your complication shows match results, it cannot show results for matches that have not happened. But even then, you can still support forward and backward time travel. If the user chooses forward time travel, just hide the scores, show a question mark, or do something similar.
As you work with complications, it’s important to construct a data model to return to the watch. What you usually return to the watch for your complication is either of type CLKComplicationTemplate or of type CLKComplicationTimelineEntry. The template defines how your data is viewed on screen. The timeline entry only binds your template (your visible data) to a date of type NSDate that dictates to the watch when it has to show your data. As simple as that. In the case of small modular complications, you can provide the following templates to the watch:
CLKComplicationTemplateModularSmallSimpleText
Has just text.
CLKComplicationTemplateModularSmallSimpleImage
Has just an image.
CLKComplicationTemplateModularSmallRingText
Has text inside a ring that you can fill from 0 to 100%.
CLKComplicationTemplateModularSmallRingImage
Has an image inside a ring that you can fill.
CLKComplicationTemplateModularSmallStackText
Has two lines of code, the second of which can be highlighted.
CLKComplicationTemplateModularSmallStackImage
Has an image and a text, with the text able to be highlighted.
CLKComplicationTemplateModularSmallColumnsText
Has a 2x2 text display where you can provide 4 pieces of textual data. The second coloumn can be highlighted and have its text alignment adjusted.
As you saw in Figure 2-39, this example bases our small-modular template on CLKComplicationTemplateModularSmallRingText. So we provide only a text (the current hour) and a value between 0 to 1 that will tell watchOS how much of the ring around our number it has to fill (0..100%).
Let’s now begin defining our data for this example. For every hour, we want our template to show the current hour. Just before midnight, we provide another 24 new complication data for that day to the watch. So let’s define a data structure that can contain a date, the hour value, and the fraction (between 0..1) to set for our complication. Start off by creating a file called DataProvider.swift and write all this code in that:
protocol WithDate{
var hour: Int {get}
var date: NSDate {get}
var fraction: Float {get}
}
Now we can define our actual structure that conforms to this protocol:
struct Data : WithDate{
let hour: Int
let date: NSDate
let fraction: Float
var hourAsStr: String{
return "\(hour)"
}
}
Later, when we work on our complication, we will be asked to provide, inside the getCurrentTimelineEntryForComplication(_:withHandler:) method of CLKComplicationDataSource, a template to show to the user for the current time. We are also going to create an array of 24 Data structures. So it would be great if we could always, inside this array, easily find the Data object for the current date.
extension NSDate{
func hour() -> Int{
let cal = NSCalendar.currentCalendar()
return cal.components(NSCalendarUnit.Hour, fromDate: self).hour
}
}
extension CollectionType where Generator.Element : WithDate {
func dataForNow() -> Generator.Element?{
let thisHour = NSDate().hour()
for d in self{
if d.hour == thisHour{
return d
}
}
return nil
}
}
NOTE
The dataForNow() function goes through any collection that has objects that conform to the WithDate protocol that we specified earlier, and finds the object whose current hour is the same as that returned for the current moment by NSDate().
Let’s now create our array of 24 Data objects. We do this by iterating from 1 to 24, creating NSDate objects using NSDateComponents and NSCalendar. Then, using those objects, we construct instances of the Data structure that we just wrote:
struct DataProvider{
func allDataForToday() -> [Data]{
var all = [Data]()
let now = NSDate()
let cal = NSCalendar.currentCalendar()
let units = NSCalendarUnit.Year.union(.Month).union(.Day)
let comps = cal.components(units, fromDate: now)
comps.minute = 0
comps.second = 0
for i in 1...24{
comps.hour = i
let date = cal.dateFromComponents(comps)!
let fraction = Float(comps.hour) / 24.0
let data = Data(hour: comps.hour, date: date, fraction: fraction)
all.append(data)
}
return all
}
}
That was our entire data model. Now let’s move onto the complication class of our watch app. In the getNextRequestedUpdateDateWithHandler(_:) method of the CLKComplicationDataSource protocol to which our complication conforms, we are going to be asked when watchOS should next call our complication and ask for new data. Because we are going to provide data for the whole day, today, we would want to be asked for new data for tomorrow. So we need to ask to be updated a few seconds before the start of the next day. For that, we need anNSDate object that tells watchOS when the next day is. So let’s extend NSDate:
extension NSDate{
class func endOfToday() -> NSDate{
let cal = NSCalendar.currentCalendar()
let units = NSCalendarUnit.Year.union(NSCalendarUnit.Month)
.union(NSCalendarUnit.Day)
let comps = cal.components(units, fromDate: NSDate())
comps.hour = 23
comps.minute = 59
comps.second = 59
return cal.dateFromComponents(comps)!
}
}
Moving to our complication, let’s define our data provider first:
class ComplicationController: NSObject, CLKComplicationDataSource {
let dataProvider = DataProvider()
...
We know that our data provider can give us an array of Data objects, so we need a way of turning those objects into our templates so they that can be displayed on the screen:
func templateForData(data: Data) -> CLKComplicationTemplate{
let template = CLKComplicationTemplateModularSmallRingText()
template.textProvider = CLKSimpleTextProvider(text: data.hourAsStr)
template.fillFraction = data.fraction
template.ringStyle = .Closed
return template
}
Our template of type CLKComplicationTemplateModularSmallRingText has a few important properties:
textProvider of type CLKTextProvider
Tells watchOS how our text has to appear. We never instantiate CLKTextProvider directly, though. We use one of its subclasses, such as the CLKSimpleTextProvider class. There are other text providers that we will talk about later.
fillFraction of type Float
A number between 0.0 and 1.0 that tells watchOS how much of the ring around our template it has to fill.
ringStyle of type CLKComplicationRingStyle
The style of the ring we want around our text. It can be Open or Closed.
Later we are also going to be asked for timeline entries of type CLKComplicationTimelineEntry for the data that we provide to watchOS. So for every Data object, we need to be able to create a timeline entry:
func timelineEntryForData(data: Data) -> CLKComplicationTimelineEntry{
let template = templateForData(data)
return CLKComplicationTimelineEntry(date: data.date,
complicationTemplate: template)
}
In this example, we support forward and backward time travel (of type CLKComplicationTimeTravelDirections) so let’s tell watchOS that:
func getSupportedTimeTravelDirectionsForComplication(
complication: CLKComplication,
withHandler handler: (CLKComplicationTimeTravelDirections) -> Void) {
handler([.Forward, .Backward])
}
NOTE
If you don’t want to support time travel, call the handler argument with the value of CLKComplicationTimeTravelDirections.None.
The next thing we have to do is implement the getTimelineStartDateForComplication(_:withHandler:) method of CLKComplicationDataSource. This method gets called on our delegate whenever watchOS wants to find out the beginning of the date/time range of our time travel. For our example, since we want to provide 24 templates, one for each hour in the day, we tell watchOS the date of the first template
func getTimelineStartDateForComplication(complication: CLKComplication,
withHandler handler: (NSDate?) -> Void) {
handler(dataProvider.allDataForToday().first!.date)
}
Similarly, for the getTimelineEndDateForComplication(_:withHandler:) method, we provide the date of the last event:
func getTimelineEndDateForComplication(complication: CLKComplication,
withHandler handler: (NSDate?) -> Void) {
handler(dataProvider.allDataForToday().last!.date)
}
Complications can be displayed on the watch’s lock screen. Some complications might contain sensitive data, so they might want to opt out of appearing on the lock screen. For this, we have to implement the getPrivacyBehaviorForComplication(_:withHandler:) method as well. We call the handler with an object of type CLKComplicationPrivacyBehavior, such as ShowOnLockScreen or HideOnLockScreen. Since we don’t have any sensitive data, we show our complication on the lock screen:
func getPrivacyBehaviorForComplication(complication: CLKComplication,
withHandler handler: (CLKComplicationPrivacyBehavior) -> Void) {
handler(.ShowOnLockScreen)
}
Now to the stuff that I like. The getCurrentTimelineEntryForComplication(_:withHandler:) method will get called on our delegate whenever the runtime needs to get the complication timeline (the template plus the date to display) for the complication to display no. Do you remember the dataForNow() method that we wrote a while ago as an extension on CollectionType? Well, we are going to use that now:
func getCurrentTimelineEntryForComplication(complication: CLKComplication,
withHandler handler: ((CLKComplicationTimelineEntry?) -> Void)) {
if let data = dataProvider.allDataForToday().dataForNow(){
handler(timelineEntryForData(data))
} else {
handler(nil)
}
}
NOTE
Always implement the handlers that the class gives you. If they accept optional values and you don’t have any data to pass, just pass nil.
Now we have to implement the getTimelineEntriesForComplication(_:beforeDate:limit:beforeDate:) method of our complication delegate. This method gets called whenever watchOS needs timeline entries for data before a certain date, with a maximum of limit entries. So let’s say that you have 1000 templates to return but the limit is 100. Do not return more than 100 in that case. In our example, I will go through all the data items that we have, filter them by their dates, find the ones coming before the given date (the beforeDate parameter), and create a timeline entry for all of those with the timelineEntryForData(_:) method that we wrote:
func getTimelineEntriesForComplication(complication: CLKComplication,
beforeDate date: NSDate, limit: Int,
withHandler handler: (([CLKComplicationTimelineEntry]?) -> Void)) {
let entries = dataProvider.allDataForToday().filter{
date.compare($0.date) == .OrderedDescending
}.map{
self.timelineEntryForData($0)
}
handler(entries)
}
Similarly, we have to implement the getTimelineEntriesForComplication(_:afterDate:limit:withHandler:) method to return the timeline entries after a certain date (afterDate parameter):
func getTimelineEntriesForComplication(complication: CLKComplication,
afterDate date: NSDate, limit: Int,
withHandler handler: (([CLKComplicationTimelineEntry]?) -> Void)) {
let entries = dataProvider.allDataForToday().filter{
date.compare($0.date) == .OrderedAscending
}.map{
self.timelineEntryForData($0)
}
handler(entries)
}
The getNextRequestedUpdateDateWithHandler(_:) method is the next method we need to implement. This method gets called to ask us when we would like to be asked for more data later. For our app we specify the next day, since we have already provided all the data for today:
func getNextRequestedUpdateDateWithHandler(handler: (NSDate?) -> Void) {
handler(NSDate.endOfToday());
}
Last but not least, we have to implement the getPlaceholderTemplateForComplication(_:withHandler:) method that we talked about before. This is where we provide our placeholder template:
func getPlaceholderTemplateForComplication(complication: CLKComplication,
withHandler handler: (CLKComplicationTemplate?) -> Void) {
if let data = dataProvider.allDataForToday().dataForNow(){
handler(templateForData(data))
} else {
handler(nil)
}
}
Now when I run my app on my watch, since the time is 10:24 and the hour is 10, our complication will show 10 and fill the circle around it to show how much of the day has passed by 10:00 (see Figure 2-40).
Figure 2-40. Our complication on the bottom left is showing the hour
And if I engage timetravel and move forward to 18:23, our complication updates itself as well, showing 18 as the hour (see Figure 2-41).
Figure 2-41. The user moves the time to the future and our complication updates itself as well
See Also