Widgets provide quick access to information from your app. Learn to create beautiful, interactive iOS widgets.
Widget Extension Setup
Create Widget Extension
- 1File > New > Target
- 2Choose "Widget Extension"
- 3Name your widget
- 4Choose "Include Configuration Intent" for interactive widgets
Basic Widget Structure
swift
import WidgetKit
import SwiftUI
struct SimpleWidget: Widget {
let kind: String = "SimpleWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: Provider()) { entry in
SimpleWidgetEntryView(entry: entry)
}
.configurationDisplayName("Simple Widget")
.description("A simple widget example")
.supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
}
}
struct SimpleWidgetEntry: TimelineEntry {
let date: Date
let title: String
let description: String
}
struct Provider: TimelineProvider {
func placeholder(in context: Context) -> SimpleWidgetEntry {
SimpleWidgetEntry(
date: Date(),
title: "Widget Title",
description: "Widget description"
)
}
func getSnapshot(in context: Context, completion: @escaping (SimpleWidgetEntry) -> Void) {
let entry = SimpleWidgetEntry(
date: Date(),
title: "Snapshot",
description: "Snapshot content"
)
completion(entry)
}
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> Void) {
let currentDate = Date()
let entry = SimpleWidgetEntry(
date: currentDate,
title: "Current Time",
description: "Updated at \(currentDate.formatted(date: .omitted, time: .standard))"
)
let nextUpdate = Calendar.current.date(byAdding: .minute, value: 15, to: currentDate)!
let timeline = Timeline(entries: [entry], policy: .after(nextUpdate))
completion(timeline)
}
}
struct SimpleWidgetEntryView: View {
var entry: Provider.Entry
var body: some View {
VStack(alignment: .leading, spacing: 8) {
Text(entry.title)
.font(.headline)
Text(entry.description)
.font(.caption)
.foregroundColor(.secondary)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.blue.opacity(0.1))
}
}Multiple Widget Sizes
Adaptive Layout
swift
struct AdaptiveWidgetEntryView: View {
var entry: Provider.Entry
@Environment(\.widgetFamily) var family
var body: some View {
switch family {
case .systemSmall:
SmallWidgetView(entry: entry)
case .systemMedium:
MediumWidgetView(entry: entry)
case .systemLarge:
LargeWidgetView(entry: entry)
case .systemExtraLarge:
ExtraLargeWidgetView(entry: entry)
@unknown default:
SmallWidgetView(entry: entry)
}
}
}
struct SmallWidgetView: View {
var entry: Provider.Entry
var body: some View {
VStack {
Image(systemName: "star.fill")
.font(.largeTitle)
Text(entry.title)
.font(.headline)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(
LinearGradient(
colors: [.blue, .purple],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
}
}
struct MediumWidgetView: View {
var entry: Provider.Entry
var body: some View {
HStack {
VStack(alignment: .leading) {
Text(entry.title)
.font(.headline)
Text(entry.description)
.font(.caption)
}
Spacer()
Image(systemName: "star.fill")
.font(.title)
.foregroundColor(.yellow)
}
.padding()
}
}
struct LargeWidgetView: View {
var entry: Provider.Entry
var body: some View {
VStack(alignment: .leading, spacing: 12) {
HStack {
Text(entry.title)
.font(.title2)
.fontWeight(.bold)
Spacer()
}
Divider()
VStack(alignment: .leading, spacing: 8) {
Label("Feature 1", systemImage: "checkmark.circle")
Label("Feature 2", systemImage: "checkmark.circle")
Label("Feature 3", systemImage: "checkmark.circle")
}
.font(.caption)
Spacer()
Text(entry.description)
.font(.caption2)
.foregroundColor(.secondary)
}
.padding()
}
}Interactive Widgets
Widget Configuration
swift
struct ConfigurationWidget: Widget {
let kind: String = "ConfigurationWidget"
var body: some WidgetConfiguration {
AppIntentConfiguration(kind: kind, intent: ConfigurationIntent.self, provider: Provider()) { entry in
ConfigurationWidgetEntryView(entry: entry)
}
.configurationDisplayName("Configurable Widget")
.description("A widget with configuration options")
.supportedFamilies([.systemSmall, .systemMedium])
}
}
struct ConfigurationIntent: AppIntent {
static var title: LocalizedStringResource = "Configuration"
static var description = IntentDescription("Configure the widget")
@Parameter(title: "Background Color")
var backgroundColor: AppEnum?
static var backgroundColorParameter: Parameter<AppEnum> {
Parameter(
title: "Background Color",
options: [
AppEnum(1, "Blue"),
AppEnum(2, "Green"),
AppEnum(3, "Orange")
]
)
}
}Interactive Buttons
swift
struct InteractiveWidgetEntryView: View {
var entry: Provider.Entry
var body: some View {
VStack(spacing: 12) {
Text("Quick Actions")
.font(.headline)
HStack(spacing: 12) {
Link(destination: URL(string: "myapp://action1")!) {
Label("Action 1", systemImage: "heart.fill")
.frame(maxWidth: .infinity)
}
.buttonStyle(.borderedProminent)
Link(destination: URL(string: "myapp://action2")!) {
Label("Action 2", systemImage: "star.fill")
.frame(maxWidth: .infinity)
}
.buttonStyle(.borderedProminent)
}
}
.padding()
}
}Data Sharing
App Group Setup
- 1Enable App Groups in capabilities
- 2Use shared UserDefaults or FileManager
swift
class WidgetDataSharing {
static let shared = WidgetDataSharing()
private let suiteName = "group.com.yourcompany.widget"
private let defaults: UserDefaults
init() {
defaults = UserDefaults(suiteName: suiteName)!
}
func saveData<T: Encodable>(_ data: T, forKey key: String) {
if let encoded = try? JSONEncoder().encode(data) {
defaults.set(encoded, forKey: key)
}
}
func getData<T: Decodable>(forKey key: String, type: T.Type) -> T? {
guard let data = defaults.data(forKey: key),
let decoded = try? JSONDecoder().decode(type, from: data) else {
return nil
}
return decoded
}
}
// Usage in main app
struct WidgetData: Codable {
let title: String
let value: Int
let lastUpdated: Date
}
// Save from main app
let data = WidgetData(title: "Dashboard", value: 42, lastUpdated: Date())
WidgetDataSharing.shared.saveData(data, forKey: "widgetData")
// Load in widget
let provider = Provider()
if let data: WidgetData = WidgetDataSharing.shared.getData(forKey: "widgetData", type: WidgetData.self) {
print("Widget data: \(data)")
}Timeline Management
Multiple Timeline Entries
swift
struct Provider: TimelineProvider {
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> Void) {
var entries: [SimpleWidgetEntry] = []
let currentDate = Date()
let calendar = Calendar.current
// Create entries for next 24 hours
for hour in 0..<24 {
if let date = calendar.date(byAdding: .hour, value: hour, to: currentDate) {
let entry = SimpleWidgetEntry(
date: date,
title: "Hour \(hour)",
description: "Widget content"
)
entries.append(entry)
}
}
let timeline = Timeline(entries: entries, policy: .atEnd)
completion(timeline)
}
}Best Practices
- 1Performance: Keep widgets lightweight
- 2Updates: Update timeline efficiently
- 3Images: Use system icons or optimized images
- 4Data: Share data via App Groups
- 5Interactivity: Use widget intents for interactivity
- 6Testing: Test all widget sizes
- 7Deep Links: Handle deep links properly
- 8Refresh: Refresh content appropriately
Widgets extend your app's presence and provide quick value to users!