Home iOS & Swift Books watchOS With SwiftUI by Tutorials

12
SwiftUI Complications Written by Scott Grosch

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

You can unlock the rest of this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

For most complications, you’ll use the templates you learned about in Chapter 8: “Complications Introductory”. Sometimes, however, you’ll want greater control over the design. watchOS lets you design some complications with SwiftUI.

Graphs are, of course, a great use case for SwiftUI-based complications. That’s no fun, though, so you’re going to show the next calendar entry for today.

Showing an appointment

Open CalendarComplication.xcodeproj from this chapter’s starter materials. EventStore.swift reads the local calendar via EventKit, as well as EventView.swift, which is the view you’ll modify.

Build and run the project on a physical device. Your watch will ask you to grant calendar permissions, which of course, you must agree to for the project to work.

Note: You must use a physical device because the Simulator doesn’t include a calendar. Also, EventKit will only display calendar items from your local calendar, not calendars in the cloud.

Event display

Edit EventView.swift, the view you’ll see in the complication. I already performed the pieces related to EventKit to save you some time. Your goal is to create a display similar to the one Apple provides for their Calendar app.

HStack {
  // 1
  RoundedRectangle(cornerRadius: 3)
    // 2
    .frame(width: 5)
    // 3
    .foregroundColor(Color(event.calendar.cgColor))
}
// 1
private let formatter: DateIntervalFormatter = {
  let formatter = DateIntervalFormatter()
  // 2
  formatter.dateStyle = .none
  formatter.timeStyle = .short
  return formatter
// 3
}()
// 1
VStack(alignment: .leading) {
  // 2
  Text(formatter.string(from: event.startDate, to: event.endDate))
    .font(.subheadline)
  // 3
  Text(event.title)
    .font(.headline)
  // 4
  if let location = event.location {
    Text(location)
      .font(.subheadline)
  }
}

Event refactoring

Right now, the code has multiple issues. Not only are you unable to preview the complication, but also everything is tightly tied to EventKit. What happens when you work with CalDAV or any of the other calendaring platforms?

Creating an Event type

Create a new file named Event.swift and paste in:

import SwiftUI
import EventKit

// 1
struct Event {
  let color: Color
  let startDate: Date
  let endDate: Date
  let title: String
  let location: String?

  // 2
  init(ekEvent: EKEvent) {
    color = Color(ekEvent.calendar.cgColor)
    startDate = ekEvent.startDate
    endDate = ekEvent.endDate
    title = ekEvent.title
    location = ekEvent.location
  }

  // 3
  init(
    color: Color,
    startDate: Date,
    endDate: Date,
    title: String,
    location: String?
  ) {
    self.color = color
    self.startDate = startDate
    self.endDate = endDate
    self.title = title
    self.location = location
  }
}

Refactoring the view

Sometimes you find that you’ve named a view poorly and need to fix it. In this case, EventView should really be EventComplicationView because you need an EventView for previewing the event.

let event: Event
static var event = Event(
  color: .blue,
  startDate: .now,
  endDate: .now.addingTimeInterval(3600),
  title: "Gnomes rule!",
  location: "Everywhere"
)
import ClockKit
Group {
  EventView(event: event)

  CLKComplicationTemplateGraphicRectangularFullView(
    EventView(event: event)
  )
    .previewContext()
}

let event: Event?
} else if let event = eventStore.nextEvent {
} else if let event = event {
  EventView(event: event)
EventComplicationView(event: nil)
EventComplicationView()
EventComplicationView(event: nil)

The event complication

It’s finally time to use your SwiftUI view in a complication.

Event to timeline entry

All the complication methods need to be able to create a CLKComplicationTimelineEntry from an EKEvent. So, add the following method to ComplicationController.swift:

private func timelineEntry(for ekEvent: EKEvent?) -> CLKComplicationTimelineEntry {
  // 1
  let event: Event?
  if let ekEvent = ekEvent {
    event = Event(ekEvent: ekEvent)
  } else {
    event = nil
  }

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

  // 3
  return .init(
    date: event?.startDate ?? .now,
    complicationTemplate: template
  )
}
import EventKit

Localizable sample

It’s important to have a sample complication for users to see when they’re choosing complications. Provide one with the following delegate method, still in ComplicationController.swift:

func localizableSampleTemplate(
  for complication: CLKComplication
) async -> CLKComplicationTemplate? {
  // 1
  let start = Calendar.current.date(
    bySettingHour: 10, minute: 0, second: 0, of: .now
  )!

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

  // 3
  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. Replace the body of currentTimelineEntry(for:) with:

return timelineEntry(for: EventStore.shared.nextEvent)

Future appointments

If your work calendar is anything like mine, you have way more than one event every day. You’ll want to provide future events like you did in previous chapters.

func timelineEntries(
  for complication: CLKComplication,
  after date: Date,
  limit: Int
) async -> [CLKComplicationTimelineEntry]? {
  // 1
  guard let events = EventStore.shared.eventsForToday() else {
    return [timelineEntry(for: nil)]
  }

  let wanted = events
    // 2
    .filter {
      date.compare($0.startDate) == .orderedAscending
    }
    // 3
    .prefix(limit)
    // 4
    .map { timelineEntry(for: $0) }

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

Tinting

In Chapter 11: “Tinted Complications”, you learned about tinting. SwiftUI views let you specify the foreground via the complicationForeground modifier.

.complicationForeground()
Text(event.title)
  .font(.headline)
.previewContext(faceColor: .green)

@Environment(\.complicationRenderingMode) var renderingMode

Key points

  • ClockKit provides multiple graphic complication types that use a SwiftUI View.
  • SwiftUI views easily support tinting via the complicationForeground() modifier.
  • For complete tinting control, use the ComplicationRenderingMode environment property.

Where to go from here?

For more information, check out these resources:

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.

© 2022 Razeware LLC

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

Unlock Now

To highlight or take notes, you’ll need to own this book in a subscription or purchased by itself.