How to Build a Weather Widget in iOS with WidgetKit

Build a weather widget for iOS using WidgetKit with a TimelineProvider and SwiftUI views. WidgetKit is an iOS 14 framework for creating Home Screen widgets. It uses a timeline-based update model where a TimelineProvider supplies TimelineEntry objects at scheduled dates. SwiftUI renders each entry into a widget view. The widget runs as an app extension in a separate process from the container app.

What Is WidgetKit and How Do iOS Widgets Work?

WidgetKit is Apple's framework for displaying app content on the Home Screen. Apple introduced it in iOS 14, replacing the legacy Today View widget system. Widgets display glanceable information that updates at scheduled intervals.

A widget is an app extension bundled inside a container app. It runs in a separate process. Communication between the widget and its container app uses shared containers like UserDefaults with App Groups. Widgets cannot make arbitrary network calls on demand. They rely on timeline entries prepared by the TimelineProvider.

What Are the Three Core Components of a WidgetKit Widget?

Every WidgetKit widget has three components. The Configuration defines the widget's name, description, and SwiftUI view. The TimelineProvider supplies timeline entries with data and update schedules. The SwiftUI View renders each entry into visual content.

struct WeatherWidget: Widget {
    let kind: String = "WeatherWidget"

    var body: some WidgetConfiguration {
        StaticConfiguration(kind: kind, provider: WeatherProvider()) { entry in
            WeatherWidgetEntryView(entry: entry)
                .containerBackground(.fill.tertiary, for: .widget)
        }
        .configurationDisplayName("Weather")
        .description("Shows current weather for your city.")
        .supportedFamilies([.systemSmall, .systemMedium])
    }
}

StaticConfiguration is for widgets without user-configurable properties. IntentConfiguration supports configurable widgets where users customize parameters like a stock ticker or city name.

How to Implement a TimelineProvider for Weather Data

TimelineProvider is a protocol with three methods. placeholder returns a quick stub entry for initial rendering. getSnapshot returns a single entry for the widget gallery preview. getTimeline returns an array of entries with a reload policy.

struct WeatherEntry: TimelineEntry {
    let date: Date
    let temperature: Int
    let condition: String
    let city: String
}

struct WeatherProvider: TimelineProvider {
    func placeholder(in context: Context) -> WeatherEntry {
        WeatherEntry(date: Date(), temperature: 22, condition: "Sunny", city: "Paris")
    }

    func getSnapshot(in context: Context, completion: @escaping (WeatherEntry) -> Void) {
        let entry = WeatherEntry(date: Date(), temperature: 22, condition: "Sunny", city: "Paris")
        completion(entry)
    }

    func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> Void) {
        let currentDate = Date()
        let nextUpdate = Calendar.current.date(byAdding: .hour, value: 1, to: currentDate)!

        WeatherService.shared.fetchWeather { data in
            let entry = WeatherEntry(
                date: currentDate,
                temperature: data.temperature,
                condition: data.condition,
                city: data.city
            )
            let timeline = Timeline(entries: [entry], policy: .after(nextUpdate))
            completion(timeline)
        }
    }
}

The Timeline policy controls when the system requests the next timeline: .after(Date) asks for a reload after the specified date, .atEnd reloads after the last entry date, and .never prevents automatic reloads.

How to Build the Widget UI with SwiftUI

WidgetKit requires SwiftUI for rendering. Build compact, glanceable layouts with system fonts and SF Symbols.

struct WeatherWidgetEntryView: View {
    var entry: WeatherProvider.Entry

    var body: some View {
        VStack(alignment: .leading, spacing: 4) {
            Text(entry.city)
                .font(.headline)
            HStack {
                Image(systemName: weatherIcon(for: entry.condition))
                    .font(.title)
                Text("(entry.temperature)°")
                    .font(.system(size: 28, weight: .bold))
            }
            Text(entry.condition)
                .font(.caption)
            Text(entry.date, style: .time)
                .font(.caption2)
                .foregroundColor(.secondary)
        }
    }

    func weatherIcon(for condition: String) -> String {
        switch condition.lowercased() {
        case "sunny": return "sun.max.fill"
        case "cloudy": return "cloud.fill"
        case "rainy": return "cloud.rain.fill"
        default: return "questionmark"
        }
    }
}

Use containerBackground modifier on iOS 17 and later for proper widget backgrounds. For iOS 14-16, apply a background color or material directly on the view.

How to Share Data Between the App and Widget

App Groups with UserDefaults enables data sharing between the container app and the widget extension. Enable App Groups in both targets and use the same group identifier.

let sharedDefaults = UserDefaults(suiteName: "group.com.example.weather")!
sharedDefaults.set("London", forKey: "selectedCity")

The widget reads the shared value in getTimeline.

func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> Void) {
    let defaults = UserDefaults(suiteName: "group.com.example.weather")!
    let city = defaults.string(forKey: "selectedCity") ?? "Paris"
    // Fetch weather for city
}

Call WidgetCenter.shared.reloadAllTimelines() from the app whenever data changes. This forces the widget to request a new timeline immediately.

WidgetCenter.shared.reloadAllTimelines()

How to Support Multiple Widget Families

WidgetKit provides three built-in families. systemSmall displays a compact view at 4 items per row. systemMedium displays a wider view at 2 items per row. systemLarge displays a tall view at 1 item per row.

Use the family environment variable to adjust the layout per family.

@Environment(.widgetFamily) var family

var body: some View {
    switch family {
    case .systemSmall:
        SmallWeatherView(entry: entry)
    case .systemMedium:
        MediumWeatherView(entry: entry)
    case .systemLarge:
        LargeWeatherView(entry: entry)
    @unknown default:
        SmallWeatherView(entry: entry)
    }
}

Practical Tips for WidgetKit Development

Keep widgets lightweight. Widgets are not mini apps. Display brief glanceable information. Avoid heavy computations in getSnapshot.

Use placeholder for fast initial rendering. This synchronous method returns immediately with dummy data so WidgetKit shows content while the real data loads.

Schedule updates wisely. The system limits widget refresh frequency to preserve battery. Hourly updates work well for weather. Use .after(Date) with a reasonable interval.

Always test on real devices. The simulator provides approximate widget rendering but does not accurately reproduce the Home Screen layout or update frequency.

Use containerBackground on iOS 17 and later. This modifier ensures proper appearance in widget stacks and across all widget families.

Summary

This tutorial covered the complete implementation process from setup to working code. You now have a functional implementation that you can extend and customize for your own projects. The same patterns apply to related features in the iOS ecosystem.

Frequently Asked Questions

Can I test this on the iOS Simulator? Some features require a physical device with a compatible processor. The article notes specific sections where the Simulator is sufficient and where a real device is required.

What iOS version is required? This tutorial targets iOS 14. Most of the APIs shown work on earlier versions as well, but some newer APIs may require the version specified in the article.

What are the prerequisites for this tutorial? You need Xcode installed on a Mac and basic familiarity with Swift programming. No prior experience with the specific framework is required.

All articles