In our app HealthFace, it is possible to fully customise how Apple Watch complications display your data from HealthKit.
On most of the Apple Watch watch faces, there are multiple complications displayed. For example, on the Utility watch face, there is the "Utility Small" (top left) and "Utility Large" sizes (bottom centre).
This means you could display, say, your water consumption in one position, and the number of mindful minutes you've recorded in the other position.
With the recent update to HealthFace, we wanted to add an option to let you quickly insert data when you tap a complication.
For example:
- Tapping the water consumption complication should allow you to record water
- Tapping the mindful minutes complication should allow you to record mindful minutes.
Unfortunately, WatchKit/ClockKit don't directly provide functionality to determine which complication was tapped.
There are, however, some things we do know.
Firstly, we can determine which complications are showing. Even though there are 7 different positions, we can narrow it down to the two positions that are displaying.
if let activeComplications = CLKComplicationServer.sharedInstance().activeComplications {
// Based on the above configuration, activeComplications will have two entries,
// with the following `.family` property:
//
// - CLKComplicationFamily.utilitarianSmall
// - CLKComplicationFamily.utilitarianLarge
}
Secondly, we can determine when the app is launched by tapping a complication (as opposed to being launched from the app launcher, the dock, or a notification). The presence of CLKLaunchedTimelineEntryDateKey
indicates the app was launched from a complication.
class ExtensionDelegate: NSObject, WKExtensionDelegate {
// ...
// Triggered when a complication is tapped
func handleUserActivity(_ userInfo: [AnyHashable : Any]?) {
if let date = userInfo?[CLKLaunchedTimelineEntryDateKey] as? Date {
// We now know the app was definitely
// launched by tapping the complication
}
}
}
Note: We wrote a blog post about this previously.
One thing we didn't realise though: the CLKLaunchedTimelineEntryDateKey
value doesn't represent the time on the watch face when the user tapped the complication.
Thanks to this post on StackOverflow, we realised we could manipulate the timestamp of the entry to reverse-engineer the current watch face.
The date passed via CLKLaunchedTimelineEntryDateKey
represents the timestamp stored with the complication data that is displaying on the watch face at the time.
class ComplicationController: NSObject, CLKComplicationDataSource {
// ...
func getCurrentTimelineEntry(for complication: CLKComplication, withHandler handler: @escaping (CLKComplicationTimelineEntry?) -> Void) {
let template = ... // Create the template
let date = Date()
let entry = CLKComplicationTimelineEntry(date: date, complicationTemplate: template)
handler(entry)
}
// ...
}
So in this case, it represents the value in the date
variable. This means we can manipulate this value to embed the specific complication family.
To do so, I created this Date
extension. Note that you can't manipulate milliseconds directly, with DateComponents
, but as this solution doesn't work with nanoseconds, we convert nanoseconds to milliseconds (and vice-versa).
extension Date {
func encodedForComplication(family: CLKComplicationFamily) -> Date? {
let calendar = Calendar.current
var dc = calendar.dateComponents(in: calendar.timeZone, from: self)
dc.nanosecond = family.rawValue.millisecondsToNanoseconds
return calendar.date(from: dc)
}
}
extension Int {
var millisecondsToNanoseconds: Int {
return self * 1000000
}
}
This code manipulate the number of milliseconds based on the enum rawValue
of the watch face. For example, CLKComplicationFamily.utilitarianSmall
has a rawValue
of 2, so if you had the time of 12:30:25
, this would make it 12:30:25.02
.
You can now create your complication timeline entry in a slightly different way:
class ComplicationController: NSObject, CLKComplicationDataSource {
// ...
func getCurrentTimelineEntry(for complication: CLKComplication, withHandler handler: @escaping (CLKComplicationTimelineEntry?) -> Void) {
let template = ... // Create the template
let date = Date().encodedForComplication(family: complication.family)
let entry = CLKComplicationTimelineEntry(date: date, complicationTemplate: template)
handler(entry)
}
// ...
}
When creating date
, the value is modified using encodedForComplication(:)
.
Conversely, in order to make use of this value, you need to decode the complication family from the supplied date in CLKLaunchedTimelineEntryDateKey
. To aid with this, I also created the following extensions, which reverses the above.
extension Date {
var complicationFamilyFromEncodedDate: CLKComplicationFamily? {
let calendar = Calendar.current
let ns = calendar.component(.nanosecond, from: self)
return CLKComplicationFamily(rawValue: ns.nanosecondsToMilliseconds)
}
}
extension Int {
var nanosecondsToMilliseconds: Int {
return Int(round(Double(self) / 1000000))
}
}
When you receive a Date
object, you can access the complicationFamilyFromEncodedDate
property to see which complication was encoded.
You can now modify handleUseActivity
to use this new extension:
class ExtensionDelegate: NSObject, WKExtensionDelegate {
// ...
// Triggered when a complication is tapped
func handleUserActivity(_ userInfo: [AnyHashable : Any]?) {
if let date = userInfo?[CLKLaunchedTimelineEntryDateKey] as? Date {
if let family = date.complicationFamilyFromEncodedDate {
switch family {
case .utilitarianSmall:
// Handle the utility small complication being tapped
case .utilitarianLarge:
// Handle the utility large complication being tapped
default:
// Also handle the others...
}
}
}
}
}
As a fallback option, you can also use CLKComplicationServer.sharedInstance().activeComplications
. If this only returns a single entry, then you know that's the one you want.
Referring back to HealthFace, we can now show the appropriate screen based on which complication the user taps:
The great thing about this is that with just a couple of taps, new data can be recorded into HealthKit then displayed directly on the watch face.