App Extensions & WidgetKit
Irbisa · cheatsheetSeptember 13, 2026

App Extensions & WidgetKit

WidgetKit timelines, Live Activities, App Intents, share and notification extensions, App Groups

Middle Developer16 itemscompressed for a skim
  1. 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?

    Easy

    A 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 {
    …
  2. 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?

    Easy

    The gallery calls snapshot, and it passes a context with isPreview == 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
    }
    …
  3. 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?

    Medium

    A 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 .never nothing 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.
    …
  4. 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?

    Medium

    Each 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?
    }
    …
  5. 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?

    Medium

    In AppIntentConfiguration the choices come from an EntityQuery on an AppEntity, 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
    …
  6. 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?

    Medium

    A widget is interactive only through Button(intent:) and Toggle(isOn:intent:), the tap runs an AppIntent outside 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"
    …
  7. 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?

    Medium

    Link gives 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 .widgetURL applies.

    struct OrdersWidgetView: View {
      let entry: OrdersEntry
      @Environment(\.widgetFamily) private var family
    
      var body: some View {
        VStack(alignment: .leading) {
    …
  8. 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?

    Medium

    Everything that can change during the activity goes in ContentState; everything else goes in the attributes, which are fixed at request time 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
    …
  9. 09

    Ops wants to start and update a delivery Live Activity entirely from the server — which push tokens do they need, and what expires?

    Medium

    Two 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. 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?

    Medium

    The 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. 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?

    Medium

    A widget is rendered in three modes — .fullColor, .accented and .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. 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?

    Medium

    completeRequest tears 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. 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?

    Hard

    An AppIntent on its own only reaches the Shortcuts app; to be voice- and Spotlight-invocable out of the box it must also be declared in an AppShortcutsProvider, with phrases that name the app and a parameterSummary that 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. 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?

    Hard

    You 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. 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?

    Hard

    The 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 Logger instead of print.

    import OSLog
    import WidgetKit
    
    private let log = Logger(subsystem: "com.example.notes.widget", category: "timeline")
    
    struct NotesProvider: TimelineProvider {
    …
  16. 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?

    Hard

    A control's displayed value comes from a ControlValueProvider running 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 by ControlCenter.shared.reloadControls(ofKind:).

    import AppIntents
    import WidgetKit
    import SwiftUI
    
    struct SetLockIntent: SetValueIntent {
      static let title: LocalizedStringResource = "Lock the front door"
    …