Make a new project for this app using the iOS App with WatchKit App template. In the New Project dialog, be sure to check the box next to Include Complication, because this will create the infrastructure you need to begin providing complication data. This project is going to focus on reading food-related events from the user’s calendar, so name it “NomNomNom.”
When your project is created by Xcode, it has an empty iPhone app, a WatchKit app, and a WatchKit extension, just as you’d expect. Inside the WatchKit extension are three classes: an empty interface controller named InterfaceController, an empty extension delegate named ExtensionDelegate, and a class named ComplicationController that conforms to the CLKComplicationDataSource protocol. As the name suggests, this protocol is the communication channel through which you’ll give the system your complication data. This class, unlike the others in the Xcode template, is preloaded with all of the methods you’ll need to implement to get your complications working.
As you start to look at the methods in the CLKComplicationDataSource protocol, you’ll notice that they all have one thing in common: their final parameter is named handler and is a closure that takes some form of data. You’ll be using these handlers to send data back to the system. When the user selects your app for his complication, watchOS will create an instance of your ComplicationController class and call these methods to get the complication data. This API design, where the system is responsible for initiating the retrieval of complication data, helps to limit power consumption and streamline the complication-loading process. But how does watchOS know which class to instantiate? To explain that, let’s look at more work the template has done on your behalf.
Select the NomNomNom project from Xcode’s Project Navigator (⌘1); then select the NomNomNom WatchKit Extension target from the list of targets. Under the General tab, the Complications Configuration section is where the metadata for your complications is stored. The Data Source Class setting is what tells watchOS which class it should instantiate to get complication data. Any class that conforms to CLKComplicationDataSource will do here; the ComplicationController class that Xcode created is just a convenience.
Next on this screen is a list of Supported Families. These check boxes indicate which complication families your app supports; since you want your users to be able to select your app’s complication from any watch face, leave them all checked. With this data in place, you’re ready to start your app.
Your complication shows users their food schedule. You’ll filter their calendar for events with words like breakfast, brunch, lunch, supper, and dinner in the title. To get access to the user’s calendar, you’ll use the EventKit framework. On watchOS, access to the user’s calendar is read-only, but that’s fine for your needs. To find the next meal event for the current day, you’ll add a new extension to the EKEventStore in EventKit that returns the next food-related event in the current day.
To find out what “the current day” is, you’ll need to do some date math. Specifically, you’ll need to find out when the next and previous midnight are for a given date, so that you can filter events between those dates. Create a new Swift file in this project called DateMath.swift and create an NSDate extension with these two methods in it:
1: | import Foundation |
- | |
- | extension NSDate { |
- | var midnightBefore: NSDate { |
5: | let calendar = NSCalendar.currentCalendar() |
- | |
- | let components = calendar.components([.Year, .Month, .Day, .Hour, .Minute, |
- | .Second], fromDate: self) |
- | |
10: | components.hour = 0 |
- | components.minute = 0 |
- | components.second = 0 |
- | |
- | return calendar.dateFromComponents(components)! |
15: | } |
- | |
- | var midnightAfter: NSDate { |
- | let calendar = NSCalendar.currentCalendar() |
- | |
20: | let components = calendar.components([.Year, .Month, .Day, .Hour, .Minute, |
- | .Second], fromDate: self) |
- | |
- | components.hour = 0 |
- | components.minute = 0 |
25: | components.second = 0 |
- | |
- | let midnightOf = calendar.dateFromComponents(components)! |
- | |
- | let oneDayComponents = NSDateComponents() |
30: | oneDayComponents.day = 1 |
- | |
- | return calendar.dateByAddingComponents(oneDayComponents, toDate: midnightOf, |
- | options: NSCalendarOptions())! |
- | } |
35: | } |
If this code looks a little heavy handed for something that seems simple, don’t worry—that’s emblematic of date- and time-handling code in general. First, on line 7 you extract components of the date from the current calendar. This does assume that you’re working in the user’s time zone, but since this is for a watch that’s likely already set to the correct time zone, it’ll work for you. Once you have these components, lines 10, 11, and 12 take the hour, minute, and second values and reset them to 0. This gives you midnight at the beginning of the day, and on line 14 you use those components to create a new NSDate representing it. To get midnight at the end of the day, you need to add a day to midnightOf, so you create a new NSDateComponents instance on line 29 and set its day property to 1. Finally, on line 32, you return a new date created by adding oneDayComponents to midnightOf.
Now that you can use your date code, you can write a method on EKEventStore to find food-like events in the user’s calendar. You’ll add an extension to ComplicationController.swift:
1: | import EventKit |
- | |
- | extension EKEventStore { |
- | func requestNextMealEvent(forDate date: NSDate = NSDate(), |
5: | handler: EKEvent? -> Void) { |
- | requestAccessToEntityType(.Event) { (success, error) in |
- | if success { |
- | let timePredicate = self.predicateForEventsWithStartDate(date, |
- | endDate: date.midnightAfter, calendars: nil) |
10: | |
- | let mealPredicates = ["breakfast", "brunch", "lunch", "supper", |
- | "dinner", "snack", "coffee", "tea", "happy hour", "drinks"] |
- | .map { NSPredicate(format: "title contains[cd] %@", $0) } |
- | |
15: | let mealMatchingPredicate = NSCompoundPredicate( |
- | orPredicateWithSubpredicates: mealPredicates) |
- | |
- | let events = self.eventsMatchingPredicate(timePredicate) |
- | .filter { mealMatchingPredicate.evaluateWithObject($0) } |
20: | .sort { return $0.startDate.compare($1.startDate) == |
- | .OrderedAscending } |
- | |
- | handler(events.first) |
- | } |
25: | else { |
- | handler(nil) |
- | } |
- | } |
- | } |
30: | } |
This method takes an NSDate parameter, defaulting to the current date, and a closure to handle success or failure. You can’t just return a calendar event—represented by the EKEvent class in EventKit—because the APIs you’re using will be asynchronous. The first thing you need to do is request access to the user’s events, done on line 6. The user will need to authorize this app before you can access any events, which we’ll cover later in this chapter. Next, on line 8, you create a special NSPredicate to filter calendar events between the given date and midnight on that day—which is where your NSDate extension comes in. This predicate will be enough to filter events based on time.
Next, you need to write some more NSPredicates for filtering—this time on the event title. You’re looking for food events, so you build an array of food event names on line 11 (feel free to add your own!) and use map on line 13 to transform them into NSPredicate instances. The [cd] in the predicate text causes the search to be case- and diacritic-insensitive, so “LUNCH” and “Lunch” will both match “lunch.” On line 15 you construct one predicate from this array of predicates. By using the NSCompoundPredicate(orPredicateWithSubpredicates:) method, you’re making a compound predicate that will match a string including any of these terms. This compound predicate is enough to filter events based on title. All that’s left is to execute a search.
First, you call eventsMatchingPredicate(predicate:) on line 18 to filter events by date. Next, you use filter on line 19 to include only those events that match the title search. Then, on line 21, you sort the array by the event’s startDate property, making the earliest event the first in the array. When this is done, you can call the callback on line 23 with the first item in the array of events. If nothing matched the search, this may be nil. Finally, in the last else clause on line 26, you call the callback with nil if you couldn’t obtain authorization from the user to access their events. This method is now done—given an authorized user, you’ll get back the next event that includes food.
With your data in place, you can provide the actual complications to the system. You’ll start with the Modular Large complication family. Much like Apple’s built-in Calendar complication, yours will display the time of the event and its name. First, you’ll implement getCurrentTimelineEntryForComplication(_:handler:) to provide the complication:
1: | lazy var eventStore = EKEventStore() |
- | |
- | func getCurrentTimelineEntryForComplication(complication: CLKComplication, |
- | withHandler handler: ((CLKComplicationTimelineEntry?) -> Void)) { |
5: | |
- | switch complication.family { |
- | case .ModularLarge: |
- | eventStore.requestNextMealEvent { event in |
- | guard let event = event else { |
10: | handler(self.modularLargeTimelineEntryForNoEvents()) |
- | return |
- | } |
- | |
- | handler(self.modularLargeTimelineEntry(forEvent: event)) |
15: | } |
- | |
- | default: |
- | handler(nil) |
- | } |
20: | } |
The first thing you do in this function, on line 6, is to switch on the family property of the complication argument you’ve been passed. Based on the family, which corresponds to the watch face the user has selected and the position of the complication on that face, you’ll provide different complications. For now you’re going to implement the Modular Large family of complications, but you’ll fill in the rest later. For this family, you can display two types of complications: a calendar event regarding food or a message saying there are no more events (much like the built-in Calendar complication). To that end, you request the next event from your event store on line 8. If you don’t get an event back, your guard statement on line 9 triggers, and you’ll return a timeline entry with a message saying there are no more events. Otherwise, you’ll create a timeline entry for the next event and return it. Notice that instead of just using return to return the timeline entry, you invoke handler with it; this allows you to create timeline entries asynchronously, which greatly helps your ability to use frameworks like EventKit. Now that you’ve written this, you need two methods to return these two timeline entry types. Let’s start with modularLargeTimelineEntryForNoEvents:
1: | func modularLargeTimelineEntryForNoEvents() -> CLKComplicationTimelineEntry { |
- | let template = CLKComplicationTemplateModularLargeStandardBody() |
- | |
- | template.headerTextProvider = CLKSimpleTextProvider(text: |
5: | "Food Calendar") |
- | |
- | template.headerTextProvider.tintColor = .yellowColor() |
- | |
- | template.body1TextProvider = CLKSimpleTextProvider(text: |
10: | "No more food today") |
- | |
- | return CLKComplicationTimelineEntry(date: NSDate(), |
- | complicationTemplate: template) |
- | } |
Every complication timeline entry starts with a template in the form of a CLKComplicationTemplate subclass. Here you’re using CLKComplicationTemplateModularLargeStandardBody on line 2, which gives you room for header text and one or two lines of body text. To provide this text, instead of simple String properties on the template, you use subclasses of CLKTextProvider. This layer of abstraction allows you to give watchOS more context around the text. For now, you’ll just use a CLKSimpleTextProvider to supply a simple string, for both the headerTextProvider and body1TextProvider properties. The CLKComplicationTemplateModularLargeStandardBody template has an optional body2TextProvider property, but if you leave it as nil, the text in body1TextProvider will wrap to both lines of the template. Note also that you supply a tintColor property for the header text provider. In some situations where the user hasn’t chosen his watch face’s color, you can provide a tint color to use. Feel free to use your brand colors, your app’s colors, or whatever looks good—just remember that your users may have settings that result in the tint color you supply being ignored. Finally, on line 12 you create your CLKComplicationTimelineEntry instance, which requires two pieces of data: a date variable, for which you’ll simply use the current date, and the complication template itself. The date property is extremely important for the Time Travel feature we’ll cover later.
Now that you’ve finished, you’ve created a method that returns a template for the case where you can’t find any events. Hopefully, you won’t need it too often. For the case where you can find events on the user’s calendar, you’ll implement modularLargeTimelineEntry(forEvent:):
1: | func modularLargeTimelineEntry(forEvent event: EKEvent) -> |
- | CLKComplicationTimelineEntry { |
- | let template = CLKComplicationTemplateModularLargeStandardBody() |
- | |
5: | template.headerTextProvider = CLKTimeIntervalTextProvider( |
- | startDate: event.startDate, endDate: event.endDate) |
- | |
- | template.headerTextProvider.tintColor = .yellowColor() |
- | |
10: | template.body1TextProvider = CLKSimpleTextProvider(text: event.title) |
- | |
- | if let location = event.location { |
- | template.body2TextProvider = CLKSimpleTextProvider(text: location) |
- | } |
15: | |
- | return CLKComplicationTimelineEntry(date: event.startDate, |
- | complicationTemplate: template) |
- | } |
This method begins the same way as modularLargeTimelineEntryForNoEvents, creating a new CLKComplicationTemplateModularLargeStandardBody, but the text providers you use are very different. Instead of creating a simple, text-only text provider for the header, on line 5 you create a CLKTimeIntervalTextProvider. With this class, you can specify the start and end dates of the EKEvent you’re displaying, and the text provider will automatically format the time interval as appropriate for the amount of space you have. In the case of the Modular Large Standard Body complication template, there’s plenty of room, so a brunch event from 11:00 a.m. to 1:00 p.m. will appear as “11:00 am - 1:00 pm,” all spelled out. As you’ll see later in this chapter, other complication templates will format the time differently to fill less space. The other two text providers are just text, so you create them as before. Finally, on line 16, you return a timeline entry for this event. Instead of using NSDate to get the current date, you use the event’s start date as the timeline entry’s date. This marks it as beginning when the event began. You could just use the current date here, since you’re providing the current timeline entry, but it’s important to have good data on these entries when it comes to time.
With these methods in place, you’re ready to run the app! Build and run the app and you’ll be greeted with the black screen shown in the figure. That’s OK! Since all of the code you’ve written so far has been in your ComplicationController, you’ll only see it from the watch face. To get there, press ⌘⇧H with your watch simulator open. This simulates pressing the Digital Crown button and thus returns you to the home screen. Click the watch icon to open the watch face. On a real Apple Watch, you configure the watch face by using Force Touch on the watch face, leading to the configuration screens in the following figure. But how do you do a Force Touch on the simulator?
To simulate Force Touch, use the Simulator menu option Hardware → Force Touch Pressure → Deep Press (⇧⌘2). This setting acts as a toggle between Shallow Press and Deep Press, so you’ll need to do a little back-and-forth dance between them to configure the complication. First, using Deep Press, tap the watch face until the Customize button appears, as in the preceding figure. Switch back to Shallow Press using the menu or ⇧⌘1, and then scroll left and right until you find the Modular watch face. If you don’t see it, head to the rightmost screen with the Add button; then tap it and select Modular. When you’re on the Modular face and the Customize button is at the bottom, tap it to reveal two pages of configuration. The left page allows you to select the color of the watch face (or Multicolor, which allows the tint color you selected to come through), while the right page allows you to configure the individual complications. Clicking a complication slot moves the selection picker to that slot. For the Modular face, there are four small slots, the time in the upper-right corner (which you can’t remove), and one large slot in the middle. The middle one is where your Modular Large template goes, so click that. Scroll up and down with your mouse or trackpad to cycle between the complications. Yours will appear as NomNomNom WatchKit App. Once the watch face is configured to your desire, switch back to a Deep Press to click and exit customization mode, and then switch to Shallow Press to select the watch face (you can also short-circuit this by pressing ⌘⇧H). You’re back on the watch and your complication is visible! In all likelihood, you’ll see the “No more food today” message as seen in the following figure, unless your simulator’s calendar has some food events on it.
Now that your complication is installed and working, let’s make it a little easier to update. In Xcode, click the Scheme List in the toolbar. By default, the WatchKit app scheme is selected, which is why you opened the app to a black screen earlier. Choose the Complication - NomNomNom WatchKit App scheme, then build and run again, and you’ll see that the watch simulator opens directly to the complication! It also updates the complication immediately, which is very useful for debugging purposes.
To test the complication, add some data to the simulator’s calendar. On the iPhone simulator, open the Calendar app and add a food-related event between now and midnight. Build and run again with the complication scheme selected, and voilà! Your calendar event is now in the complication, as shown in the figure. Now that you’ve created your complication for the Modular Large family, let’s look at providing a complication for another watch face entirely.
Luckily, supporting the second complication family isn’t going to require anywhere near the effort the first one took. You’ll be able to reuse a good deal of code. In fact, you’ll only need to rewrite the bits that deal with the complication templates themselves. Your second supported family will be for the Utilitarian clock face—the Utilitarian Large template. Like the stock Calendar app, you’ll use the text area underneath the Utility watch face’s watch face to write a description of the next event. To begin, let’s mirror your Modular Large complication methods with a couple of new ones, utilitarianLargeTimelineEntryForNoEvents and utilitarianLargeTimelineEntry(forEvent:). You’ll start with the former:
| func utilitarianLargeTimelineEntryForNoEvents() |
| -> CLKComplicationTimelineEntry { |
| let template = CLKComplicationTemplateUtilitarianLargeFlat() |
| |
| template.textProvider = CLKSimpleTextProvider(text: |
| "No more food today", shortText: "No more food") |
| |
| return CLKComplicationTimelineEntry(date: NSDate(), |
| complicationTemplate: template) |
| } |
Just like the other complication family, this one is fairly straightforward. In fact, it’s even easier. Since the Utilitarian Large Flat template has room for only one line of text, you only need to set the textProvider property. This time around, you’re creating a CLKSimpleTextProvider with an additional parameter, shortText. This parameter allows watchOS to determine if the string is too long to display, using the shorter variation if necessary. That’s it! These three lines are enough for a complication when there are no more events. But what about when there is an event? You have only one line of text to work with, so how will you get all of the relevant data in it?
The answer lies in CLKTextProvider’s textProviderWithFormat(_:…) method, which allows you to combine multiple text providers into one! By putting multiple providers together, you can create a text provider that reports both the event time and its title, all on one line. Plus, since the time interval text provider can scale to multiple widths, it will automatically switch its display mode to accommodate longer event titles. Neat! One problem: you can’t call this method from Swift. Let’s look at how it’s declared in Objective-C:
| + (CLKTextProvider *)textProviderWithFormat:(NSString *)format, ... |
It’s the ellipsis at the end that gets you. Variadic methods in Objective-C can’t be imported directly into Swift, so to use this API, you’re going to have to write a little bit of Objective-C. In Xcode, select File → New → File… and choose the Objective-C File template under iOS Source. Set the File Type to Category, the class to CLKTextProvider, and the File to SwiftAdditions. Make sure that the WatchKit Extension target is selected; then click Create. Xcode will ask if you want to make a bridging header to expose Objective-C methods to Swift, as you can see in the screenshot.
By choosing Create Bridging Header, you will create three new files in the project: CLKTextProvider+SwiftAdditions.h, CLKTextProvider+SwiftAdditions.m, and NomNomNom WatchKit Extension-Bridging Header.h. To be able to use your Objective-C code in Swift, you’ll need to add it to the bridging header. Add the following line:
| #import "CLKTextProvider+SwiftAdditions.h" |
Now, anything you define in CLKTextProvider+SwiftAdditions.h will be available to your project in Swift. Open that file and declare the method you’ll be adding:
| NS_ASSUME_NONNULL_BEGIN |
| |
| @interface CLKTextProvider (SwiftAdditions) |
| |
| + (CLKTextProvider *)nnn_textProviderByJoiningProvider:(CLKTextProvider *)provider1 |
| andProvider:(CLKTextProvider *)provider2 |
| withString:(NSString *)string; |
| |
| @end |
| |
| NS_ASSUME_NONNULL_END |
First, you use NS_ASSUME_NONNULL_BEGIN to indicate that the return values and parameters specified below are all assumed to not be nil. This prevents Swift from importing these as implicitly unwrapped optionals or regular optionals, which makes it easier to use. You’ll use NS_ASSUME_NONNULL_END at the end to clean this up. Next, you define one method on CLKTextProvider. As is customary in Objective-C, you use a lowercase, three-character prefix on the method name. This prevents problems if future updates or other code creates a method with the same name, since Objective-C doesn’t have namespaces. The method, nnn_textProviderByJoiningProvider:andProvider:withString:, will take two CLKTextProvider instances and one NSString. Let’s switch over to the implementation file, CLKTextProvider+SwiftAdditions.m, and implement the method:
| @implementation CLKTextProvider (SwiftAdditions) |
| |
| + (CLKTextProvider *)nnn_textProviderByJoiningProvider:(CLKTextProvider *)provider1 |
| andProvider:(CLKTextProvider *)provider2 |
| withString:(NSString *)string |
| { |
| return [CLKTextProvider textProviderWithFormat:@"%@%@%@", |
| provider1, string, provider2]; |
| } |
| |
| @end |
From Objective-C, there’s no problem calling textProviderWithFormat: on CLKTextProvider. The format string you provide has three %@ tokens: one for the first provider, then one for the string that joins the two, and finally one for the second provider. Now that you’ve added this method, let’s go back to the nice, friendly Swift code you’ve been writing! Return to ComplicationController.swift, where you can finally implement utilitarianLargeTimelineEntry(forEvent:) with your Objective-C helper:
| func utilitarianLargeTimelineEntry(forEvent event: EKEvent) -> |
| CLKComplicationTimelineEntry { |
| let template = CLKComplicationTemplateUtilitarianLargeFlat() |
| |
| let timeTextProvider = CLKTimeIntervalTextProvider( |
| startDate: event.startDate, endDate: event.endDate) |
| let titleTextProvider = CLKSimpleTextProvider(text: event.title) |
| |
| template.textProvider = CLKTextProvider |
| .nnn_textProviderByJoiningProvider(timeTextProvider, |
| andProvider: titleTextProvider, withString: " ") |
| |
| return CLKComplicationTimelineEntry(date: event.startDate, |
| complicationTemplate: template) |
| } |
At first, this method seems just like the one you wrote for the Modular watch face. You make a template and then make a pair of text providers for the event’s time and title. You combine them into one text provider and then set it as the template’s text provider. Now that you have this method implemented, let’s get it working. Return to your getCurrentTimelineEntryForComplication(_:handler:) implementation and add another case statement for this family:
| case .UtilitarianLarge: |
| eventStore.requestNextMealEvent { event in |
| guard let event = event else { |
| handler(self.utilitarianLargeTimelineEntryForNoEvents()) |
| return |
| } |
| |
| handler(self.utilitarianLargeTimelineEntry(forEvent: event)) |
| } |
Build and run to get back to the watch face. Using a Deep Press touch, customize the watch face, selecting the Utility face and then customizing it to add your complication along the bottom. Huzzah! Your event appears as in the screenshot, with both its time and its title! This is great for text, but what about images? Next, let’s look at image support in complications.
So far, your complications have dealt entirely in text. There’s nothing wrong with that—they convey plenty of information and look great on the user’s watch face. Providing images, however, opens up your app to a wider world of functionality. While the complication families you’ve seen so far are great for text, the smaller ones on these watch faces are a bit cramped for this kind of information. They can fit a word at best and a few measly characters at worst. When it comes to packing information into a dense space, an image is a great idea.
Just like providing text with a CLKTextProvider, you’ll provide images using a CLKImageProvider. These images must be template images—watchOS will use their alpha channel only, drawing them at whatever color the user specifies (for exact image dimensions, refer to the ClockKit documentation). You can provide one-piece or two-piece images; the latter can have a foreground and background image, giving you a two-tone appearance (even though you can’t always pick the colors). For some watch faces with a Multicolor option, a tintColor property is available. Use this to specify the default color for your image, but don’t be surprised to see it in another color if the user has one selected. The two-piece images are used only in multicolor environments, so you’ll still need to provide a one-piece image.
We’ve covered providing data for complications, but there are many more methods in CLKComplicationDataSource. Next, let’s look at how to control the privacy of complication data.
3.145.97.104