iOS Widget Development (WidgetKit)
WidgetKit is Apple's framework for widgets on home screen, lock screen, and Standby Mode (iOS 17). Widgets don't run as background processes — they're static SwiftUI View snapshots that system updates by schedule via TimelineProvider. Main architectural constraint: widget cannot fetch data at render time, it only displays data prepared in advance.
How WidgetKit Works Inside
TimelineProvider — Widget Core
struct MyWidgetProvider: TimelineProvider {
func placeholder(in context: Context) -> SimpleEntry {
SimpleEntry(date: Date(), data: .placeholder)
}
func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> Void) {
completion(SimpleEntry(date: Date(), data: cachedData()))
}
func getTimeline(in context: Context, completion: @escaping (Timeline<SimpleEntry>) -> Void) {
Task {
let data = await fetchData()
let entries = buildEntries(from: data)
let timeline = Timeline(entries: entries, policy: .atEnd)
completion(timeline)
}
}
}
getTimeline is called by system by schedule, not by app request. Apple doesn't guarantee precision — widget updates "approximately" on time. Widgets get update budget: ~40-70 updates per day across all device widgets. Budget exhausted — updates delayed.
policy: .atEnd — request new timeline when current entries end. policy: .after(date) — request at specific time. policy: .never — don't request, only on explicit WidgetCenter.shared.reloadTimelines(ofKind:) from main app.
App Group for Data Transfer
Widget is separate Extension, no direct access to main app data. Common container — App Group:
// App writes:
let defaults = UserDefaults(suiteName: "group.com.company.app")
defaults?.set(encodedData, forKey: "widgetData")
// Widget reads:
let defaults = UserDefaults(suiteName: "group.com.company.app")
let data = defaults?.data(forKey: "widgetData")
FileManager with containerURL(forSecurityApplicationGroupIdentifier:) — for files (images, databases). CoreData with NSPersistentContainer and common URL — for complex data.
Common mistake: try Keychain without accessGroup — widget won't access main app's Keychain without explicit group.
Sizes and Configuration
System families: .systemSmall, .systemMedium, .systemLarge, .systemExtraLarge (iPad only). Lock screen (iOS 16+): .accessoryCircular, .accessoryRectangular, .accessoryInline. Standby: .systemSmall in Full Screen mode.
@Environment(\.widgetFamily) inside View — for conditional rendering across sizes. One widget = one Widget struct; multiple formats — multiple View per family.
WidgetConfiguration comes in two types:
-
StaticConfiguration— no user customization -
IntentConfiguration— user configures via Siri Intents or App Intents (iOS 17): which city, which account, which cryptocurrency
App Intents (iOS 17+) replaces SiriKit Intents for widget configuration. AppIntentConfiguration + WidgetConfigurationIntent — type-safe way without .intentdefinition files.
Use Case: Food Delivery Widget
Widget shows current order status: "Preparing", "In delivery", "Arrived" with animation. Main challenge — frequent updates (every 2-3 minutes during active order) exhausts budget.
Solution: Push notification from backend on status change → app receives background notification → calls WidgetCenter.shared.reloadAllTimelines(). Widget updates not by schedule but by event. Budget not spent.
For status change animation — .contentTransition(.identity) or .contentTransition(.numericText()) in SwiftUI — smooth replacement without flicker.
Lock Screen and Standby
Accessory widgets (lock screen) — small, black-and-white in standard state, with accent color. widgetAccentable() modifier — marks element as "colorful" when accent enabled.
Standby (iPhone on stand, iOS 17): widget .systemSmall displayed full screen. Extra requirement: readability from 1-2 meters. Large fonts, minimal details.
Timeframe: 3-5 days. Depends on configuration complexity and need for backend sync. Cost calculated after requirements analysis.







