How to Build iOS 16 Lock Screen Widgets with WidgetKit

WidgetKit allows developers to extend the reach of their iOS applications by displaying glanceable, dynamic content directly on the user's Home Screen and Lock Screen. With iOS 16, Apple expanded WidgetKit to support Lock Screen widgets, enabling users to access vital information even when their device is locked. This guide explains what WidgetKit is, breaks down the specific Lock Screen widget families introduced in iOS 16, and walks through the fundamental steps developers take to build and configure them using SwiftUI.

Understanding WidgetKit

WidgetKit is an Apple framework introduced in iOS 14 that uses SwiftUI to build lightweight, glanceable extensions for apps. Rather than running continuously in the background, widgets rely on a timeline architecture. Developers define a schedule of updates—known as a timeline—allowing iOS to render views ahead of time and display them with minimal impact on battery life and system performance. In iOS 16, WidgetKit gained support for Lock Screen placements, adapting the framework's existing timeline model to a more constrained, monochromatic design environment.

Lock Screen Widget Families

Lock Screen widgets in iOS 16 use the "accessory" widget families. Developers target three specific form factors:

  • accessoryInline: A single line of text and an optional icon placed directly above the Lock Screen clock.
  • accessoryCircular: A compact, circular gauge, progress ring, or icon placed below the clock.
  • accessoryRectangular: A larger rectangular area placed below the clock that accommodates up to three lines of text, graphs, or small data lists.

Step-by-Step Implementation

Building a Lock Screen widget involves four core components: the widget definition, the timeline entry, the timeline provider, and the SwiftUI view.

1. Add the Widget Extension

In Xcode, navigate to File > New > Target and select Widget Extension. Ensure "Include Live Activity" is unchecked if only building standard widgets, and complete the setup. Xcode generates the boilerplate files containing the basic WidgetKit protocol implementations.

2. Define the Timeline Entry

The timeline entry holds the date the widget should render and any custom data the view needs to display:

struct SimpleEntry: TimelineEntry {
    let date: Date
    let score: Int
}

3. Create the Timeline Provider

The TimelineProvider determines when the widget updates. It implements three required methods:

  • placeholder(in:): Displays placeholder content while the widget loads.
  • getSnapshot(in:completion:): Provides a quick preview for the widget gallery.
  • getTimeline(in:completion:): Generates an array of entries scheduled for specific times, along with a reload policy (such as .atEnd or .never).
struct Provider: TimelineProvider {
    func placeholder(in context: Context) -> SimpleEntry {
        SimpleEntry(date: Date(), score: 0)
    }

    func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> Void) {
        completion(SimpleEntry(date: Date(), score: 42))
    }

    func getTimeline(in context: Context, completion: @escaping (Timeline<SimpleEntry>) -> Void) {
        let entry = SimpleEntry(date: Date(), score: 42)
        let timeline = Timeline(entries: [entry], policy: .after(Date().addingTimeInterval(3600)))
        completion(timeline)
    }
}

4. Build the Adaptive SwiftUI View

The view reads the widgetFamily environment variable to determine which Lock Screen layout to render:

import SwiftUI
import WidgetKit

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

    var body: some View {
        switch family {
        case .accessoryInline:
            Text("Score: \(entry.score)")
        case .accessoryCircular:
            Gauge(value: Double(entry.score), in: 0...100) {
                Text("Score")
            } currentValueLabel: {
                Text("\(entry.score)")
            }
            .gaugeStyle(.accessoryCircular)
        case .accessoryRectangular:
            VStack(alignment: .leading) {
                Text("Daily Progress")
                    .font(.headline)
                Text("Current Score: \(entry.score)")
                    .font(.subheadline)
            }
        default:
            Text("Unsupported")
        }
    }
}

5. Declare Supported Families in the Widget Configuration

In the widget's main configuration struct, declare which accessory families your widget supports using the .supportedFamilies modifier:

@main
struct LockScreenWidget: Widget {
    let kind: String = "LockScreenWidget"

    var body: some WidgetConfiguration {
        StaticConfiguration(kind: kind, provider: Provider()) { entry in
            LockScreenWidgetView(entry: entry)
        }
        .configurationDisplayName("Progress Tracker")
        .description("Track your score directly on the Lock Screen.")
        .supportedFamilies([
            .accessoryInline,
            .accessoryCircular,
            .accessoryRectangular
        ])
    }
}

Design and Performance Considerations

Lock Screen widgets have distinct constraints compared to Home Screen widgets:

  • Color and Tinting: Lock Screen widgets are primarily monochromatic. The system applies vibrant rendering modes, adjusting brightness and contrast automatically to ensure readability against various user wallpapers. Use standard SwiftUI text and system symbols (SF Symbols) for consistent rendering.
  • Interactivity: Lock Screen widgets do not support custom buttons or scrolling. Tapping an accessory widget always opens the parent app. Developers can use the .widgetURL(_:) modifier to deep-link directly into relevant content within the app.
  • Budgeted Updates: Timeline updates are budgeted by iOS to preserve battery life. Developers should push timeline reloads efficiently using server-side background notifications (push-to-update) or optimize update intervals rather than requesting frequent local reloads.