WWDC Quick Look 💓 By SwiftGGTeam
Build widgets for the Smart Stack on Apple Watch

Build widgets for the Smart Stack on Apple Watch

Watch original video

Highlight

watchOS 10 brings WidgetKit to the Smart Stack on Apple Watch. Developers can replace the old IntentConfiguration with AppIntentConfiguration, use relevance scores in TimelineEntry and RelevantIntentManager so the system shows widgets at the right moment, and support two watchOS-specific sizes: rectangular and inline accessory.


Core Content

The problem: watchOS apps did not surface information quickly enough

Before watchOS 10, options for real-time information on Apple Watch were limited. Complications can show data, but they are small and interaction is constrained. Users had to open apps for details—awkward in the few seconds after raising their wrist.

Widgets on iOS proved that “a glance is enough”—but watchOS had no equivalent.

Apple’s approach: Smart Stack + WidgetKit

watchOS 10 introduced the Smart Stack. Turn the Digital Crown or swipe up from the watch face and the Smart Stack appears. It arranges widgets automatically by timeline and relevance—users do not pick manually.

The key change: WidgetKit now supports watchOS. You build widgets with nearly the same APIs as on iOS, plus two watch-specific sizes—accessoryRectangular and accessoryInline.

More importantly, the system chooses which widget to show based on “relevance.” Developers supply: TimelineEntry relevance scores, future events registered with RelevantIntentManager, and preconfigured options from recommendations().

The full widget build flow

The session uses the Backyard Birds app to build a Smart Stack widget from scratch. It shows backyard bird visits and remaining water and food time.

The flow has five steps:

  1. Define ConfigurationIntent (using AppIntentConfiguration)
  2. Create TimelineEntry (with relevance property)
  3. Implement TimelineProvider (placeholder / snapshot / timeline / recommendations)
  4. Build SwiftUI views (layout per widgetFamily)
  5. Set up relevance management (RelevantIntentManager + WidgetCenter)

Detailed Content

Configuration type: from IntentConfiguration to AppIntentConfiguration

watchOS 10 widgets use the new AppIntentConfiguration instead of the old IntentConfiguration. The configuration intent follows WidgetConfigurationIntent, defining parameters in code without a separate Intent Definition File (04:15).

TimelineEntry: more than time and data

TimelineEntry is the core of the widget data model. Besides display data, it tells the system how important each entry is (04:15):

struct SimpleEntry: TimelineEntry {
    var date: Date
    var configuration: ConfigurationAppIntent
    var backyard: Backyard

    var bird: Bird? {
        return backyard.visitorEventForDate(date: date)?.bird
    }

    var waterDuration: Duration {
        return Duration.seconds(abs(self.date.distance(to: self.backyard.waterRefillDate)))
    }

    var foodDuration: Duration {
        return Duration.seconds(abs(self.date.distance(to: self.backyard.foodRefillDate)))
    }

    var relevance: TimelineEntryRelevance? {
        if let visitor = backyard.visitorEventForDate(date: date) {
            return TimelineEntryRelevance(score: 10, duration: visitor.endDate.timeIntervalSince(date))
        }
        return TimelineEntryRelevance(score: 0)
    }
}

Key points:

  • TimelineEntry must include a date property—the basis for when the system shows the entry
  • bird is a computed property that looks up whether a bird visited at the current date
  • waterDuration and foodDuration compute time until the next refill
  • relevance is critical—score 10 with duration covering the visit when a bird is present; score 0 when none

TimelineProvider: four required methods

The widget data flow is controlled by TimelineProvider, which implements four methods: placeholder, snapshot, timeline, and recommendations.

placeholder — loading state (07:50):

func placeholder(in context: Context) -> SimpleEntry {
    return SimpleEntry(date: Date(), configuration: ConfigurationAppIntent(), backyard: Backyard.anyBackyard(modelContext: modelContext))
}

Key points:

  • placeholder shows when the widget first loads and data is not ready
  • Must return synchronously—no network or heavy work
  • Uses anyBackyard for sample data

snapshot — preview (08:15):

func snapshot(for configuration: ConfigurationAppIntent, in context: Context) async -> SimpleEntry {
    if let backyard = Backyard.backyardForID(modelContext: modelContext, backyardID: configuration.backyardID) {
        if let event = backyard.visitorEvents.first {
            return SimpleEntry(date: event.startDate, configuration: configuration, backyard: backyard)
        } else {
            return SimpleEntry(date: Date(), configuration: configuration, backyard: backyard)
        }
    }

    let yard = Backyard.anyBackyard(modelContext: modelContext)
    return SimpleEntry(date: Date(), configuration: ConfigurationAppIntent(), backyard: yard)
}

Key points:

  • snapshot is for Widget Gallery preview and can return asynchronously
  • Prefers the user’s selected backyard
  • If there is a visit event, uses the first event’s start date as the entry date
  • Falls back to any backyard if none is selected

