watchOS: Complications

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

Part 2: Tinted & Custom Complications

13. Display a SwiftUI View in a Complication

Episode complete

About this episode
Leave a rating/review
See forum comments
Cinema mode Mark complete Download course materials
Previous episode: 12. Refactor SwiftUI Views

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: 13. Display a SwiftUI View in a Complication

For more information on watchOS development, check out our book watchOS With SwiftUI by Tutorials

For more information about complications, check out these resources:

Transcript: 13. Display a SwiftUI View in a Complication

After all of that work, it’s time to wrap this project up and check out your SwiftUI view in a complication.

For the previews, we used the .graphicRectangular family since it works well for a calendar display. To keep things focused, we won’t implement any other complication families in this course. But you are welcome to give it a try yourself!

As you hopefully know now, the place to get started with complications is in ComplicationController.swift with a timeline entry.

Event to timeline entry

All the complication methods need to be able to create a CLKComplicationTimelineEntry from an EKEvent.

Which means we need to import EventKit at the top of the file!

import EventKit

And now we can add a method to take in EKEvents and give us back a timeline entry.

private func timelineEntry(for ekEvent: EKEvent?) -> CLKComplicationTimelineEntry {

}

First, convert the EKEvent you have to an Event, since that’s what your views expect. Remember, if you pass nil to the event parameter of the EventComplicationView initializer, it will display a message saying there are no more events.

  let event: Event?
  if let ekEvent = ekEvent {
    event = Event(ekEvent: ekEvent)
  } else {
    event = nil
  }

We’ll need to use one of the template types which expects you to give it a SwiftUI view to use as a template. For example, GraphicRectangularFullView

  let template = CLKComplicationTemplateGraphicRectangularFullView(
    EventComplicationView(event: event)
  )

To wrap this up, generate a CLKComplicationTimelineEntry based on the event’s start date and the SwiftUI template. If there isn’t an event, then use the current date.

  return .init(
    date: event?.startDate ?? .now,
    complicationTemplate: template
  )
}

Localizable sample

As we learned earlier in the course, it’s important to have a sample complication for users to see when they’re choosing complications.

So let’s provide one with the localizableSampleTemplate method

func localizableSampleTemplate(
  for complication: CLKComplication
) async -> CLKComplicationTemplate? {

}

Create a Date entry for the current day at 10:00 am. Recall that your date display doesn’t include the day, so using today is OK.

  let start = Calendar.current.date(
    bySettingHour: 10, minute: 0, second: 0, of: .now
  )!

Add an hour to the start time to indicate when the event ends.

  let end = Calendar.current.date(
    byAdding: .hour, value: 1, to: start
  )!

And finally return a Graphic Rectangular Full View template with some fake data to display in the sample template.

  return CLKComplicationTemplateGraphicRectangularFullView(
    EventView(event: .init(
      color: .blue,
      startDate: start,
      endDate: end,
      title: "Gnomes rule!",
      location: "Everywhere"
    ))
  )

The current appointment

Just like when using non-SwiftUI templates, you have to provide the current timeline entry if one exists.

So, update currentTimelineEntry(for:) to return a timeline entry from EventStore’s nextEvent

return timelineEntry(for: EventStore.shared.nextEvent)

Notice how even if there’s not an event, you don’t return nil. If you return nil, you don’t get an actual display for the complication, which isn’t what you want.

If there’s no event, you still want the “No more events” message.

Future appointments

It is likely you have more than one event on your calendar every day.

So, you’ll want to provide future events like you did earlier in this course, with the timelineEntries method.

func timelineEntries(
  for complication: CLKComplication,
  after date: Date,
  limit: Int
) async -> [CLKComplicationTimelineEntry]? {

}

If there are no more events for today, return the “No more events” placeholder event.

  guard let events = EventStore.shared.eventsForToday() else {
    return [timelineEntry(for: nil)]
  }

If there are events left, ensure that they’re after the date specified in the date method parameter.

  let wanted = events
    .filter {
      date.compare($0.startDate) == .orderedAscending
    }

Honor the limit method parameter by only creating that many timeline entries, and then convert each EKEvent to a CLKComplicationTimelineEntry.

    .prefix(limit)
    .map { timelineEntry(for: $0) }

If there were events after all of that, return them.

  return wanted.count > 0 ? wanted : [timelineEntry(for: nil)]
}

Notice how you might have zero entries after filtering. Be sure you don’t return nil. Instead, return the “No more entries” item.

Before we give this a look on the watch, let’s add some tinting support, like we did for our happy cat face.

Tinting

In EventView.swift, just after the .foregroundColor(event.color) line of RoundedRectangle, add the complication foreground modifier

.complicationForeground()

Then do the same for the event title:

Now, in the previews body, update the context with a face color:

.previewContext(faceColor: .green)

In the canvas, you’ll see that both the rectangle and the title now receive the tint color.

Ideally, specifying the foreground will be all you need to make your complication look perfect. Depending on whether the watch face is tinted, you might need to do something more drastic.

Remember that you can be intentional about how your complication looks when tinted vs. full color with the complicationRenderingMode Environment value we used with the Happy watch app.

Finally - Build and run the app.

Once the app starts on your Apple Watch, add a new watch face using the Modular Compact design, if you aren’t already using it.

This face includes the graphic rectangular complication.

Select your calendar app for the complication and then return to the home screen.

And there it is!