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.atEndor.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.