iOS Widgets skill for Codex

Widgets that are fresh without burning budget. Codex builds widgets that render from a small snapshot in the App Group container, request timelines with the right reload policy, and reload only the affected kind when the app's data changes. It adds interactive App Intents buttons, Dynamic Island and lock screen Live Activities with push-token updates from the server, and ends activities when the real event ends.

  • Mobile & Desktop
  • $ios-widgets
  • 195 lines
  • 2 files
  • ~2,100 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- ios-widgets

Installs to ~/.agents/skills/ios-widgets/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $ios-widgets, or pick it from /skills. You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.

What Codex reads before loading it

Builds WidgetKit widgets and ActivityKit Live Activities for iOS - widget extensions, TimelineProvider and AppIntentTimelineProvider, timeline reload policies and budgets, App Group data sharing, configurable and interactive widgets with App Intents, lock screen accessory families, Live Activities with Dynamic Island layouts, push-token updates over APNs, and previews. Use when adding a home screen or lock screen widget, when a widget shows stale data or placeholders, when building a Live Activity for deliveries, rides or scores, or when updating activities from a server.

Use it when

  • Adding a home screen, lock screen or StandBy widget
  • Widgets show stale or placeholder content
  • Making widgets configurable or interactive with App Intents
  • Building or server-updating Live Activities and Dynamic Island layouts

Use something else when

How to check the result

Every supported family renders in previews with real and empty data, an app change updates the widget within seconds, and a Live Activity starts, updates by push, goes stale and ends on device.

The mistake it prevents

Using a mistyped App Group suite name or a group missing from release profiles, so the widget reads an empty store and shows placeholders only on release builds.

~/.agents/skills/ios-widgets/SKILL.md

--- name: ios-widgets description: Builds WidgetKit widgets and ActivityKit Live Activities for iOS - widget extensions, TimelineProvider and AppIntentTimelineProvider, timeline reload… license: MIT ---

Done well means: the widget renders instantly from data already on disk, timelines carry enough future entries that the system rarely needs to wake the extension, the app reloads timelines only when data actually changes, and Live Activities end themselves when the real-world event ends.

Before changing anything

  1. Read AGENTS.md, then check targets in the Xcode project: is there a widget extension, which App Group it shares with the app, and the deployment target (interactive widgets and containerBackground need iOS 17; push-to-start Live Activities need iOS 17.2).
  2. Find where the app stores the data the widget shows; the widget cannot call into the running app and should not depend on the network to render.
  3. Confirm both targets have the same App Group entitlement and that release provisioning profiles include it.

Data flow

App (writes snapshot) --> App Group container (JSON file, UserDefaults suite, or shared SwiftData/SQLite)
                     --> WidgetCenter.shared.reloadTimelines(ofKind:)
Widget extension (TimelineProvider reads snapshot) --> entries --> SwiftUI views
  • Write a small, widget-specific snapshot (the five items shown, not the whole database).
  • Reload only the affected kind, only when the snapshot changed. Calling reloadAllTimelines() on every app event wastes the daily budget.
  • If the widget must fetch from the network, do it in the provider with a short timeout and fall back to the last snapshot.

Timeline provider

struct OrderEntry: TimelineEntry {
    let date: Date
    let orders: [OrderSummary]
}

struct OrderProvider: TimelineProvider {
    func placeholder(in context: Context) -> OrderEntry { .init(date: .now, orders: OrderSummary.samples) }

    func getSnapshot(in context: Context, completion: @escaping (OrderEntry) -> Void) {
        completion(.init(date: .now, orders: context.isPreview ? OrderSummary.samples : SharedStore.loadOrders()))
    }

    func getTimeline(in context: Context, completion: @escaping (Timeline<OrderEntry>) -> Void) {
        let orders = SharedStore.loadOrders()
        let entries = (0..<4).map { i in
            OrderEntry(date: Calendar.current.date(byAdding: .minute, value: i * 15, to: .now)!, orders: orders)
        }
        completion(Timeline(entries: entries, policy: .atEnd))
    }
}

struct OrderWidget: Widget {
    var body: some WidgetConfiguration {
        StaticConfiguration(kind: "OrderWidget", provider: OrderProvider()) { entry in
            OrderWidgetView(entry: entry)
                .containerBackground(.fill.tertiary, for: .widget)
        }
        .configurationDisplayName("Orders")
        .description("Your active orders at a glance.")
        .supportedFamilies([.systemSmall, .systemMedium, .accessoryRectangular])
    }
}
Reload policyUse when
.atEndEntries cover a known window; ask again after the last
.after(date)Data changes on a schedule (next train, next calendar event)
.neverData changes only when the app writes it; app calls reloadTimelines
  • The system budgets refreshes (roughly a few dozen per day for a frequently viewed widget) and decides actual timing; requested dates are hints.
  • For countdowns and clocks use Text(date, style: .timer) or Text(timerInterval:countsDown:); they tick without new entries.
  • Keep the extension light: it has a tight memory limit (tens of MB). Downscale images before writing them to the App Group.

