Widgets That Work in Real Life
Many widgets look polished but fail in daily use because they overload information and refresh too infrequently.
A better mental model:
- One primary signal (status, next action, or trend)
- One high-value action via App Intent
- Predictable timeline refresh
Choosing the Right Widget Type
| Widget Type | Best For | Common Mistake |
|---|---|---|
| Small | Single KPI or next task | Too much text |
| Medium | Summary + one action | Multiple competing CTAs |
| Lock Screen | Real-time-ish glance | Dense visuals on tiny space |
Start from user context, not design preferences.
App Intents: Keep Actions Focused
Use App Intents for actions that are:
- Safe to run quickly
- Understandable without opening the app
- Valuable from home/lock screen
Great examples include โMark done,โ โStart timer,โ and โLog water.โ
import AppIntents
struct CompleteTaskIntent: AppIntent {
static var title: LocalizedStringResource = "Complete Task"
static var description = IntentDescription("Mark a task as completed from the widget.")
@Parameter(title: "Task ID")
var taskID: String
func perform() async throws -> some IntentResult {
try await TaskService.shared.complete(taskID: taskID)
WidgetCenter.shared.reloadTimelines(ofKind: "TaskWidget")
return .result()
}
}
Timeline Strategy: Fresh Enough, Not Wasteful
Three rules keep your widget healthy:
- Refresh on meaningful data changes
- Precompute upcoming entries when schedule is predictable
- Avoid minute-level refresh unless product value clearly justifies it
This improves battery behavior while keeping content useful.
import WidgetKit
struct TaskEntry: TimelineEntry {
let date: Date
let title: String
let remainingCount: Int
}
struct TaskProvider: TimelineProvider {
func placeholder(in context: Context) -> TaskEntry {
TaskEntry(date: .now, title: "Daily Plan", remainingCount: 3)
}
func getSnapshot(in context: Context, completion: @escaping (TaskEntry) -> Void) {
completion(TaskEntry(date: .now, title: "Daily Plan", remainingCount: 3))
}
func getTimeline(in context: Context, completion: @escaping (Timeline<TaskEntry>) -> Void) {
let now = Date()
let entry = TaskEntry(date: now, title: "Daily Plan", remainingCount: TaskService.shared.remainingToday())
let refresh = Calendar.current.date(byAdding: .minute, value: 30, to: now) ?? now.addingTimeInterval(1800)
completion(Timeline(entries: [entry], policy: .after(refresh)))
}
}
Complete Widget Example
import SwiftUI
import WidgetKit
struct TaskWidgetEntryView: View {
var entry: TaskProvider.Entry
var body: some View {
VStack(alignment: .leading, spacing: 8) {
Text(entry.title).font(.headline)
Text("\(entry.remainingCount) tasks left").font(.subheadline)
Button(intent: CompleteTaskIntent(taskID: "next-task-id")) {
Text("Mark next done")
}
.buttonStyle(.borderedProminent)
}
.containerBackground(.fill.tertiary, for: .widget)
}
}
struct TaskWidget: Widget {
let kind: String = "TaskWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: TaskProvider()) { entry in
TaskWidgetEntryView(entry: entry)
}
.configurationDisplayName("Today Tasks")
.description("See your next task and complete it quickly.")
.supportedFamilies([.systemSmall, .systemMedium])
}
}
UX Patterns That Increase Engagement
- Show relative time (โupdated 12m agoโ) to build trust
- Use clear empty states (โNo tasks due todayโ)
- Keep tap targets intentional and limited
- Mirror app terminology so users feel continuity
Consistency beats novelty for recurring widgets.
Shipping Checklist โ
- Widget communicates one primary idea in <2 seconds
- App Intent action completes without confusion
- Timeline refresh policy is documented
- Empty/error states are designed, not default
- Accessibility labels are present and meaningful
Final Thought
Useful widgets remove steps from a habit. If users can glance, act, and continue their day, your widget is doing its job.
Share on
Twitter Facebook LinkedInโ Buy me a coffee! ๐
If you found this article helpful, consider buying me a coffee to support my work! ๐
