Mobile Development 14 min read

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.

Ctrip Technology
Ctrip Technology
Ctrip Technology
iOS Widget Development: App Extension Basics, Architecture, and Practical Implementation Tips

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

Mobile DevelopmentiOSSwiftSwiftUIWidgetKitApp Extension
Ctrip Technology
Written by

Ctrip Technology

Official Ctrip Technology account, sharing and discussing growth.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.