Configurable and interactive widgets

  • iOS 17+: use AppIntentConfiguration with an AppIntentTimelineProvider and a WidgetConfigurationIntent for user-chosen parameters (which list, which account).
  • Interactive controls: Button(intent:) and Toggle(isOn:intent:) with an AppIntent whose perform() writes to the shared store and returns. The system reloads the widget after it runs. Only buttons and toggles are interactive; everything else deep links.
  • Deep links: .widgetURL(_:) for the whole small widget, Link for regions in larger families; route them through the app’s URL router.
  • iOS 18 Control Center controls (ControlWidget) follow the same App Intents model.

Live Activities

  1. Add NSSupportsLiveActivities = YES to the app’s Info.plist.
  2. Define attributes in a file shared by app and extension:
struct DeliveryAttributes: ActivityAttributes {
    struct ContentState: Codable, Hashable {
        var status: String
        var eta: Date
    }
    var orderID: String
}
  1. Lay out lock screen and Dynamic Island in the widget extension:
ActivityConfiguration(for: DeliveryAttributes.self) { context in
    DeliveryLockScreen(state: context.state)
} dynamicIsland: { context in
    DynamicIsland {
        DynamicIslandExpandedRegion(.leading) { Image(systemName: "box.truck") }
        DynamicIslandExpandedRegion(.trailing) { Text(context.state.eta, style: .timer) }
        DynamicIslandExpandedRegion(.bottom) { Text(context.state.status) }
    } compactLeading: { Image(systemName: "box.truck") }
      compactTrailing: { Text(context.state.eta, style: .timer).frame(width: 44) }
      minimal: { Image(systemName: "box.truck") }
}
  1. Start from the app (in the foreground) and send the push token to your server:
let activity = try Activity.request(
    attributes: DeliveryAttributes(orderID: id),
    content: .init(state: .init(status: "Preparing", eta: eta), staleDate: eta.addingTimeInterval(1800)),
    pushType: .token
)
Task { for await token in activity.pushTokenUpdates { await api.register(liveActivityToken: token, order: id) } }
  1. Update and end from the server with APNs: apns-push-type: liveactivity, apns-topic: <bundle id>.push-type.liveactivity, payload aps.event of update or end, aps.timestamp (seconds), aps.content-state matching ContentState’s JSON keys exactly, optional aps.stale-date and aps.dismissal-date.
  • A Live Activity runs up to 8 hours and can stay on the lock screen for up to 4 more after ending. End it explicitly when the event ends (activity.end(_:dismissalPolicy:)).
  • Content state is limited to about 4 KB; send status and numbers, not images.
  • Users can disable Live Activities per app; check ActivityAuthorizationInfo().areActivitiesEnabled.
  • Frequent updates use priority 10 sparingly; Apple throttles high-priority updates, so use priority 5 for non-urgent changes.

Push transport details (tokens, JWT auth, errors) are in mobile-push-notifications.

Previews and debugging

#Preview(as: .systemMedium) { OrderWidget() } timeline: {
    OrderEntry(date: .now, orders: OrderSummary.samples)
    OrderEntry(date: .now, orders: [])
}
  • Run the widget extension scheme in Xcode to attach the debugger to the extension.
  • Placeholder-only rendering on device usually means the provider crashed or exceeded memory; check the device console filtered by the extension’s process name.
  • Test redacted rendering (.privacySensitive(), .redacted(reason: .placeholder)) for locked devices showing private data.

Gotchas

  • Each kind string is persistent identity; renaming it removes users’ placed widgets.
  • UserDefaults(suiteName:) with a typo returns a working but separate store; both targets silently read different data. Centralize the suite name in a shared constant.
  • Accessory (lock screen) families render in vibrant monochrome; test with widgetRenderingMode and do not rely on color.
  • Interactive widgets run the intent in the extension process; code it needs (models, store) must be in a framework or both targets.

Verification

  • Every supported family renders in previews, light and dark, with empty data
  • App change -> widget updates within seconds via reloadTimelines(ofKind:)
  • Interactive button updates state with the app not running
  • Live Activity: start, server update, stale date, end, and disabled-by-user paths
  • Release build on device (App Group in release profiles)

Output

**Widgets:** <kind -> families, provider, reload policy, data source>
**Intents:** <configuration and interactive intents>
**Live Activity:** <attributes, start trigger, server update path, end condition>
**Tested:** <device, iOS version, families> -> result

What the zip contains

SKILL.md plus the interface file below. Nothing runs on install.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "iOS Widgets"
  short_description: "Widgets that are fresh without burning budget"
  default_prompt: "$ios-widgets Add a home screen and lock screen widget that shows today's tasks"

About this skill

Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.

esc
Just launched

Codex Skills Pro is live

79 premium skills for Codex, each with scripts, references and an evidence report that proves the result.

Bought one by one$882.21
All 79 skills$49
94% off

That's $0.62 a skill, paid once.

Get all 79 for $49Or pick single skills from $4.99 →

Secure checkout by Creem