watchOS: Complications

Feb 7 2023 · Swift 5.6, watchOS 8.5, Xcode 13

Part 1: Introduction to Complications

04. Support Multiple Families

Episode complete

Play next episode

Next
About this episode
Leave a rating/review
See forum comments
Cinema mode Mark complete Download course materials
Previous episode: 03. Update a Complication's Data Next episode: 05. Create Templates for Multiple Families

Get immediate access to this and 4,000+ other videos and books.

Take your career further with a Kodeco Personal Plan. With unlimited access to over 40+ books and 4,000+ professional videos in a single subscription, it's simply the best investment you can make in your development career.

Learn more Already a subscriber? Sign in.

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.

Transcript: 04. Support Multiple Families

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.