A widget reads a constrained snapshot of app state and updates through its timeline and supported interactions. It is not the full app view running indefinitely in the background.

Shared snapshot → Timeline entry → Widget view → Intent-triggered change
  1. 1Shared snapshot
  2. 2Timeline entry
  3. 3Widget view
  4. 4Intent-triggered change

Work through the example

Separate storage shared with the app from preview fixtures. Validate size families and empty data before adding styling.

Start with a disposable branch and synthetic data. Write the expected outcome before changing the implementation, then keep the first failing result. This prevents a later repair from quietly redefining the task. The procedure below is grounded in the repository reference; its examples must still be checked against your project and installed toolchain.

Implementation reference

The following focused section is adapted from the maintained project guide. It preserves the source’s examples and limitations.

TimelineProvider (Placeholder, Snapshot, Timeline)

struct SimpleEntry: TimelineEntry {
    let date: Date
    let title: String
    let value: Int
    let icon: String
}

struct SimpleProvider: TimelineProvider {
    // Shown while widget is loading. Must return synchronously.
    func placeholder(in context: Context) -> SimpleEntry {
        SimpleEntry(date: .now, title: "Loading...", value: 0, icon: "star")
    }

    // Shown in the widget gallery and transient situations.
    func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> Void) {
        if context.isPreview {
            // Return sample data for the gallery preview
            completion(SimpleEntry(date: .now, title: "Steps Today", value: 8432, icon: "figure.walk"))
        } else {
            // Fetch real data for transient display
            let entry = SimpleEntry(date: .now, title: "Steps Today", value: fetchStepCount(), icon: "figure.walk")
            completion(entry)
        }
    }

    // Provides the timeline of entries that drive the widget's display.
    func getTimeline(in context: Context, completion: @escaping (Timeline<SimpleEntry>) -> Void) {
        var entries: [SimpleEntry] = []
        let currentDate = Date()

        // Create entries for the next 5 hours
        for hourOffset in 0..<5 {
            let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
            let entry = SimpleEntry(
                date: entryDate,
                title: "Steps Today",
                value: fetchStepCount() + (hourOffset * 500),
                icon: "figure.walk"
            )
            entries.append(entry)
        }

        // Timeline reload policies:
        // .atEnd     - reload after the last entry's date passes
        // .after(d)  - reload after a specific date
        // .never     - only reload when the app explicitly requests it
        let timeline = Timeline(entries: entries, policy: .atEnd)
        completion(timeline)
    }

    private func fetchStepCount() -> Int { return 8432 }
}

// Async provider using AppIntentTimelineProvider (cleaner async/await API)
struct ConfigurableProvider: AppIntentTimelineProvider {
    typealias Entry = SimpleEntry
    typealias Intent = SelectCategoryIntent

    func placeholder(in context: Context) -> SimpleEntry {
        SimpleEntry(date: .now, title: "Loading...", value: 0, icon: "star")
    }

    func snapshot(for configuration: SelectCategoryIntent, in context: Context) async -> SimpleEntry {
        SimpleEntry(date: .now, title: configuration.category?.name ?? "All", value: 42, icon: "star")
    }

    func timeline(for configuration: SelectCategoryIntent, in context: Context) async -> Timeline<SimpleEntry> {
        let entries = [
            SimpleEntry(
                date: .now,
                title: configuration.category?.name ?? "All",
                value: 42,
                icon: "star"
            )
        ]
        return Timeline(entries: entries, policy: .after(.now.addingTimeInterval(3600)))
    }
}

Acceptance and failure review

Checkpoint What to inspect If it does not match
Shared snapshot Confirm the input and environment Preserve the failure and return to this step
Timeline entry Inspect the intermediate artifact Preserve the failure and return to this step
Widget view Run the focused check Preserve the failure and return to this step
Intent-triggered change Record the observed result Preserve the failure and return to this step

Ask the agent to explain the smallest change that resolves the observed mismatch. Keep unrelated refactors out of the repair. A change that makes a warning disappear is not enough if the behavior or ownership contract has changed. Re-run the same acceptance check so the before and after results are comparable.

Evidence and limits

The installed Xcode 26.6 cannot verify iOS 27-only widget styling; that lab is blocked.

This is an educational guide. Its presence in the series does not certify a completed client-specific lab. The series evidence record separates executed checks from exercises and blocked environments.

Inspect the source used in this lesson.

What to do next

Next: Designing Siri actions with App Intents and testable domain operations