watchOS: Complications

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

Part 1: Introduction to Complications

05. Create Templates for 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: 04. Support Multiple Families Next episode: 06. Update with Background Tasks

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: 05. Create Templates for Multiple Families

The sample project in final shows implementations of almost all the supported complication types. You’ll learn about the SwiftUI-specific complications in a later episode.

Apple’s Human Interface Guidelines for watchOS contains 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: 05. Create Templates for Multiple Families

To put our protocol to work, we’re going to create one file per family that we’ll support. So add a Templates folder group, inside Complications, to hold all of those files.

Start by creating GraphicCircular.swift

And then add a struct with the same name, using our template factory protocol.

import ClockKit

struct GraphicCircular: ComplicationTemplateFactory {

}

We just implemented a ton of functionality for that protocol, so, we’ve got quite a lot going on out of the box.

The only piece you still need to handle is the creation of the actual template with the template(for:) method. And from there, use the full color image provider and text provider that the protocol already has ready for you to fill out a template

func template(for waterLevel: Tide) -> CLKComplicationTemplate {
    CLKComplicationTemplateGraphicCircularStackImage(
      line1ImageProvider: fullColorImageProvider(for: waterLevel),
      line2TextProvider: textProvider(for: waterLevel)
    )
}

We’ve only got this one family at the moment, but when there are more we will need a way to determine which template family struct to use.

So! Create a new Swift file in Complications, and call it ComplicationTemplates.swift

import ClockKit at the top, of course. And then we’ll set up a caseless enumeration.

import ClockKit

enum ComplicationTemplates {

}

When your implementation is only going to contain static methods, you can use an enum like this to prevent accidental instantiation.

Now, the method we need will generate a template factory for a given complication

  static func generate(
    for complication: CLKComplication
  ) -> ComplicationTemplateFactory? {  }

We can switch on the family, and return the appropriate struct which implements ComplicationTemplateFactory.

  static func generate(... {
    switch complication.family {
    case .graphicCircular: return GraphicCircular()
    }
  }

We’ve only got one of those at the moment. So, if the given complication family isn’t supported, we can use a default case to return nil.

default: return nil

Updating the complication controller

Now that you’ve implemented the factory pattern, head back to ComplicationController.swift again to take advantage of your hard work.

First, update the body of currentTimelineEntry(for:), and create the factory for the given complication at the top of the guard statement.

guard
  // 1
  let factory = ComplicationTemplates.generate(for: complication),
  // 2
  let tide = Tide.getCurrent()
else {
  return nil
}

// 3
let template = factory.template(for: tide)
return .init(date: tide.date, complicationTemplate: template)

By calling the factory generation method, you determine whether the provided complication is supported. No more looking at family types in the complication controller!

And, if there’s not a current data point to display, then there’s still nothing to do.

Once you have the factory and a current tide instance, you can combine them to create the right template, and return it for the appropriate date.

guard
...

let template = factory.template(for: tide)
return .init(date: tide.date, complicationTemplate: template)

The localizableSampleTemplate(for:) method can be incredibly compact now.

Just one line! You don’t even need the return keyword.

ComplicationTemplates.generate(for: complication)?.templateForSample()

Because you had generate(for:) return nil when a family isn’t supported, you can use a nill chain operation. If the family isn’t supported, template will set to nil. If it is, then you’ll assign the actual template sample.

But…why?

If it’s not clear why you added the extra level of indirection, imagine your manager tells you that now you must support the .graphicBezel complication family.

How much effort will that take?

Not much!

There are only three steps required.

First, add .graphicBezel to supportedFamilies in complicationDescriptors() of ComplicationController.swift:

supportedFamilies: [.graphicCircular, .graphicBezel]

Next, add a new entry to switch in ComplicationTemplates.swift:

case .graphicBezel: return GraphicBezel()

Ignore the compiler error telling you that GraphicBezel() doesn’t exist.

Finally, create GraphicBezel.swift in the Templates folder Add a new struct for GraphicBezel And implement that single method.

import ClockKit

struct GraphicBezel: ComplicationTemplateFactory {
  func template(for waterLevel: Tide) -> CLKComplicationTemplate {
    
  }
}

When generating a complication, the CLKComplicationTemplate subclass you wish to use will drive how template(for:) is implemented.

For example, the template for Graphic Bezel Circular Text is asking us for a Graphic Circular Image template as well as a CLKTextProvider.

    return CLKComplicationTemplateGraphicBezelCircularText(
      circularTemplate: ~,
      textProvider: textProvider(for: waterLevel, unitStyle: .long)
    )

While you’ve already coded the method to generate the text provider, you still need a circular image.

So, let’s see what that involves.

    🟩let circularTemplate = CLKComplicationTemplateGraphicCircularImage(
      imageProvider: ~
    )

    return CLKComplicationTemplateGraphicBezelCircularText(...)

It turns out CLKComplicationTemplateGraphicCircularImage just requires a CLKFullColorImageProvider, and we’ve already set up a method for that!

    let circularTemplate = CLKComplicationTemplateGraphicCircularImage(
      imageProvider: 🟩fullColorImageProvider(for: waterLevel)
    )

    return CLKComplicationTemplateGraphicBezelCircularText(
      circularTemplate: 🟩circularTemplate,
      textProvider: textProvider(for: waterLevel, unitStyle: .long)
    )

At this point, you can see how simple it becomes to add new complication families to your app. Beyond that, maintenance is contained in a single file, named after the complication.

If you decide to switch the .graphicCircular family from a Graphic Circular Stack Image to a Graphic Circular Image, the update is quick and simple. You know that GraphicCircular.swift is the only file you’ll need to edit.

Alright you’ve done some great work creating your first complication. But, have you noticed the issue with the data?

Your complication is only going to be right if your customer runs the app hourly.

Coming up next, you’ll learn how to use the future data you’ve downloaded, as well as keep the data up-to-date even if the user doesn’t run the app.