timeline — timeline generation (16:30):

func timeline(for configuration: ConfigurationAppIntent, in context: Context) async -> Timeline<SimpleEntry> {
    var entries: [SimpleEntry] = []

    if let backyard = Backyard.backyardForID(modelContext: modelContext, backyardID: configuration.backyardID) {
        for event in backyard.visitorEvents {
            let entry = SimpleEntry(date: event.startDate, configuration: configuration, backyard: backyard)
            entries.append(entry)
            let afterEntry = SimpleEntry(date: event.endDate, configuration: configuration, backyard: backyard)
            entries.append(afterEntry)
        }
    }
    return Timeline(entries: entries, policy: .atEnd)
}

Key points:

  • timeline is the core method, producing a time-ordered set of entries
  • Each visit creates two entries: one at startDate (bird present) and one at endDate (bird left)
  • Different relevance scores let the system decide whether to show the widget
  • policy: .atEnd means after the timeline ends, the system calls this method again at the last entry’s time

recommendations — suggested configurations (18:35):

func recommendations() -> [AppIntentRecommendation<ConfigurationAppIntent>] {
    var recs = [AppIntentRecommendation<ConfigurationAppIntent>]()

    for backyard in Backyard.allBackyards(modelContext: modelContext) {
        let configIntent = ConfigurationAppIntent()
        configIntent.backyardID = backyard.id.uuidString
        let gardenRecommendation = AppIntentRecommendation(intent: configIntent, description: backyard.name)
        recs.append(gardenRecommendation)
    }

    return recs
}

Key points:

  • recommendations let users pick preconfigured options when adding a widget
  • Each backyard gets one recommendation so users do not set parameters manually
  • description appears in the widget picker

SwiftUI views: layout per widgetFamily

The widget view switches layout based on the widgetFamily environment variable (10:26):

struct BackyardBirdsWidgetEntryView: View {
    @Environment(\.widgetFamily) private var family
    var entry: SimpleEntry

    var body: some View {
        switch family {
        case .accessoryRectangular:
            RectangularBackyardView(entry: entry)
        default:
            Text(entry.date, style: .time)
        }
    }
}

Key points:

  • widgetFamily is an environment value; on watchOS it can be accessoryRectangular, accessoryInline, accessoryCircular, or accessoryCorner
  • Switch to different subviews per family
  • accessoryRectangular is most common on watch—it has enough space for multiple lines

Full rectangular view implementation (11:23):

struct RectangularBackyardView: View {
    var entry: SimpleEntry

    var body: some View {
        HStack {
            if let bird = entry.bird {
                ComposedBird(bird: bird)
                    .scaledToFit()
                    .widgetAccentable()
                    .frame(width: 50, height: 50)
                VStack(alignment: .leading) {
                    Text(bird.speciesName)
                        .font(.headline)
                        .foregroundStyle(bird.colors.wing.color)
                        .widgetAccentable()
                        .minimumScaleFactor(0.75)
                    Text(entry.backyard.name)
                        .minimumScaleFactor(0.75)
                    HStack {
                        Image(systemName: "drop.fill")
                        Text(entry.waterDuration, format: remainingHoursFormatter)
                        Image(systemName: "fork.knife")
                        Text(entry.foodDuration, format: remainingHoursFormatter)
                    }
                    .imageScale(.small)
                    .minimumScaleFactor(0.75)
                    .foregroundStyle(.secondary)
                }
                .frame(maxWidth: .infinity, alignment: .leading)
            } else {
                Image(.fountainFill)
                    .foregroundStyle(entry.backyard.backgroundColor)
                    .imageScale(.large)
                    .scaledToFit()
                    .widgetAccentable()
                    .frame(width: 50, height: 50)
                VStack(alignment: .leading) {
                    Text(entry.backyard.name)
                        .font(.headline)
                        .foregroundStyle(entry.backyard.backgroundColor)
                        .widgetAccentable()
                        .minimumScaleFactor(0.75)
                    HStack {
                        Image(systemName: "drop.fill")
                        Text(entry.waterDuration, format: remainingHoursFormatter)
                        Image(systemName: "fork.knife")
                        Text(entry.foodDuration, format: remainingHoursFormatter)
                    }
                    .imageScale(.small)
                    .minimumScaleFactor(0.75)
                    Text("\(entry.backyard.historicalEvents.count) visitors")
                        .minimumScaleFactor(0.75)
                        .foregroundStyle(.secondary)
                }
                .frame(maxWidth: .infinity, alignment: .leading)
            }
        }
        .containerBackground(entry.backyard.backgroundColor.gradient, for: .widget)
    }
}

