iOS Widget Development: App Extension Basics, Architecture, and Practical Implementation Tips
This article explains the fundamentals of iOS App Extensions, details the WidgetKit framework, shows how to configure single and multiple widgets, describes data sharing, refresh strategies, interaction patterns, and shares practical lessons learned from building flight‑ticket widgets for a production app.
Introduction – iOS 14 introduced Widgets as a key way for users to personalize their home screen. The author, a senior R&D manager at Ctrip, describes the need to implement flight‑ticket related widgets and shares the development experience.
App Extension Overview – Since iOS 8, App Extensions allow developers to create isolated, task‑specific binaries (with the .appex suffix) that run within a host app. Extensions cannot be installed independently and communicate with the host app via shared containers (App Groups) using NSUserDefaults or NSFileManager . The lifecycle involves a containing app, a host app that launches the extension, and the extension itself.
Widget Basics – Widgets are the evolution of Today Extensions, now usable on the home screen in three fixed sizes (small, medium, large). Development uses WidgetKit and SwiftUI; UIKit is no longer supported for new widgets. Widgets are added by users through the system widget picker.
Widget Development Framework
Single widget: implement the Widget protocol.
Multiple widgets: implement the WidgetBundle protocol.
Example of a single widget entry point:
@main
struct Widget1: Widget {
let kind: String = "widgetTag"
var body: some WidgetConfiguration {
...
}
}Example of a widget bundle:
@main
struct TripWidgets: WidgetBundle {
@WidgetBundleBuilder
var body: some Widget {
Widget1()
Widget2()
Widget3()
...
}
}Widget configuration uses StaticConfiguration (non‑editable) or IntentConfiguration (editable). A typical static configuration looks like:
struct Widget1: Widget {
let kind: String = "widgetTag"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: Provider()) { entry in
Widget1View(entry: entry)
}
.configurationDisplayName("Travel Inspiration")
.description("Start your next journey now")
.supportedFamilies([.systemSmall, .systemMedium])
}
}Refresh Strategy – Widgets refresh via the TimelineProvider protocol, which defines placeholder , getSnapshot , and getTimeline . Developers supply an array of TimelineEntry objects with associated dates and a TimelineReloadPolicy (e.g., atEnd , after(Date) , never ). Example protocol definition:
public protocol TimelineProvider {
associatedtype Entry: TimelineEntry
func placeholder(in context: Context) -> Entry
func getSnapshot(in context: Context, completion: @escaping (Entry) -> Void)
func getTimeline(in context: Context, completion: @escaping (Timeline
) -> Void)
}Timeline struct:
public struct Timeline
where EntryType: TimelineEntry {
public let entries: [EntryType]
public let policy: TimelineReloadPolicy
public init(entries: [EntryType], policy: TimelineReloadPolicy) { ... }
}App‑Widget Interaction – Data sharing uses NSUserDefaults :
// Store
let userDefaults = [[NSUserDefaults alloc] initWithSuiteName:@"group.xxx.xxx.xx"];
[userDefaults setObject:@"test_content" forKey:@"test"];
[userDefaults synchronize];
// Retrieve
let userDefaults = [[NSUserDefaults alloc] initWithSuiteName:@"group.xxx.xxx.xx"];
NSString *content = [userDefaults objectForKey:@"test"];or NSFileManager for file‑based sharing:
// Store
NSURL *containerURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:@"group.xxx.xxx.xx"];
containerURL = [containerURL URLByAppendingPathComponent:@"testfile"];
[data writeToURL:containerURL atomically:YES];
// Retrieve
NSURL *containerURL = [[NSFileManager defaultManager] containerURLForSecurityApplicationGroupIdentifier:@"group.xxx.xxx.xx"];
containerURL = [containerURL URLByAppendingPathComponent:@"testfile"];
NSData *value = [NSData dataWithContentsOfURL:containerURL];To force a refresh from the host app:
WidgetCenter.shared.reloadTimelines(ofKind: "widgetTag")Widgets can launch the containing app via URL schemes. The app handles the URL in the scene delegate:
func scene(_ scene: UIScene, openURLContexts URLContexts: Set
) {
// Process URLContexts.first?.url
}Practical Experience Summary
Maximum of 5 widget types per app, each supporting up to three sizes (15 widgets total).
Only SwiftUI components listed in Apple’s WidgetKit documentation are usable; unsupported views are ignored.
Images must be loaded synchronously; large images (>200 KB) may fail to render.
Small widgets support only a single widgetURL action; medium/large widgets can use Link for multiple tappable areas.
Shared code must avoid APIs marked NS_EXTENSION_UNAVAILABLE (e.g., HealthKit UI, long‑running background tasks).
Refresh frequency is limited (minimum ~5 minutes) and subject to system‑controlled heuristics.
Widget size contributes to the overall app bundle size; no hot‑patch mechanism exists for widgets.
References
App Extension Documentation
Creating a Widget Extension
Widget Refresh Mechanism
Widget Design Guidelines
Ctrip Technology
Official Ctrip Technology account, sharing and discussing growth.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.