Notes: 04. Support Multiple Families
Apple’s Human Interface Guidelines for watchOS contain a wealth of useful material related to complications. For example, you’ll find image size and composition guidance, descriptions of each family type and example images of how the complication family appears on the watch face.
If you’d like to dive deeper into Design Patterns, like the Factory Method design pattern that you implemented in this chapter, please check out our book, Design Patterns by Tutorials.
We’ve got a functional app with complication support, but it’s pretty limited.
For customers to use your complication, they have to use one of the watch faces that supports .graphicCircular.
So, how can we add support for more families?
We specified a single family to the supportedFamilies parameter in the complicationDescriptors() method.
It’s just a few keystrokes for you to add the rest of the types, or even simply specify CLKComplicationFamily.allCases, but you’d still have to handle each distinct template type.
Most of the resources you’ll see for complications will tell you to create a switch statement, against the family, in each method to determine what actions to take.
While you could do that, I’ll show you an alternative that you might find easier to maintain.
Factory Method design pattern
Consider the code you’ve written so far, and you can likely see some common patterns that you’ll need to replicate across each family.
Just to support a single family, you needed to:
- Generate the current timeline entry.
- Generate the sample template.
- Generate the text to display the tide height.
- Generate the image to represent the tide type.
There’s a common design pattern, called Factory Method, which we’ll use to wrap all of those repeated steps together into a protocol.
Create a new file in Complications called ComplicationTemplateFactory.swift.
Current timeline entry
We’ll be generating timeline entries here, so, in this new file, import ClockKit at the top.
import ClockKit
And create a ComplicationTemplateFactory protocol.
protocol ComplicationTemplateFactory {
}
OK! Regardless of type, any family you support needs a way to convert from your Tide data model to a CLKComplicationTemplate.
func template(for waterLevel: Tide) -> CLKComplicationTemplate
But, the code for each type of family will be distinct, so you can’t provide a default implementation.
Samples
But what about samples?
func templateForSample() -> CLKComplicationTemplate
Consider what localizableSampleTemplate(for:) currently does over in ComplicationController.
It generates a fake Tide entry, creates the template for the correct family and then returns that template.
You’ve just specified that any class, which conforms to ComplicationTemplateFactory, knows how to generate a template based on real data. Why not pass that method some fake data instead?
Protocols let you add default method implementations, but only in an extension.
So, open up an extension on ComplicationTemplateFactory, and start that method.
Create a some fake tide data, again. And return the template using the first method we defined!
extension ComplicationTemplateFactory {
func templateForSample() -> CLKComplicationTemplate {
let tide = Tide(entity: Tide.entity(), insertInto: nil)
tide.date = Date()
tide.height = 24
tide.type = .falling
return template(for: tide)
}
}
By add a default implementation for templateForSample(), you’ve ensured that every single complication family you support will already know how to generate a localizable sample.
Tide height text
Right now, your code simply shows the height, but you can do better than that.
Text providers for complications support both a short and long version of the text, so, let’s add a method to help us handle that.
func textProvider(for waterLevel: Tide, unitStyle: Formatter.UnitStyle) -> CLKSimpleTextProvider
Then add a default implementation in the extension.
It will generate the appropriate verbiage based on a provided Tide. Depending on the type of family, you might want to display the height in different lengths, but let’s default to .short.
func textProvider(
for waterLevel: Tide,
unitStyle: Formatter.UnitStyle = .short
) -> CLKSimpleTextProvider {
}
The short text will simply be the tide’s height.
let shortText = waterLevel.heightString(unitStyle: unitStyle)
For the long text, let’s use the tide’s type, followed by its height.
let longText = "\(waterLevel.type.rawValue.capitalized), \(shortText)"
And finally, pass both types of text to CLKSimpleTextProvider
return .init(text: longText, shortText: shortText)
When you use the two-parameter version of CLKSimpleTextProvider, the Apple Watch will choose which text to display based on the configuration of the family.
If the longer text fits, great! That’s what will show up. If the complication in use is too narrow, then the shorter version will show.
Tide image
Images are as easy to support as text, but they’ll take two methods. One for full color images, and one for “plain” images. “Plain”, here, just means monochromatic.
func fullColorImageProvider(for waterLevel: Tide) -> CLKFullColorImageProvider
func plainImageProvider(for waterLevel: Tide) -> CLKImageProvider
While the sample app doesn’t differentiate between full color and plain images, the factory you’re designing here will be reusable across all your apps.
The default implementations for these are fairly straightforward.
Return a full color version of the water level from one, and a onePieceImage version from the other.
func fullColorImageProvider(for waterLevel: Tide) -> CLKFullColorImageProvider {
.init(fullColorImage: waterLevel.image())
}
func plainImageProvider(for waterLevel: Tide) -> CLKImageProvider {
.init(onePieceImage: waterLevel.image())
}
In a production app, you’d probably be using two separate image generation methods on the Tide, but our app isn’t so fancy.
That’s our protocol! In the next episode we’ll put it to work, and get you set up to create templates for as many complication families as you’d like.