Key points:

  • Two states: bird visit shows image and species name; no bird shows fountain icon
  • .widgetAccentable() marks elements the system can tint with the theme color
  • .minimumScaleFactor(0.75) shrinks text in tight space without truncation
  • .containerBackground(for: .widget) sets the widget background; the system adapts light/dark mode
  • Water (drop.fill) and food (fork.knife) SF Symbols show remaining time

Relevance management: show widgets at the right time

The Smart Stack’s core ability is picking the most relevant widget. Developers help the system in two ways.

Step one: provide relevance in TimelineEntry (shown above). Score 10 when there is an event, 0 otherwise. The system compares scores across widgets and prioritizes higher ones.

Step two: register future events with RelevantIntentManager (20:47):

func updateBackyardRelevantIntents() async {
    let modelContext = ModelContext(DataGeneration.container)
    var relevantIntents = [RelevantIntent]()

    for backyard in Backyard.allBackyards(modelContext: modelContext) {

        let configIntent = ConfigurationAppIntent()
        configIntent.backyardID = backyard.id.uuidString
        let relevantFoodDateContext = RelevantContext.date(from: backyard.lowSuppliesDate(for: .food), to: backyard.expectedEmptyDate(for: .food))
        let relevantFoodIntent = RelevantIntent(configIntent, widgetKind: "BackyardVisitorsWidget", relevance: relevantFoodDateContext)
        relevantIntents.append(relevantFoodIntent)

        let relevantWaterDateContext = RelevantContext.date(from: backyard.lowSuppliesDate(for: .water), to: backyard.expectedEmptyDate(for: .water))
        let relevantWaterIntent = RelevantIntent(configIntent, widgetKind: "BackyardVisitorsWidget", relevance: relevantWaterDateContext)
        relevantIntents.append(relevantWaterIntent)
    }

    do {
        try await RelevantIntentManager.shared.updateRelevantIntents(relevantIntents)
    } catch { }
}

Key points:

  • RelevantIntent has three parts: configuration intent, widget kind, and relevance context
  • RelevantContext.date(from:to:) defines a time window when the widget is more relevant
  • Separate intents for food and water because users need reminders when supplies run low
  • widgetKind must match the kind string in the widget definition

Step three: reload timelines after data changes (23:00):

Task {
    await updateBackyardRelevantIntents()
    WidgetCenter.shared.reloadTimelines(ofKind: "BackyardVisitorsWidget")
}

Key points:

  • Call this when app data changes (e.g., user adds supplies)
  • Update relevant intent registration first, then refresh the timeline
  • reloadTimelines(ofKind:) triggers the timeline provider to regenerate entries

Core Takeaways

Ideas you can build right away:

  • Commute countdown widget: Show the next bus or train arrival. Store each arrival in TimelineEntry, set high relevance score near departure so the system surfaces it when you leave. Entry API: generate schedule entries in TimelineProvider.timeline(for:in:).

  • Medication reminder widget: Raise widget relevance during medication windows from the user’s schedule. Register the next 24 hours with RelevantIntentManager. When the user raises their wrist, Smart Stack shows the next dose. Entry API: RelevantIntentManager.shared.updateRelevantIntents(_:).

  • Delivery tracking widget: Use accessoryRectangular for status, ETA, and rider distance. In the last 10 minutes, TimelineEntryRelevance(score: 10) pushes the widget to the top of Smart Stack. Entry API: TimelineEntryRelevance with .atEnd refresh policy.

  • Activity progress widget: Show today’s Activity Rings completion percentage; update the timeline every 25%. Use .containerBackground(for: .widget) so the background shifts from red to green with progress. Entry API: HealthKit data source + periodic TimelineProvider refresh.

  • Meeting room status widget: Integrate calendar API to show whether a room is free and when the next meeting starts. accessoryRectangular for title and time; accessoryCircular for a countdown ring. Entry API: WidgetCenter.shared.reloadTimelines(ofKind:) when calendar events change.


  • Bring widgets to new places — the widget ecosystem is expanding: discover how you can use the latest widgetkit apis to make your widget look great everywhere. we’ll show you how to identify your widget’s background, adjust layout dynamically, and prepare colors for vibrant rendering so that your widget can sit seamlessly in any environment.
  • Bring widgets to life — learn how to make animated and interactive widgets for your apps and games. we’ll show you how to tweak animations for entry transitions and add interactivity using swiftui button and toggle so that you can create powerful moments right from the home screen and lock screen.
  • Update your app for watchOS 10 — join us as we update an apple watch app to take advantage of the latest features in watchos 10. in this code-along, we’ll show you how to use the latest swiftui apis to maximize glanceability and reorient app navigation around the digital crown.
  • Explore SwiftUI animation — explore swiftui’s powerful animation capabilities and find out how these features work together to produce impressive visual effects. learn how swiftui refreshes the rendering of a view, determines what to animate, interpolates values over time, and propagates context for the current transaction.

Comments

GitHub Issues · utterances