Skip to Content
Course content

159: Building a Watch Complication with WidgetKit

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Think of a Watch complication not as a live app, but as a series of scheduled postcards. If you wanted to tell a friend how your day was going, you wouldn't stand next to their mailbox and whisper updates every few seconds—that's exhausting and wasteful. Instead, you'd write five postcards in advance: "9 AM: Just had coffee," "12 PM: Lunch was great," "3 PM: Almost done with work," and so on. You give the stack to the mailman and tell him exactly when to deliver each one. The friend just glances at the mailbox and sees the current postcard.

In WidgetKit, your complication works exactly like those postcards. You don't "run" code in real-time on the watch face; instead, you provide a Timeline of entries. The system holds onto these and swaps them out at the exact moment you specified. This is the only way Apple allows complications to work because if every app had a live process running on the watch face, the battery would be dead by noon.

Scheduling the Delivery with TimelineProvider

To make this work, we need a TimelineProvider. This is our "mailman." Its job is to look at the current state of the app and decide what the next few hours of "postcards" should look like. Let's imagine we're building a Water Intake Tracker. We want the complication to show how close the user is to their daily goal.

struct WaterEntry: TimelineEntry {
    let date: Date
    let ouncesDrunk: Int
    let goal: Int
}

struct WaterProvider: TimelineProvider {
    func placeholder(in context: Context) -> WaterEntry {
        WaterEntry(date: Date(), ouncesDrunk: 32, goal: 64)
    }

    func getSnapshot(in context: Context, completion: @escaping (WaterEntry) -> Void) {
        completion(WaterEntry(date: Date(), ouncesDrunk: 32, goal: 64))
    }

    func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) {
        // I'm creating a timeline that refreshes every hour for the next 5 hours
        var entries: [WaterEntry] = []
        let currentDate = Date()
        
        for hourOffset in 0..<5 {
            let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
            // In a real app, you'd fetch actual data here
            entries.append(WaterEntry(date: entryDate, ouncesDrunk: 32 + (hourOffset * 8), goal: 64))
        }

        let timeline = Timeline(entries: entries, policy: .atEnd)
        completion(timeline)
    }
}

Notice the policy: .atEnd. I'm telling watchOS, "Keep showing these postcards until the last one is delivered, then come back to me for a new stack."

Designing the Glance

Now we need the actual postcard design. Because complications come in so many shapes—circles, rectangles, corners—you have to be flexible. You aren't building a full screen; you're building a tiny fragment of a screen. I usually recommend keeping the UI dead simple: an icon and a percentage or a number.

struct WaterWidgetView : View {
    var entry: WaterProvider.Entry

    var body: some View {
        VStack {
            Text("💧")
            Text("\(entry.ouncesDrunk)/\(entry.goal)")
                .font(.system(size: 10, weight: .bold))
        }
    }
}

If you try to put too much text in here, watchOS will simply truncate it or, in some cases, refuse to render it. I've spent way too many hours fighting with a font size that was just 2 points too large for a Circular complication. Keep it lean.

Defining the Widget Configuration

Finally, we tie the provider and the view together using a Widget configuration. This is where you tell the system that this specific piece of code is intended to be a complication. By using WidgetFamily, the system knows how to scale your view to fit the specific watch face the user has chosen.

@main
struct WaterWidget: Widget {
    let kind: String = "WaterIntakeWidget"

    var body: some WidgetConfiguration {
        StaticConfiguration(kind: kind, provider: WaterProvider()) { entry in
            WaterWidgetView(entry: entry)
        }
        .supportedFamilies([.accessoryCircular, .accessoryRectangular, .accessoryCorner])
    }
}

One thing to keep in mind: since the view is rendered by the system, not your app, you can't use interactive elements like buttons or text fields inside the complication. The only "interaction" is a tap, which simply launches your main app.




📋 Practical Task

Exercise: The Pomodoro Focus Glance

Build a Watch complication for a Pomodoro timer. Your goal is to create a timeline that alternates between a "Focus" state and a "Break" state.

  • Create a PomodoroEntry that contains a Boolean called isFocusSession and a Date.
  • Implement a TimelineProvider that generates a sequence of entries: 25 minutes of "Focus" followed by 5 minutes of "Break," repeating for at least 3 cycles.
  • Design a PomodoroWidgetView that displays a red circle icon 🔴 when isFocusSession is true, and a green circle icon 🟢 when it is false.
  • Ensure the widget supports the .accessoryCircular family.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.