App Extensions & WidgetKit
WidgetKit timelines, Live Activities, App Intents, share and notification extensions, App Groups
- 01
A teammate wants the widget to reuse the app's session singleton, its networking stack and its 40 MB image cache — what about a widget extension rules that out?
EasyA widget extension is a separate process with its own bundle identifier and a memory ceiling a fraction of the app's, so it shares your source code but never your app's running state.
import WidgetKit import SwiftUI import ImageIO @main struct DashboardBundle: WidgetBundle { … - 02
In the widget gallery your widget sits blank for a second and then shows the signed-out state, while on the Home Screen it is fine — which provider callback is wrong?
EasyThe gallery calls
snapshot, and it passes a context withisPreview == true— that call must return canned sample data immediately instead of waiting on a store or the network.struct SalesEntry: TimelineEntry { let date: Date let total: Decimal let store: StoreEntity } … - 03
The sales widget still shows yesterday's total at noon, although the app has been opened twice since — what does your timeline's reload policy actually promise?
MediumA reload policy tells WidgetKit the earliest moment it may ask you for a new timeline — it is a request, not a schedule, and once the current timeline's entries are exhausted under
.nevernothing will ever ask again.func timeline(for configuration: SalesIntent, in context: Context) async -> Timeline<SalesEntry> { let hours = await SalesStore.forecast(for: configuration.store) let entries = hours.map { SalesEntry(date: $0.start, total: $0.total) } // WRONG: one entry dated now with .atEnd asks for a reload immediately, forever. … - 04
Your widget updates faithfully all morning and then freezes until the evening, with no crash and no error — how does the refresh budget decide that?
MediumEach installed widget has a rolling daily budget of background reloads — Apple's guidance is roughly 40 to 70 a day, one every 15 to 60 minutes — and once it is spent the system quietly stops honouring your reload policy until it refills.
struct ShiftEntry: TimelineEntry { let date: Date let shift: Shift var relevance: TimelineEntryRelevance? } … - 05
The widget must let each person pick which project it shows, but the .intentdefinition file is gone from the new codebase — where does that list of projects get built now?
MediumIn
AppIntentConfigurationthe choices come from anEntityQueryon anAppEntity, and that query runs inside the widget extension while the user edits the widget — never in your app.import AppIntents import WidgetKit struct ProjectEntity: AppEntity { let id: String let name: String … - 06
Tapping the tick in your reminders widget does nothing for a second and then the whole widget flashes — what may an interactive widget do, and how should that tap look?
MediumA widget is interactive only through
Button(intent:)andToggle(isOn:intent:), the tap runs anAppIntentoutside the render pass, and it is.invalidatableContent()that makes the wait look deliberate instead of broken.import AppIntents import WidgetKit import SwiftUI struct CompleteTaskIntent: AppIntent { static let title: LocalizedStringResource = "Complete task" … - 07
Every row of your medium widget opens the same screen, and on the Lock Screen the row links do nothing at all — what are the rules for linking out of a widget?
MediumLinkgives per-element destinations but only in the medium, large and extra-large families; everywhere else — small widgets and every Lock Screen accessory family — the whole widget is a single tap target and only.widgetURLapplies.struct OrdersWidgetView: View { let entry: OrdersEntry @Environment(\.widgetFamily) private var family var body: some View { VStack(alignment: .leading) { … - 08
For a delivery Live Activity — order id, restaurant, courier name, ETA, current step — which of those belong in ContentState, and what does getting the split wrong cost?
MediumEverything that can change during the activity goes in
ContentState; everything else goes in the attributes, which are fixed atrequesttime and can never be updated afterwards.import ActivityKit struct DeliveryAttributes: ActivityAttributes { // Static half: fixed at request time, never updatable. let orderID: String let restaurantName: String … - 09
Ops wants to start and update a delivery Live Activity entirely from the server — which push tokens do they need, and what expires?
MediumTwo different tokens: a push-to-start token that belongs to the activity type and exists before anything is running, and a per-activity token you only get after that particular activity has started.
import ActivityKit // 1. Push-to-start: registered once at launch, before any activity exists. func observePushToStart() { Task { for await token in Activity<DeliveryAttributes>.pushToStartTokenUpdates { … - 10
Your Live Activity looks right in the Dynamic Island until the user starts a timer in another app, and then it shrinks to a circle — what is the system doing?
MediumThe compact presentation is only used when yours is the only active Live Activity; as soon as a second app has one, the system falls back to the minimal presentation for both.
struct DeliveryLiveActivity: Widget { var body: some WidgetConfiguration { ActivityConfiguration(for: DeliveryAttributes.self) { context in // Required: devices without an Island, and the banner on update, use this. LockScreenView(context: context) .activityBackgroundTint(.black.opacity(0.6)) … - 11
Your widget is fine on the Home Screen, but on the Lock Screen its coloured status dots become identical grey blobs — what did you skip?
MediumA widget is rendered in three modes —
.fullColor,.accentedand.vibrant— and the last two discard your colours, so anything that carries meaning in hue alone stops carrying it.struct BuildStatusView: View { @Environment(\.widgetRenderingMode) private var renderingMode let build: Build var body: some View { VStack(alignment: .leading, spacing: 6) { … - 12
A share extension receives a 200 MB video, stages the file fine, and then the upload silently never completes — what is wrong with uploading from the extension?
MediumcompleteRequesttears the extension process down at once, and the system kills it anyway when the sheet dismisses, so any in-process transfer dies with it — the extension's job is to hand the file off, not to send it.import Social import UniformTypeIdentifiers final class ShareViewController: SLComposeServiceViewController { private let groupID = "group.com.example.clips" … - 13
Product wants "Log a coffee in Brewlog" to work from Siri and Spotlight with no setup by the user — what do you ship beyond the AppIntent itself?
HardAn
AppIntenton its own only reaches the Shortcuts app; to be voice- and Spotlight-invocable out of the box it must also be declared in anAppShortcutsProvider, with phrases that name the app and aparameterSummarythat reads as a sentence.import AppIntents struct LogDrinkIntent: AppIntent { static let title: LocalizedStringResource = "Log a drink" static let description = IntentDescription("Adds a drink to today's journal.") … - 14
Rich push images show up sometimes, and the failures arrive as plain text with nothing decrypted — what is your notification service extension getting wrong?
HardYou get roughly 30 seconds, and if you have not called the content handler by then the system delivers the original payload — a slow attachment download is exactly how a rich push degrades back into plain text.
import UserNotifications final class NotificationService: UNNotificationServiceExtension { private var contentHandler: ((UNNotificationContent) -> Void)? private var bestAttempt: UNMutableNotificationContent? … - 15
The widget shows "Unable to Load" on a real device, none of your print statements reach Xcode, and the app itself runs fine — how do you debug that?
HardThe widget runs in a separate, system-launched process that Xcode is not attached to when you run the app scheme, so you attach to the extension deliberately and log through
Loggerinstead ofprint.import OSLog import WidgetKit private let log = Logger(subsystem: "com.example.notes.widget", category: "timeline") struct NotesProvider: TimelineProvider { … - 16
The Control Centre toggle for your smart lock still reads "Unlocked" hours after the user locked the door inside the app — what is the control missing?
HardA control's displayed value comes from a
ControlValueProviderrunning in the widget extension on the system's schedule, never from your app, so a state change made anywhere else has to be published to shared storage and followed byControlCenter.shared.reloadControls(ofKind:).import AppIntents import WidgetKit import SwiftUI struct SetLockIntent: SetValueIntent { static let title: LocalizedStringResource = "Lock the front door" …