widgetkit — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited widgetkit (Agent Skill) and scored it 45/100 (orange). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 2 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 2 flagged
A base64 string of 128+ characters appears in a documentation file. Encoded prompt injection hides the hostile instruction in base64 — invisible to keyword filters — and relies on the agent's ability to decode it at runtime. There is no normal authoring reason to embed a multi-hundred-byte base64 blob in skill docs.
*.sig, SIGNATURES) outside the documentation.Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Build home screen widgets, Lock Screen widgets, Control Center controls, and StandBy or CarPlay widget surfaces for iOS 26+.
Keep adjacent-framework guidance scoped to WidgetKit integration. Include ActivityKit and App Intents only where they connect directly to WidgetKit surfaces; hand off full lifecycle, APNs content-state, Siri/Shortcuts/Spotlight, or entity-modeling work to sibling activitykit or app-intents skills.
See references/widgetkit-advanced.md for timeline strategies, push-based updates, Xcode setup, and advanced patterns.
TimelineEntry struct with a date property and display data.TimelineProvider (static) or AppIntentTimelineProvider (configurable).WidgetFamily.Widget conforming struct with a configuration and supported families.WidgetBundle annotated with @main.ActivityConfiguration in the widget bundle when the app has aLive Activity, but keep ActivityAttributes, request/update/end, APNs content-state, and Dynamic Island layout depth in activitykit.
Button, Toggle, ControlWidgetButton, and ControlWidgetTogglein WidgetKit views or controls, but keep intent modeling, entities, queries, Siri, Shortcuts, and Spotlight in app-intents.
AppIntent/OpenIntent for a button, or a SetValueIntent for a toggle.ControlWidgetButton or ControlWidgetToggle in the widget bundle.StaticControlConfiguration or AppIntentControlConfiguration.Run through the Review Checklist at the end of this document.
Every widget conforms to the Widget protocol and returns a WidgetConfiguration from its body.
struct OrderStatusWidget: Widget {
let kind: String = "OrderStatusWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: OrderProvider()) { entry in
OrderWidgetView(entry: entry)
}
.configurationDisplayName("Order Status")
.description("Track your current order.")
.supportedFamilies([.systemSmall, .systemMedium])
}
}Use WidgetBundle to expose multiple widgets from a single extension.
@main
struct MyAppWidgets: WidgetBundle {
var body: some Widget {
OrderStatusWidget()
FavoritesWidget()
DeliveryActivityWidget() // ActivityConfiguration handoff
QuickActionControl() // Control Center
}
}Use StaticConfiguration for non-configurable widgets. Use AppIntentConfiguration (recommended) for configurable widgets paired with AppIntentTimelineProvider.
// Static
StaticConfiguration(kind: "MyWidget", provider: MyProvider()) { entry in
MyWidgetView(entry: entry)
}
// Configurable
AppIntentConfiguration(kind: "ConfigWidget", intent: SelectCategoryIntent.self,
provider: CategoryProvider()) { entry in
CategoryWidgetView(entry: entry)
}| Modifier | Purpose |
|---|---|
.configurationDisplayName(_:) | Name shown in the widget gallery |
.description(_:) | Description shown in the widget gallery |
.supportedFamilies(_:) | Array of WidgetFamily values |
.supplementalActivityFamilies(_:) | Live Activity sizes (.small, .medium) |
For static (non-configurable) widgets. Uses completion handlers. Three required methods:
struct WeatherProvider: TimelineProvider {
typealias Entry = WeatherEntry
func placeholder(in context: Context) -> WeatherEntry {
WeatherEntry(date: .now, temperature: 72, condition: "Sunny")
}
func getSnapshot(in context: Context, completion: @escaping (WeatherEntry) -> Void) {
let entry = context.isPreview
? placeholder(in: context)
: WeatherEntry(date: .now, temperature: currentTemp, condition: currentCondition)
completion(entry)
}
func getTimeline(in context: Context, completion: @escaping (Timeline<WeatherEntry>) -> Void) {
Task {
let weather = await WeatherService.shared.fetch()
let entry = WeatherEntry(date: .now, temperature: weather.temp, condition: weather.condition)
let nextUpdate = Calendar.current.date(byAdding: .hour, value: 1, to: .now)!
completion(Timeline(entries: [entry], policy: .after(nextUpdate)))
}
}
}For configurable widgets. Uses async/await natively. Receives user intent configuration.
struct CategoryProvider: AppIntentTimelineProvider {
typealias Entry = CategoryEntry
typealias Intent = SelectCategoryIntent
func placeholder(in context: Context) -> CategoryEntry {
CategoryEntry(date: .now, categoryName: "Sample", items: [])
}
func snapshot(for config: SelectCategoryIntent, in context: Context) async -> CategoryEntry {
let items = await DataStore.shared.items(for: config.category)
return CategoryEntry(date: .now, categoryName: config.category.name, items: items)
}
func timeline(for config: SelectCategoryIntent, in context: Context) async -> Timeline<CategoryEntry> {
let items = await DataStore.shared.items(for: config.category)
let entry = CategoryEntry(date: .now, categoryName: config.category.name, items: items)
return Timeline(entries: [entry], policy: .atEnd)
}
}| Family | Platform |
|---|---|
.systemSmall | iOS, iPadOS, macOS, CarPlay (iOS 26+) |
.systemMedium | iOS, iPadOS, macOS |
.systemLarge | iOS, iPadOS, macOS |
.systemExtraLarge | iPadOS only |
.accessoryCircular | iOS, watchOS |
.accessoryRectangular | iOS, watchOS |
.accessoryInline | iOS, watchOS |
.accessoryCorner | watchOS only |
Adapt layout per family using @Environment(\.widgetFamily):
@Environment(\.widgetFamily) var family
var body: some View {
switch family {
case .systemSmall: CompactView(entry: entry)
case .systemMedium: DetailedView(entry: entry)
case .accessoryCircular: CircularView(entry: entry)
default: FullView(entry: entry)
}
}Use Button and Toggle with intent types available to the widget extension or shared code. WidgetKit owns the view placement; app-intents owns intent modeling and behavior.
struct InteractiveWidgetView: View {
let entry: FavoriteEntry
var body: some View {
Button(intent: ToggleFavoriteIntent(itemID: entry.itemID)) {
Image(systemName: entry.isFavorite ? "star.fill" : "star")
}
}
}WidgetKit registers Live Activity surfaces in the widget extension. Keep this section to registration and rendering handoff; use activitykit for ActivityAttributes, lifecycle, push updates, and full Dynamic Island patterns.
struct DeliveryActivityWidget: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(for: DeliveryAttributes.self) { context in
DeliveryLiveActivityView(context: context)
} dynamicIsland: { context in
DeliveryDynamicIsland(context: context)
}
}
}WidgetKit owns control configuration, placement, kind, display name, push handler, and extension registration. Control actions and value intents belong in app-intents.
struct OpenCameraControl: ControlWidget {
var body: some ControlWidgetConfiguration {
StaticControlConfiguration(kind: "OpenCamera") {
ControlWidgetButton(action: OpenCameraIntent()) {
Label("Camera", systemImage: "camera.fill")
}
}
.displayName("Open Camera")
}
}
struct FlashlightControl: ControlWidget {
var body: some ControlWidgetConfiguration {
StaticControlConfiguration(kind: "Flashlight", provider: FlashlightValueProvider()) { value in
ControlWidgetToggle(isOn: value, action: ToggleFlashlightIntent()) {
Label("Flashlight", systemImage: value ? "flashlight.on.fill" : "flashlight.off.fill")
}
}
.displayName("Flashlight")
}
}Use accessory families and AccessoryWidgetBackground.
struct StepsWidget: Widget {
let kind = "StepsWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: StepsProvider()) { entry in
ZStack {
AccessoryWidgetBackground()
VStack {
Image(systemName: "figure.walk")
Text("\(entry.stepCount)").font(.headline)
}
}
}
.supportedFamilies([.accessoryCircular, .accessoryRectangular, .accessoryInline])
}
}Small system widgets can appear in StandBy and CarPlay. Use @Environment(\.widgetLocation) for conditional rendering:
@Environment(\.widgetLocation) var location
// location == .standBy, .homeScreen, .lockScreen, .carPlay, etc.Use one .widgetURL(_:) as the whole-widget fallback route. Use Link for deliberate subtargets only where the family and layout support them, including .accessoryRectangular, .systemSmall, and larger system widgets. For small widgets, prefer one clear fallback; avoid multiple Link targets unless the visual affordance and hit areas remain unambiguous.
Never attach multiple widgetURL modifiers in the hierarchy.
Use TimelineEntryRelevance(score:duration:) on timeline entries for timely iPhone and iPad Smart Stack relevance. Keep scores on a consistent positive scale; zero or lower means not relevant.
For configurable widgets, donate App Intents that correspond to user actions or widget parameters from app-side code, such as with intent.donate() or IntentDonationManager. Keep AppEntity and EntityQuery design in app-intents.
On watchOS, contextual relevance uses WidgetRelevance([WidgetRelevanceAttribute(...)]) from the provider relevance() callback. That path is not used by iPhone or iPad Smart Stacks.
.gaugeStyle(.accessoryCircular) forLock Screen circular widgets and .linearCapacity for home screen capacity bars. The system handles styling, accessibility, and rendering-mode adaptation.
instead of padding and background modifiers.
The lack of per-element accessibility is acceptable since the entire widget surface is a single tap target.
40–70 refreshes per day with entries at least 5 minutes apart. Use Text(timerInterval:countsDown:) for live countdowns instead of burning timeline entries.
See references/widgetkit-advanced.md for code examples and detailed guidance on each pattern.
Adapt widgets to Liquid Glass with @Environment(\.widgetRenderingMode), .widgetAccentable(), and Image.widgetAccentedRenderingMode(_:). In .vibrant, the system maps content into the material style, so avoid relying on original colors alone.
Widget push reloads:
WidgetPushHandler type in the widget extension target or sharedcode linked into it, not only in the main app target.
.pushHandler(...) on the widget configuration.WidgetKit supplies tokens through pushTokenDidChange(_:widgets:).
apns-push-type: widgets, topic suffix .push-type.widgets, andaps.content-changed.
not the only freshness model. Timelines, reload policies, shared storage or refetch, and app-triggered WidgetCenter reloads remain the fallback path.
Control push reloads:
ControlPushHandler with .pushHandler(...) on theControlWidgetConfiguration.
pushTokensDidChange(controls:) receives [ControlInfo]; read tokens fromeach control's pushInfo.
apns-push-type: controls, topic suffix .push-type.controls, andaps.content-changed.
Small system widgets can appear in CarPlay on iOS 26+. Ensure layouts are legible at a glance; taps and controls depend on vehicle touch support and, for opening the app, CarPlay integration.
IntentTimelineProvider is the older SiriKit Intents-based provider. Prefer AppIntentTimelineProvider with the App Intents framework for new widgets.
call WidgetCenter.shared.reloadTimelines(ofKind:) on every minor data change. Batch updates and use appropriate TimelineReloadPolicy values.
separate process. Use UserDefaults(suiteName:) or a shared App Group container for data the widget reads.
placeholder(in:) must returnsynchronously with sample data. Use getTimeline or timeline(for:in:) for async work.
lifecycle in activitykit and full App Intent modeling in app-intents.
reload signals. Persist state in shared storage or refetch it in the provider.
come from WidgetKit handlers, not UNUserNotificationCenter.
size-limited process. Pre-compute data in the timeline provider and pass display-ready values through the entry.
.vibrant or .accented mode, not .fullColor. Test with @Environment(\.widgetRenderingMode) and avoid relying on color alone.
significantly from Simulator. Always verify on physical hardware.
@main is on the WidgetBundle, not on individual widgetsplaceholder(in:) returns synchronously; getSnapshot/snapshot(for:in:) fast when isPreviewreloadTimelines(ofKind:) only on data changeWidgetFamily; accessory widgets tested in .vibrant modeButton/Toggle only.widgetURL(_:) fallback is used; Link subtargets are family-appropriateStaticControlConfiguration/AppIntentControlConfiguration~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.