In this Apple Watch development tutorial series we are building a watchOS 2 app with a companion iPhone app. Together these apps will allow the user to select a date that they started with our company and view how long remains until their 3 month sabbatical. Along the way we are learning about WatchKit, ClockKit, NSDate formatting and complications. In part 2 of this Apple Watch development tutorial series we successfully added an iPhone app to set the start date and update the watch app with this new information.
A complication is an old watchmaker’s term for any function added to a watch face that provides information other than the time. Showing the day of the month on a traditional mechanical watch is a complication. One of the most powerful aspects of Apple Watch development is being able to put information related to your application in a custom complication right on the face of the watch.
If you haven’t been with us from the start, the finished code from tutorial 2 is what we’re starting from.
First navigate to the General tab of the your project’s WatchKit Extension Target and find the Complications Configuration section. Here de-select all but “Utilitarian Small” from the list of Supported Families:
Each of these complication families are for differently shaped complications that can be placed on the watch face and we’ve removed all but Utilitarian Small to simplify this demo. Once you see how one family works the others make a lot more sense.
Back in the ComplicationController’s getPlaceholderTemplateForComplication method, replace “handler(nil)” with the following:
if complication.family == .UtilitarianSmall { let smallUtil = CLKComplicationTemplateUtilitarianSmallFlat() smallUtil.textProvider = CLKSimpleTextProvider(text: "7 Y") handler(smallUtil) }
This gives your watch extension placeholder information to use when there is no data, such as when the user is selecting the complication in the watch face.
Time travel allows the user to scroll forward and backward in time and see the complication change. We will only be supporting forward time travel, allowing the user see our countdown go down as they travel into the future. In the ComplicationController’s getSupportedTimeTravelDirectionsForComplication, replace “handler(nil)” with “handler([.Forward])”:
func getSupportedTimeTravelDirectionsForComplication(complication: CLKComplication, withHandler handler: (CLKComplicationTimeTravelDirections) -> Void) { handler([.Forward]) }
Since we will only be supporting forward time travel, our start date is easy. Update the ComplicationController’s getTimelineStartDateForComplication method to return today’s date:
func getTimelineStartDateForComplication(complication: CLKComplication, withHandler handler: (NSDate?) -> Void) { handler(NSDate()) }
Since we will be doing calendar calculations and date formatting in several places, create a constant called “userCalendar” and another called “dateFormatter” above the first function at the top of the CompilcationController class:
let userCalendar = NSCalendar.currentCalendar() let dateFormatter = NSDateFormatter()
In order to set the time travel end date we will first create a function in the ComplicationController to calculate the date of our sabbatical in much the same way we did so earlier in the tutorial series:
func getSabbaticalDate(startDate: NSDate) -> NSDate { // set up date formatter dateFormatter.calendar = userCalendar dateFormatter.dateFormat = "yyyy-MM-dd" // create variable to hold 7 years let sevenYears: NSDateComponents = NSDateComponents() sevenYears.setValue(7, forComponent: NSCalendarUnit.Year); // add 7 years to our start date to calculate the date of our sabbatical var sabbaticalDate = userCalendar.dateByAddingComponents(sevenYears, toDate: startDate, options: NSCalendarOptions(rawValue: 0)) // since we get a sabbatical every 7 years, add 7 years until sabbaticalDate is in the future while sabbaticalDate!.timeIntervalSinceNow.isSignMinus { sabbaticalDate = userCalendar.dateByAddingComponents(sevenYears, toDate: sabbaticalDate!, options: NSCalendarOptions(rawValue: 0)) } return sabbaticalDate! }
Add another helper method to ComplicationController to get the start date that we stored in User Defaults from the iPhone app in Tutorial 2:
func getStartDateFromUserDefaults() -> NSDate { let defaults = NSUserDefaults.standardUserDefaults() dateFormatter.dateFormat = "yyyy-MM-dd" var startDate = NSDate() if let dateString = defaults.stringForKey("dateKey") { startDate = dateFormatter.dateFromString(dateString)! } return startDate }
Now use our two new helper methods to set the end date in the ComplicationController’s getTimelineEndDateForComplication function:
func getTimelineEndDateForComplication(complication: CLKComplication, withHandler handler: (NSDate?) -> Void) { let startDate = getStartDateFromUserDefaults() let sabbaticalDate = getSabbaticalDate(startDate) handler(sabbaticalDate) }
Now we will add a helper function to our ComplicationController to create the entries that will be displayed in the complication time line:
func createComplicationEntry(shortText: String, date: NSDate, family: CLKComplicationFamily) -> CLKComplicationTimelineEntry { let smallFlat = CLKComplicationTemplateUtilitarianSmallFlat() smallFlat.textProvider = CLKSimpleTextProvider(text: shortText) let newEntry = CLKComplicationTimelineEntry(date: date, complicationTemplate: smallFlat) return(newEntry) }
Now that we have our helper methods created we can build our timeline entries. First we build the current entry. Update the getCurrentTimelineEntryForComplication function to match:
func getCurrentTimelineEntryForComplication(complication: CLKComplication, withHandler handler: ((CLKComplicationTimelineEntry?) -> Void)) { var shortText = "" if complication.family == .UtilitarianSmall { let startDate = getStartDateFromUserDefaults() let sabbaticalDate = getSabbaticalDate(startDate) let dateComparisionResult:NSComparisonResult = NSDate().compare(sabbaticalDate) if dateComparisionResult == NSComparisonResult.OrderedAscending { // current date is earlier than the end date // figure out how many days, months and years remain until sabbatical let flags: NSCalendarUnit = [.Year, .Month, .Day] let dateComponents = userCalendar.components(flags, fromDate: NSDate(), toDate: sabbaticalDate, options: []) let year = dateComponents.year let month = dateComponents.month let day = dateComponents.day // create string to display remaining time if (year > 0 ) { shortText = String(format: "%d Y | %d M", year, month) } else if (year <= 0 && month > 0) { shortText = String(format: "%d M | &d D", month, day) } else if (year <= 0 && month <= 0 && day > 0) { shortText = String(format: "%d Days", day) } } else if dateComparisionResult == NSComparisonResult.OrderedDescending { // current date is greater than end date. shortText = "0 Days" } // create current timeline entry let entry = createComplicationEntry(shortText, date: NSDate(), family: complication.family) handler(entry) } else { handler(nil) } }
The above function created a single entry representing the current point in the timeline. Now we need to create the rest of the entries in the timeline. We do that by updating the getTimelineEntriesForComplication method:
func getTimelineEntriesForComplication(complication: CLKComplication, afterDate date: NSDate, limit: Int, withHandler handler: (([CLKComplicationTimelineEntry]?) -> Void)) { // Call the handler with the timeline entries after to the given date let startDate = getStartDateFromUserDefaults() let sabbaticalDate = getSabbaticalDate(startDate) let componentDay = userCalendar.components(.Day, fromDate: date, toDate: sabbaticalDate, options: []) let days = min(componentDay.day, 100) var entries = [CLKComplicationTimelineEntry]() // create an entry in array for each day remaining for index in 1...days { let dateComparisionResult:NSComparisonResult = NSDate().compare(sabbaticalDate) if dateComparisionResult == NSComparisonResult.OrderedAscending { // entryDate is the date of the timeline entry for the complication let entryDate = userCalendar.dateByAddingUnit([.Day], value: index, toDate: date, options: [])! let flags: NSCalendarUnit = [.Year, .Month, .Day] let dateComponents = userCalendar.components(flags, fromDate: entryDate, toDate: sabbaticalDate, options: []) // number of years, months, days from the timeline entry until sabbatical date let year = dateComponents.year let month = dateComponents.month let day = dateComponents.day if (year > 0 ) { let entryText = String(format: "%d Y | %d M", year, month) let entry = createComplicationEntry(entryText, date: entryDate, family: complication.family) entries.append(entry) } else if (year <= 0 && month > 0) { let entryText = String(format: "%d M | &d D", month, day) let entry = createComplicationEntry(entryText, date: entryDate, family: complication.family) entries.append(entry) } else if (year <= 0 && month <= 0 && day > 0) { let entryText = String(format: "%d Days", day) let entry = createComplicationEntry(entryText, date: entryDate, family: complication.family) entries.append(entry) } } } handler(entries) }
Test by setting a start date in the iPhone app and then adding your new complication to the Utility watch face on the Apple Watch and you should see results similar to this:
Apple Watch Development Tutorial (Part 4): Countdown App
Touching the complication should open your watch app as well.
Here is the code for this Apple Watch development tutorial. If you are having trouble be sure to check out my article on Apple Watch Developer Tips and Tricks.
Note that if you go back to the iPhone app and change the start date the Apple Watch app will show the correct date but the complication will not be updated. We will solve that issue in part four of this series. Stay tuned!
Check out the other posts in the series: