Skip to Content
Course content

157: Building a Simple Widget Extension

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

Think of a widget as a sticky note on your refrigerator. You don't walk all the way into the pantry and start rummaging through shelves (opening the full app) just to see if you're out of milk. Instead, you glance at the note on the door. The note is a snapshot of information that's updated periodically, but it's not a live, interactive conversation with the kitchen.

In Swift, building a Widget Extension follows this exact logic. We aren't building a full app; we're building a "glanceable" experience. To do this, we need three things: the Entry (the content written on the note), the TimelineProvider (the person who decides when to rewrite the note), and the View (how the note actually looks).

Defining the Snapshot Data

Before we can draw anything, we need to define what data our widget actually cares about. For this lesson, let's build a "Water Intake Tracker." We don't need the whole user profile; we just need the current count of glasses and the time that count was recorded.

struct WaterEntry: TimelineEntry {
    let date: Date
    let glassesDrunk: Int
}

The TimelineEntry protocol requires a date. This is crucial because the system uses this date to know exactly when this specific "version" of the widget should be displayed on the home screen.

The Brains: The TimelineProvider

This is where most developers get tripped up. A widget doesn't "run" in the background like a standard app. Instead, it tells iOS: "Here is a schedule of what I want to show for the next few hours." The TimelineProvider handles this scheduling.

I've found that the easiest way to wrap your head around this is to think of it as a movie reel. You're providing a series of frames (entries) and telling the system when to switch to the next frame.

struct WaterProvider: TimelineProvider {
    // A preview version for the widget gallery
    func placeholder(in context: Context) -> WaterEntry {
        WaterEntry(date: Date(), glassesDrunk: 0)
    }

    // A quick snapshot for the system to show during transitions
    func getSnapshot(in context: Context, completion: @escaping (WaterEntry) -> Void) {
        completion(WaterEntry(date: Date(), glassesDrunk: 5))
    }

    // The actual schedule
    func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) {
        let currentDate = Date()
        // In a real app, you'd fetch this from UserDefaults or a database
        let entry = WaterEntry(date: currentDate, glassesDrunk: 8)

        // We tell the system to refresh the widget again in 1 hour
        let nextUpdate = Calendar.current.date(byAdding: .hour, value: 1, to: currentDate)!
        let timeline = Timeline(entries: [entry], policy: .after(nextUpdate))
        
        completion(timeline)
    }
}

Designing the Glance

Because widgets are essentially SwiftUI views, the design part is the most intuitive. However, keep it simple. You have very little screen real estate, and the system will kill your process if you try to do something too computationally expensive during the render.

struct WaterWidgetView: View {
    var entry: WaterProvider.Entry

    var body: some View {
        VStack {
            Text("Hydration")
                .font(.caption)
                .foregroundColor(.secondary)
            Text("\(entry.glassesDrunk)")
                .font(.system(size: 40, weight: .bold))
            Text("glasses")
                .font(.caption)
        }
        .containerBackground(.blue.gradient, for: .widget)
    }
}

Wiring the Extension Together

Finally, we need to tie the provider and the view together using a Widget configuration. This is the entry point that tells iOS, "This is a widget, here is the logic that powers it, and here is the view that displays it."

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

    var body: some WidgetConfiguration {
        StaticConfiguration(kind: kind, provider: WaterProvider()) { entry in
            WaterWidgetView(entry: entry)
        }
        .configurationDisplayName("Water Tracker")
        .description("Keep track of your daily water intake.")
        .supportedFamilies([.systemSmall])
    }
}

One quick tip: I highly recommend starting with StaticConfiguration. Unless you need the user to be able to customize the widget (like picking a specific city for a weather widget), StaticConfiguration is much faster to implement and less prone to bugs.




📋 Practical Task

Build a Daily Focus Goal Widget

Your task is to create a new Widget Extension for a "Focus" app. Instead of tracking water, this widget should display a single "Focus Goal" for the day (e.g., "Finish Swift Course" or "Clean the Garage").

Requirements:

  • Create a FocusEntry that includes the goal text (a String) and the date.
  • Implement a FocusProvider that returns a hardcoded goal in the getTimeline method and schedules the next update for 24 hours later.
  • Design a FocusWidgetView that uses a ZStack or VStack to display the goal text prominently with a background color of your choice.
  • Configure the widget using StaticConfiguration and ensure it only supports the .systemSmall family.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.