Chapters

Hide chapters

watchOS With SwiftUI by Tutorials

First Edition · watchOS 8 · Swift 5.5 · Xcode 13.1

Section I: watchOS With SwiftUI

Section 1: 16 chapters
Show chapters Hide chapters

9. Complications
Written by Scott Grosch

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

Exploring the sample

Please build and run the TideWatch app from this chapter’s starter materials. After a moment, you’ll see the current tide conditions at the Point Reyes tide station in California.

Tap the station name to pick a new location:

Even though the app is amazingly useful, as designed, your customers have to open the app to find out what the current water level is. Wouldn’t it be great if they could see the information right on their watch face?

Complication data source

When you create a watchOS project, Xcode will generate ComplicationController.swift. For the sample project, I’ve moved that file into the Complications folder. Also, I removed everything except the one method required by CLKComplicationDataSource. Most of the boilerplate code is unnecessary.

The current timeline entry

When watchOS wants to update the data displayed for your complication, it calls currentTimelineEntry(for:). You’re expected to return either the data to display right now or nil if you can’t provide any data.

// 1
guard complication.family == .circularSmall else {
  return nil
}

// 2
let template = CLKComplicationTemplateGraphicCircularStackText(
  line1TextProvider: .init(format: "Surf's"),
  line2TextProvider: .init(format: "Up!")
)

// 3
return .init(date: Date(), complicationTemplate: template)

func complicationDescriptors() async -> [CLKComplicationDescriptor] {
  return [
    // 1
    .init(
      // 2
      identifier: "com.raywenderlich.TideWatch",
      // 3
      displayName: "Tide Conditions",
      // 4
      supportedFamilies: [.graphicCircular]
    )
  ]
}

Sample data

The current timeline entry is neither displayed in this list nor the Watch app on your iPhone. When asking for the current timeline data, your app may have to perform an expensive operation or run something asynchronously.

func localizableSampleTemplate(
  for complication: CLKComplication
) async -> CLKComplicationTemplate? {
  // 1
  guard
    complication.family == .graphicCircular,
    let image = UIImage(named: "tide_rising")
  else {
    return nil
  }

  // 2
  let tide = Tide(entity: Tide.entity(), insertInto: nil)
  tide.date = Date()
  tide.height = 24
  tide.type = .high

  // 3
  return CLKComplicationTemplateGraphicCircularStackImage(
    line1ImageProvider: .init(fullColorImage: image),
    line2TextProvider: .init(format: tide.heightString())
  )
}

Updating the complication’s data

When people first learn about complications, the missing “Ah-ha!” moment is that the Apple Watch will only attempt to update the complication on the watch face when you specify that new data is available. Imagine the battery drain if watchOS had to query your complication every second to see if a new data point was available?

Telling watchOS there’s new data

Open CoOpsApi.swift, and you’ll see getLowWaterHeights(for:), the method the app calls when it needs to download new tide data. Using the Combine framework allows for a very clean data download pipeline.

import ClockKit
DispatchQueue.main.async {
  let server = CLKComplicationServer.sharedInstance()
  server.activeComplications?.forEach {
    server.reloadTimeline(for: $0)
  }
}

Providing data to the complication

Switch back to Complications/ComplicationController.swift and replace the body of currentTimelineEntry(for:) with:

// 1
guard
  complication.family == .graphicCircular,
  let tide = Tide.getCurrent()
else {
  return nil
}

// 2
let template = CLKComplicationTemplateGraphicCircularStackImage(
  line1ImageProvider: .init(fullColorImage: tide.image()),
  line2TextProvider: .init(format: tide.heightString())
)

// 3
return .init(date: tide.date, complicationTemplate: template)

Supporting multiple families

While you now have a fully functional app with complication support, it’s pretty limited. For your customers to use your complication, they must use one of the watch faces that supports .graphicCircular. Whenever you’re designing complications for the Apple Watch, you should strive to support every type of family you can.

Factory Method design pattern

There’s a common design pattern, called Factory Method, which you can implement to great effect. Create a new file in Complications called ComplicationTemplateFactory.swift. 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.

Current timeline entry

Start by adding the following code to your new file:

import ClockKit

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

Samples

Samples, however, can use a default implementation. Add the following line to your protocol:

func templateForSample() -> CLKComplicationTemplate
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)
  }
}

Tide height text

Text providers support both a short and long version of the text. Right now, your code simply shows the height, but you can do better than that.

func textProvider(for waterLevel: Tide, unitStyle: Formatter.UnitStyle) -> CLKSimpleTextProvider
// 1
func textProvider(
  for waterLevel: Tide,
  unitStyle: Formatter.UnitStyle = .short
) -> CLKSimpleTextProvider {
  // 2
  let shortText = waterLevel.heightString(unitStyle: unitStyle)

  // 3
  let longText = "\(waterLevel.type.rawValue.capitalized), \(shortText)"

  // 4
  return .init(text: longText, shortText: shortText)
}

Tide image

Images are as easy to support as text. Add two more protocol methods:

func fullColorImageProvider(for waterLevel: Tide) -> CLKFullColorImageProvider
func plainImageProvider(for waterLevel: Tide) -> CLKImageProvider
func fullColorImageProvider(for waterLevel: Tide) -> CLKFullColorImageProvider {
  .init(fullColorImage: waterLevel.image())
}

func plainImageProvider(for waterLevel: Tide) -> CLKImageProvider {
  .init(onePieceImage: waterLevel.image())
}

Templates by family

Please create a Templates folder group inside Complications. Inside Templates, you’ll create a file per family that you support. Start by creating GraphicCircular.swift and filling it with:

import ClockKit

struct GraphicCircular: ComplicationTemplateFactory {
  func template(for waterLevel: Tide) -> CLKComplicationTemplate {
    return CLKComplicationTemplateGraphicCircularStackImage(
      line1ImageProvider: fullColorImageProvider(for: waterLevel),
      line2TextProvider: textProvider(for: waterLevel)
    )
  }
}
import ClockKit

// 1
enum ComplicationTemplates {
  // 2
  static func generate(
    for complication: CLKComplication
  ) -> ComplicationTemplateFactory? {
    // 3
    switch complication.family {
    case .graphicCircular: return GraphicCircular()

    // 4
    default:
      return nil
    }
  }
}

Updating the complication controller

Now that you’ve implemented the factory pattern, it’s time to put it to use. Edit ComplicationController.swift again to take advantage of your hard work.

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)
return ComplicationTemplates.generate(for: complication)?.templateForSample()

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!

supportedFamilies: [.graphicCircular, .graphicBezel]
case .graphicBezel: return GraphicBezel()
import ClockKit

struct GraphicBezel: ComplicationTemplateFactory {
  func template(for waterLevel: Tide) -> CLKComplicationTemplate {
    let circularTemplate = CLKComplicationTemplateGraphicCircularImage(
      imageProvider: fullColorImageProvider(for: waterLevel)
    )

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

Freshness

Great work! You implemented your first complication. Have you noticed the issue with the data? Your complication is only going to be correct if your customer’s run the app hourly.

Key points

  • The complication controller’s methods are all asynchronous.
  • Using a factory pattern makes adding newly supported complication families incredibly simple.
  • Support as many complication families as possible to provide the best user experience. Even sim

Where to go from here?

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 chapter.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now