-
Create complications for Apple Watch
When you add complications to a Watch app, people can access glanceable and up to date information directly from their watch face. We'll show you how to create and build complications from the ground up and introduce you to Multiple Complications. Learn how to construct timelines, use families and templates, and discover best practices on crafting a thorough complication experience.
리소스
관련 비디오
WWDC20
-
다운로드
Hello everyone. My name is Michael Kent, and I'm a ClockKit engineer. In this session, we'll be talking about complications on Apple Watch and how you can add them to your app. Complications are a great way to get your content in front of your users. They're easily glanceable right on the clock face. With watchOS 7, we've made it even easier than ever to create a great complication experience, and that's why complications are as important as the app experience on the watch. They're the easiest way to get your content in front of the user. How can you add them to your app? We'll be talking about timelines, which are the backing structure of complications, all the bits and pieces you need to get going, how you can get your data into complications, how you can utilize a new watchOS 7 feature to provide more than one complication from your app, and we'll run through an example. We'll start out talking about timelines.
Timelines play a central role in complications. They're a representation of your complication's data over time.
Since a timeline provides all of your complication's data, ClockKit can ask your app once for all the information needed to drive your complication automatically through a date you specify.
Of course, if you get new data in your app and need to give us more entries, you can ask ClockKit to extend your timeline or completely invalidate it.
I've been working with some teammates to develop an app to track whale sightings around Hawaii. In the app, you can see recent whale sightings at several viewing stations around Maui. Let's look at an example of a timeline that might come from this app.
Here I've got a schedule of whale-watching tours, three throughout the day at different locations.
Now here's how it might look as a timeline presented to ClockKit. There are a few differences you'll notice. First, each tour is associated with only one time rather than a range. This is because your complication will show an entry until the date of the next one. Second, you'll see that the times associated with each tour are earlier than their actual start time. This is so that a user has a bit of a heads-up in order to catch the boat for the tour on time if they're interested. Not all types of data would behave like this. A temperature forecast, for example, would want to have entries right at each forecasted time. Up top is the data I'd like to show in this complication at the current time. And as that progresses throughout the day, you can see the information updates as the time changes until there are no more tours for today.
Now, let's get into how you actually specify what you want your complications to display.
Families of complications are how we break them up into different visual groups. The graphic families were introduced in watchOS 5, with the exception of the Graphic Extra Large, which is new in watchOS 7.
These families allow for more visually oriented presentations of data. Different watch faces use different complication families. Here are some examples.
The Infograph face uses Graphic Corner and Graphic Bezel as well as Graphic Circular, which is also on the Infograph Modular face, along with Graphic Rectangular.
This is Graphic Extra Large, which is now supported on the Extra Large face.
Modular Small and Modular Large on the Modular face, Utilitarian Small Flat on the Motion face, Utilitarian Small and Large on the Utility face, and lastly, Extra Large, which is used on the Extra Large face if your complication doesn't support the Graphic Extra Large family. This family is always used on the Extra Large face for Series 3 watches. Ideally, you will want to support as many complication families as you can for your complications. This way, all of your users will be able to use your complications on whichever face they prefer.
Complication templates represent different visual layouts within families. Here's an example of just some of the complication templates available in a few different families.
Despite being associated with a specific family, each template inherits from a common base class, CLKComplicationTemplate.
There are a lot of options to choose from to best represent the data that you want to show. You can find out more about what's available in the documentation on the Developer website. Now let's look at a brief example of what the code looks like to supply a timeline. The first type I want to talk about is CLKComplicationTimelineEntry. You populate your complication's timeline by giving us a list of these. Each represents what your complication should look like at a certain point in time.
It only has two properties: date, which is the date that this entry should be visible at, and complicationTemplate, which is the template containing the data you want to display for this entry. Going back to our timeline example from before, at 6:00 a.m., we'll have a timeline entry populated with the date, 6:00 a.m., and a complicationTemplate representing the Ma'alaea Harbor Tour. At 9:00 a.m., the date and template change, as well as throughout the rest of the day. Your main interaction with ClockKit is through an object you create that conforms to CLKComplicationDataSource. There's only one required method in this protocol: getCurrentTimelineEntry for complication withHandler.
You can implement this by simply calling the handler with the current timeline entry for the given complication. We'll talk more about what this complication is later.
For some complications, a current entry is enough, like the current score in a baseball game or the current Apple stock price. But if your complication can provide a timeline with entries in the future, you'll need to implement these two methods as well: getTimelineEndDate for complication withHandler, which specifies how far in the future you can provide entries, and getTimelineEntries for complication after date limit withHandler. That's a bit of a mouthful, but all you need to do is provide as many entries as is appropriate for the data you have up to the limit after the given date. The date represents the last timeline entry that we already have, and the limit is so that we only get as many entries as we need at one time. If you provide more, we'll drop all of the entries over the limit.
What if your app gets new data, and you need to reload your timeline? We've got two options.
First, if your data changes completely, and all of the entries you provided are invalid, you can call reloadTimeline for complication on the shared instance of CLKComplicationServer.
For example, if an unexpected storm came through Maui, all our whale-watching tours would be canceled, and we'd want to invalidate our whole timeline to reflect that.
But if your previously provided entries are still valid, and you just want to let us know you can provide more, call extendTimeline for complication.
ClockKit only keeps track of timelines for complications that are currently on the user's watch face. We let you know about these via the activeComplications property.
You don't ever create CLKComplication objects directly. You always get them from here or passed into a data source method you implement.
Now let's talk about what really goes into creating these complication templates.
Complications have several limitations: Watch screens are small, complications are smaller. You may want to display the same string in different templates or families of complications, all with very different layout constraints.
To give the best possible experience, we have a concept called data providers. These allow you to consistently express the same information in many different locations and contexts, formatted for you by ClockKit. Since we take care of the specific layout details of complications on your behalf, we need enough information to do so flexibly. Let me show you what I mean. We'll start with some text. Watches are all about time, so showing a date is something you may want to do often. Here's a nice way to write out a long date.
But in most complication contexts, you'd end up with something like this. That's not very clear what it's saying. So how can we do better? You would use a data provider called CLKDateTextProvider. You declare what you would like, in this case, "Wednesday, September 23," and we'll do the best we can to show it.
But if space is constrained, we'll start falling back to shorter versions...
eventually dropping less-useful information like the weekday, and if needed, all the way down to the day of the month.
Here's what that would look like in code. You create a CLKDateTextProvider by giving it a date and the calendar units you'd prefer in the longest case. What about if you wanted to answer questions like, "How far is my date from the current time?" Or "How far is my date from some other date?" You could use a CLKRelativeDateTextProvider. In the case of the first question, a relative date text provider will auto-update its text for whatever the current time is. This can be accurate down to the second with no extra work on your part.
You can use it for different formats, displaying strings like this, the time from now until sunset, or this: how much longer is left for the dough you're making to finish rising.
In the latter case, you'd create it by providing an end date, specifying the "timer" style and giving the units you'd like displayed.
Here are some other text providers, like the time text provider, which acts very similarly to the date text provider but shows a time rather than a day, time interval text provider, which can be used to show a range of time, like 7:30 to 9:00 a.m., and lastly, simple text provider, which displays any string you'd like. You can also give it a shorter version of that string to fall back on in contexts where space is constrained.
Image providers are a lot like text providers in that the data they contain can be used across several different contexts.
The big difference is that these contexts are more focused on the color of the watch face. Many watch faces allow users to customize this attribute, and complications need to match consistently.
Some watch faces apply a single color to the whole image of the complication, and others allow a more multicolor image, composed of a background and foreground.
You can see some here on the bottom: timer, sunrise and stopwatch.
CLKImageProvider is the object that makes this possible. The graphic complication families allow displaying full-color images, so their templates ask for CLKFullColorImageProviders.
However, these graphic complications are tinted in some contexts. If you only provide a full-color image, we'll desaturate it for you to apply this tint color.
If instead you want to override this behavior, CLKFullColorImageProvider allows you to set a CLKImageProvider to fall back on.
You can see here that by providing the same image with a CLKImageProvider, the complication appears brighter, with the same white color as the other complications on the top left.
For more about adapting to complication tinting, check out this great talk from 2019.
Gauge providers are a way to encapsulate the data necessary to show a graphical gauge or progress. These adapt to different complication layouts, like those seen here, in both the corners and the center.
You can customize the color or gradient of the gauge, as well as the fill fraction, all with minimal work on your part.
If you want a gauge that updates its fill fraction in real time, you can use a CLKTimeIntervalGaugeProvider, which lets you specify a start and end date, and it'll be updated automatically to show the progress at the current time.
New in watchOS 7, you can use SwiftUI in complications. All of the complication templates that use CLKFullColorImageProviders have SwiftUI view alternatives.
You can easily reuse components from other parts of your app, and it's even easier than ever to stand out and create unique complications to engage your users.
If you want to learn more about using SwiftUI in complications, we've got a wonderful session that covers this in depth.
What if your app is full of useful, relevant information to your users, and you want to provide them quick, easy access to all of it? Also new in watchOS 7, you can now provide multiple complications from a single app. This is a great way to get your data in front of your users at a quick glance down to their wrist. You can even fill a watch face with your own complications and share it.
Multiple complication support is declared in your implementation of CLKComplicationDataSource. There are two relevant methods: getComplicationDescriptors withHandler, which specifies the current list of complications that your app supports...
and handleShared ComplicationDescriptors, which will be called when a watch face containing some of your complications was shared with this watch in order to give you a heads-up that ClockKit will start asking for timeline entries for them.
A CLKComplicationDescriptor is how you define a complication. It consists of an identifier unique within your app...
a displayName, which will be shown during watch face editing, a list of complication families that this complication supports, and two optional and mutually exclusive properties that allow you to include custom data for your use later: a userInfo dictionary or a userActivity.
Our Whale Watch app has support for a few different kinds of complications: some for the number of whale sightings at each location, a complication to quickly log a new sighting, and a complication to show the overall season data. Let's look at how this works.
In the getComplicationDescriptors withHandler method, you'll create an array of ComplicationDescriptors and use it to call the handler.
Here, we iterate through each station in our data model, creating a ComplicationDescriptor for each.
These will be used to show sighting information at each location.
Next, we add a descriptor for a complication to log a new whale sighting and one to view overall season data. You'll notice that this last complication only supports the graphicRectangular family, while the others supported all families.
Since the season data complication will display a lot of information, the large canvas of the graphicRectangular family is the only one that made sense for this complication. If you ever need to invalidate this list, you can call reloadComplicationDescriptors on CLKComplicationServer, and we'll call this method again. In our Whale Watch example, maybe we only show complications for the user's favorite viewing stations. If they update those, we'll want to update the complications as well.
If you ever update this list to remove support for a complication that a user currently has on their watch face, we'll continue to ask you for timeline entries for that complication. Do your best to ensure that you can continue to provide useful information in this case.
Note that this method is different from CLKComplicationServer's reloadTimelineForComplication method. This reloads the list of complications your app offers, where the other reloads a specific complication's timeline.
Just like always, when a user taps your complication, we'll launch your app. If the tapped complication's descriptor was created with a userActivity, then it will be used when launching it.
In either case, we pass some entries in the userInfo dictionary: the date of the complication's currently visible timeline entry as well as the complication's identifier.
And of course, this dictionary contains developer-specified entries defined in the complicationDescriptor as well.
We've talked about how to describe your supported complications, but how do you know what complication ClockKit is asking for timeline entries for? Here are a couple methods that we saw earlier: getCurrentTimelineEntry and getTimelineEndDate. Each takes a complication parameter of type CLKComplication.
This looks a lot like a complicationDescriptor. However, where a complicationDescriptor defines what a complication supports, this object represents a specific, concrete instance of a complication on a user's watch face. So instead of a list of supported families, it has a property containing the family of the real complication instance. And of course, the information you provided in the descriptor with a userInfo dictionary or a userActivity is contained here as well.
We have something called the default complication identifier. Is this something you should be using to identify one of your complications? Sure, you could, but that's not its main purpose.
If you had a complication before watchOS 7, and a user has it on their watch face, or if a user shares a watch face with your complication but chooses to remove the associated data, then you'll get asked about a complication with the identifier CLKDefaultComplicationIdentifier even if you don't explicitly support it in your list of complication descriptors. This is very important. You should support this complication. If not, your users will be wondering why they're seeing a broken complication on their watch face that says it's from your app.
So how can you do that? You could display the same information as your complication did before watchOS 7, or maybe you could choose to show the most popular or relevant information from your app, or you could just show your app icon, so at the very least, the user knows what the complication is and has an idea of what would happen if they tapped on it. Now let's look at more of the specifics of our whale-watching app. We saw this method before. It creates the current entry for the given complication.
Here's another method that we haven't looked at yet: getLocalizableSampleTemplate for complication withHandler is also a part of the CLKComplicationDataSource protocol.
The template it's asking for will be used while selecting complications in face editing as well as in the Apple Watch app on the paired iPhone. This template should contain sample data, as we only ask for it once per complication and cache the result.
We've seen this createTimelineEntry method used a few times now. It's pretty simple. It creates a template and then a timeline entry with that template and the date passed into it.
Let's take a closer look at the createTemplate method. We first grab some data that we'll need to reuse while creating templates. We pull the station information from the complication. Remember, we created the descriptors with it.
And we create a FullColorImageProvider to use in a few different cases. And we create a SimpleTextProvider for logging a sighting.
Lastly, we create a closure that returns a default template for a given family that we can fall back on if something unexpected happens, like being asked for a template for the default complication identifier.
To determine which template to create, we switch on both the family and the identifier.
For the SeasonData complication in Graphic Rectangular, we create a GraphicRectangularFullView template, which has a SwiftUI view displaying a nice visual chart of the data.
For the "log a sighting" complication in graphicCircular, we create a GraphicCircularStackImage template with our ImageProvider and TextProvider that we created earlier.
For any other kind of graphicCircular complication, we want to show another SwiftUI view that displays information about the sightings at that location. GraphicCorner and GraphicExtraLarge are also very similar, each returning a complication template containing corresponding text and imageProviders from the station data.
There are a lot of other cases here to correctly provide a template for each complication supported, but since they're all similar, I won't go into all the details here.
But of course, if we are getting asked about a complication we don't think we can do anything about, like the default complication identifier, we'll just return a default template. Here's how some of those complications look in action. We have two different kinds of Graphic Rectangular complications, two kinds of Graphic Circular, we used SwiftUI to draw these beautiful charts, and the complications look great in both full-color and tinted contexts.
In watchOS 7, there are so many opportunities to get your data in front of your users right on their watch face.
So make some complications, provide your data in a timeline to keep your complication up to date, create great, customized complication content with SwiftUI, and make sure you support the default complication identifier for when you get asked about them. If you're looking for more information on building great complications, here are some other sessions you can check out. Thanks for joining me.
-
-
4:54 - CLKComplicationDataSource - Required Methods
// CLKComplicationDataSource - Required class ComplicationController: NSObject, CLKComplicationDataSource { func getCurrentTimelineEntry( for complication: CLKComplication, withHandler handler: @escaping (CLKComplicationTimelineEntry?) -> Void) { // Call the handler with the current timeline entry handler(createTimelineEntry(forComplication: complication, date: Date())) } }
-
5:16 - CLKComplicationDataSource - Timeline Support
// CLKComplicationDataSource - Timeline Support extension ComplicationController { func getTimelineEndDate( for complication: CLKComplication, withHandler handler: @escaping (Date?) -> Void) { handler(timeline(for: complication)?.endDate) } func getTimelineEntries( for complication: CLKComplication, after date: Date, limit: Int, withHandler handler: @escaping ([CLKComplicationTimelineEntry]?) -> Void) { handler(timeline(for: complication)?.entries(after: date, limit: limit)) } }
-
8:11 - CLKDateTextProvider initialization
let longDate: Date = DateComponents(year: 2020, month: 9, day: 23).date ?? Date() let units: NSCalendar.Unit = [.weekday, .month, .day] let textProvider = CLKDateTextProvider(date: longDate, units: units)
-
8:49 - CLKRelativeDateTextProvider initialization
let timerStart: Date = … let units: NSCalendar.Unit = [.hour, .minute, .second] let textProvider = CLKRelativeDateTextProvider(date: timerStart, style: .timer, units: units)
-
13:16 - CLKComplicationDataSource - Multiple Complication Support
// CLKComplicationDataSource - Multiple Complication Support extension ComplicationController { var descriptors : [CLKComplicationDescriptor] = [] var dataDict = Dictionary<AnyHashable, Any>() for station in data.stations { dataDict = [“name": station.name, “shortName": station.shortName] descriptors.append( CLKComplicationDescriptor( identifier: station.name, displayName: station.name, supportedFamilies: CLKComplicationFamily.allCases, userInfo: dataDict)) } descriptors.append( CLKComplicationDescriptor( identifier: "LogSighting", displayName: "Log Sighting", supportedFamilies: CLKComplicationFamily.allCases)) descriptors.append( CLKComplicationDescriptor( identifier: "SeasonData", displayName: "Season Data", supportedFamilies: [.graphicRectangular])) // Call the handler with the currently supported complication descriptors handler(descriptors) }
-
17:09 - CLKComplicationDataSource - Sample Templates
func getLocalizableSampleTemplate( for complication: CLKComplication, withHandler handler: @escaping (CLKComplicationTemplate?) -> Void) { let template = createSampleTemplate(forComplication: complication) handler(template) }
-
17:33 - Whale Watch - Entries
func createTimelineEntry( forComplication complication: CLKComplication, date: Date) -> CLKComplicationTimelineEntry? { guard let template = createTemplate(forComplication: complication, date: date) else { return nil } return CLKComplicationTimelineEntry(date: date, complicationTemplate: template) }
-
17:44 - Whale Watch - Templates
func createTemplate( forComplication complication: CLKComplication, date: Date) -> CLKComplicationTemplate? { var station: Station? = nil if let stationName = complication.userInfo?["name"] as? String { station = data.stations.first(where: { $0.name == stationName }) } let image = UIImage(named: "Spout-small")! let spoutFullColorImageProvider = CLKFullColorImageProvider(fullColorImage: image) let logSightingTextProvider = CLKSimpleTextProvider( text: "Log Sighting", shortText: "Log") let defaultTemplate: (CLKComplicationFamily) -> CLKComplicationTemplate = { family -> CLKComplicationTemplate in // Return a default complication template for the given family } switch (complication.family, complication.identifier) { case (.graphicRectangular, "SeasonData"): return CLKComplicationTemplateGraphicRectangularFullView( ChartView( seriesData: data.last7DaysSightings, seriesColor: .turquoise) case (.graphicCircular, "LogSighting"): return CLKComplicationTemplateGraphicCircularStackImage( line1ImageProvider: spoutFullColorImageProvider, line2TextProvider: logSightingTextProvider) case (.graphicCircular, _): guard let station = station else { return defaultTemplate(.graphicCircular) } return CLKComplicationTemplateGraphicCircularView( SightingTypeView(station: station)) case (.graphicCorner, _): guard let station = station else { return defaultTemplate(.graphicCorner) } return CLKComplicationTemplateGraphicCornerTextImage( textProvider: station.timeAndShortLocTextProvider, imageProvider: station.whaleActivityFullColorProvider) case (.graphicExtraLarge, _): guard let station = station else { return defaultTemplate(.graphicExtraLarge) } return CLKComplicationTemplateGraphicExtraLargeCircularStackText( line1TextProvider: station.timeAndLocationTextProvider, line2TextProvider: station.shortLocationTextProvider) default: return defaultTemplate(complication.family) } }
-
-
찾고 계신 콘텐츠가 있나요? 위에 주제를 입력하고 원하는 내용을 바로 검색해 보세요.