Agent skills for iOS, iPadOS, Swift, SwiftUI, and modern Apple framework development.
71
89%
Does it follow best practices?
Impact
—
No eval scenarios have been run
Advisory
Suggest reviewing before use
Extended patterns and reference material for RelevanceKit widget relevance
on watchOS 26+. See the main SKILL.md for an overview and quick-start
guidance.
A full watchOS-only widget using RelevanceConfiguration and
RelevanceEntriesProvider. This widget shows upcoming meetings and only
appears in the Smart Stack when a meeting is approaching.
import WidgetKit
import SwiftUI
import RelevanceKit
import AppIntents
// MARK: - Entry
struct MeetingRelevanceEntry: RelevanceEntry {
let title: String
let time: Date
let location: String?
let isPlaceholder: Bool
static let placeholder = MeetingRelevanceEntry(
title: "Meeting",
time: .now,
location: nil,
isPlaceholder: true
)
static let preview = MeetingRelevanceEntry(
title: "Design Review",
time: .now.addingTimeInterval(1800),
location: "Room 3",
isPlaceholder: false
)
}
// MARK: - Configuration Intent
struct MeetingWidgetIntent: WidgetConfigurationIntent {
static var title: LocalizedStringResource = "Meeting"
static var description: IntentDescription = "Shows an upcoming meeting."
@Parameter(title: "Meeting ID")
var meetingID: String
init() {}
init(meetingID: String) {
self.meetingID = meetingID
}
}
// MARK: - Provider
struct MeetingRelevanceProvider: RelevanceEntriesProvider {
typealias Configuration = MeetingWidgetIntent
typealias Entry = MeetingRelevanceEntry
func relevance() async -> WidgetRelevance<MeetingWidgetIntent> {
let meetings = await MeetingStore.shared.upcomingMeetings()
let attributes = meetings.map { meeting in
// Show the widget 15 minutes before through the meeting end
let context = RelevantContext.date(
from: meeting.startDate.addingTimeInterval(-900),
to: meeting.endDate
)
return WidgetRelevanceAttribute(
configuration: MeetingWidgetIntent(meetingID: meeting.id),
context: context
)
}
return WidgetRelevance(attributes)
}
func entry(
configuration: MeetingWidgetIntent,
context: Context
) async throws -> MeetingRelevanceEntry {
if context.isPreview {
return .preview
}
guard let meeting = await MeetingStore.shared
.meeting(id: configuration.meetingID)
else {
return .placeholder
}
return MeetingRelevanceEntry(
title: meeting.title,
time: meeting.startDate,
location: meeting.location,
isPlaceholder: false
)
}
func placeholder(context: Context) -> MeetingRelevanceEntry {
.placeholder
}
}
// MARK: - Widget
@available(watchOS 26, *)
struct MeetingRelevantWidget: Widget {
let kind = "com.example.meeting-relevant"
var body: some WidgetConfiguration {
RelevanceConfiguration(
kind: kind,
provider: MeetingRelevanceProvider()
) { entry in
MeetingWidgetView(entry: entry)
}
.configurationDisplayName("Meetings")
.description("Shows upcoming meetings when relevant")
.associatedKind("com.example.meeting-timeline")
}
}
// MARK: - View
struct MeetingWidgetView: View {
let entry: MeetingRelevanceEntry
var body: some View {
VStack(alignment: .leading) {
if entry.isPlaceholder {
Text("Meeting")
.redacted(reason: .placeholder)
} else {
Text(entry.title)
.font(.headline)
Text(entry.time, style: .time)
.font(.caption)
if let location = entry.location {
Text(location)
.font(.caption2)
.foregroundStyle(.secondary)
}
}
}
}
}Cross-platform timeline provider that adds watchOS relevance via relevance()
and RelevantIntentManager.
import WidgetKit
import SwiftUI
import RelevanceKit
import AppIntents
struct TaskEntry: TimelineEntry {
let date: Date
let task: TaskItem
}
struct TaskWidgetIntent: WidgetConfigurationIntent {
static var title: LocalizedStringResource = "Task"
static var description: IntentDescription = "Shows a task."
@Parameter(title: "Task ID")
var taskID: String
init() {}
init(taskID: String) {
self.taskID = taskID
}
}
struct TaskTimelineProvider: AppIntentTimelineProvider {
typealias Entry = TaskEntry
typealias Intent = TaskWidgetIntent
func placeholder(in context: Context) -> TaskEntry {
TaskEntry(date: .now, task: .placeholder)
}
func snapshot(for configuration: TaskWidgetIntent,
in context: Context) async -> TaskEntry {
let task = await TaskStore.shared.task(id: configuration.taskID)
?? .placeholder
return TaskEntry(date: .now, task: task)
}
func timeline(for configuration: TaskWidgetIntent,
in context: Context) async -> Timeline<TaskEntry> {
let task = await TaskStore.shared.task(id: configuration.taskID)
?? .placeholder
let entry = TaskEntry(date: .now, task: task)
// Update relevant intents alongside timeline refresh
await updateTaskRelevantIntents()
return Timeline(entries: [entry], policy: .after(
Date.now.addingTimeInterval(3600)
))
}
func recommendations() -> [AppIntentRecommendation<TaskWidgetIntent>] {
[] // Configurable widget
}
// watchOS relevance -- provides contextual clues
func relevance() async -> WidgetRelevance<TaskWidgetIntent> {
let tasks = await TaskStore.shared.dueSoonTasks()
let attributes = tasks.map { task in
WidgetRelevanceAttribute(
configuration: TaskWidgetIntent(taskID: task.id),
context: RelevantContext.date(task.dueDate, kind: .scheduled)
)
}
return WidgetRelevance(attributes)
}
private func updateTaskRelevantIntents() async {
let tasks = await TaskStore.shared.dueSoonTasks()
let intents = tasks.map { task in
RelevantIntent(
TaskWidgetIntent(taskID: task.id),
widgetKind: "com.example.task-widget",
relevance: RelevantContext.date(task.dueDate, kind: .scheduled)
)
}
try? await RelevantIntentManager.shared
.updateRelevantIntents(intents)
}
}RelevanceEntry conforms to Sendable. Keep entries lightweight -- they carry
only the data needed to render the view.
struct SimpleRelevanceEntry: RelevanceEntry {
let value: String
}struct WeatherRelevanceEntry: RelevanceEntry {
let temperature: String
let condition: String
let isPlaceholder: Bool
static let placeholder = WeatherRelevanceEntry(
temperature: "--",
condition: "Clear",
isPlaceholder: true
)
}struct FlightRelevanceEntry: RelevanceEntry {
let flightNumber: String?
let departureTime: Date?
let gate: String?
var isLoading: Bool { flightNumber == nil }
static let loading = FlightRelevanceEntry(
flightNumber: nil,
departureTime: nil,
gate: nil
)
}Location, fitness, and sleep clues require permission in both the app target and the widget extension target.
Add NSWidgetWantsLocation to the widget extension's Info.plist:
<key>NSWidgetWantsLocation</key>
<true/>Also add the appropriate location usage descriptions:
<key>NSLocationWhenInUseUsageDescription</key>
<string>Shows relevant widgets based on your location.</string>Both the app and the widget extension must request HealthKit authorization.
Add com.apple.developer.healthkit entitlement to both targets.
import HealthKit
func requestSleepPermission() async {
let store = HKHealthStore()
let sleepType = HKCategoryType(.sleepAnalysis)
try? await store.requestAuthorization(toShare: [], read: [sleepType])
}Always handle the case where permissions are denied. The widget should still function -- it just won't appear via relevance clues that require the missing permission.
func relevance() async -> WidgetRelevance<MyIntent> {
var attributes: [WidgetRelevanceAttribute<MyIntent>] = []
// Time-based clues always work (no permission needed)
for event in events {
attributes.append(
WidgetRelevanceAttribute(
configuration: MyIntent(eventID: event.id),
context: .date(event.date, kind: .scheduled)
)
)
}
// Location clues require permission -- add if available
if CLLocationManager().authorizationStatus == .authorizedWhenInUse {
attributes.append(
WidgetRelevanceAttribute(
configuration: MyIntent(mode: .nearby),
context: .location(inferred: .work)
)
)
}
return WidgetRelevance(attributes)
}// At a specific date
RelevantContext.date(someDate)
// At a date with a kind hint
RelevantContext.date(someDate, kind: .scheduled)
RelevantContext.date(someDate, kind: .informational)
RelevantContext.date(someDate, kind: .default)
// Between two dates
RelevantContext.date(from: startDate, to: endDate)
// Using DateInterval
RelevantContext.date(interval: dateInterval, kind: .scheduled)
// Using ClosedRange<Date>
RelevantContext.date(range: startDate...endDate, kind: .default)// Inferred locations (from the person's routine)
RelevantContext.location(inferred: .home)
RelevantContext.location(inferred: .work)
RelevantContext.location(inferred: .school)
RelevantContext.location(inferred: .commute)
// Specific geographic region
RelevantContext.location(CLCircularRegion(...))
// Point-of-interest category (returns optional)
RelevantContext.location(category: .cafe) // MKPointOfInterestCategory
RelevantContext.location(category: .airport)
RelevantContext.location(category: .beach)RelevantContext.fitness(.activityRingsIncomplete)
RelevantContext.fitness(.workoutActive)RelevantContext.sleep(.bedtime)
RelevantContext.sleep(.wakeup)RelevantContext.hardware(headphones: .connected)WidgetRelevanceGroup controls how the system deduplicates and organizes
widgets from the same app in the Smart Stack.
By default, the system groups widgets per-app. This means only one widget from the app may appear at a time.
WidgetRelevanceAttribute(
configuration: intent,
group: .automatic
)Opt out of default grouping. Each widget card can appear independently, useful when multiple cards should be visible simultaneously.
WidgetRelevanceAttribute(
configuration: intent,
group: .ungrouped
)Place related widgets in a named group. Only one widget from each group appears at a time, but widgets in different groups can coexist.
// These two share a group -- only one shows
WidgetRelevanceAttribute(
configuration: weatherCurrent,
group: .named("weather")
)
WidgetRelevanceAttribute(
configuration: weatherForecast,
group: .named("weather")
)
// This one is in a different group -- can show alongside weather
WidgetRelevanceAttribute(
configuration: calendarIntent,
group: .named("calendar")
)When an app offers both a timeline-based widget and a relevance-based widget
showing overlapping information, use .associatedKind(_:) to prevent
duplicate cards.
@available(watchOS 26, *)
struct EventRelevantWidget: Widget {
var body: some WidgetConfiguration {
RelevanceConfiguration(
kind: "com.example.event-relevant",
provider: EventRelevanceProvider()
) { entry in
EventView(entry: entry)
}
// When relevant cards are suggested, they replace the timeline widget
.associatedKind("com.example.event-timeline")
}
}
struct EventTimelineWidget: Widget {
var body: some WidgetConfiguration {
AppIntentConfiguration(
kind: "com.example.event-timeline",
provider: EventTimelineProvider()
) { entry in
EventView(entry: entry)
}
}
}When the system has relevant cards to show, it replaces the pinned timeline widget card with the relevant widget cards. When no relevant cards match, the timeline widget remains visible.
Useful during view development to test layout across display sizes.
#Preview("Meeting Cards", widget: MeetingRelevantWidget.self,
relevanceEntries: {
[
MeetingRelevanceEntry(
title: "Standup",
time: .now.addingTimeInterval(600),
location: "Zoom",
isPlaceholder: false
),
MeetingRelevanceEntry(
title: "Long Planning Session Title",
time: .now.addingTimeInterval(3600),
location: "Conference Room B",
isPlaceholder: false
),
]
})Useful for testing that the provider creates correct entries from configurations.
#Preview("Relevance", widget: MeetingRelevantWidget.self, relevance: {
WidgetRelevance([
WidgetRelevanceAttribute(
configuration: MeetingWidgetIntent(meetingID: "standup"),
context: .date(Date(), kind: .scheduled)
),
WidgetRelevanceAttribute(
configuration: MeetingWidgetIntent(meetingID: "planning"),
context: .date(
Date().addingTimeInterval(3600),
kind: .scheduled
)
),
])
})End-to-end preview using the actual provider. Supply test data through a preview-specific data source.
#Preview("Full Provider", widget: MeetingRelevantWidget.self,
relevanceProvider: MeetingRelevanceProvider())A single widget can be relevant under diverse conditions. Mix clue types to cover different scenarios.
func relevance() async -> WidgetRelevance<PodcastIntent> {
var attributes: [WidgetRelevanceAttribute<PodcastIntent>] = []
// Show during commute (likely listening time)
attributes.append(
WidgetRelevanceAttribute(
configuration: PodcastIntent(mode: .recentEpisode),
context: .location(inferred: .commute)
)
)
// Show when headphones are connected
attributes.append(
WidgetRelevanceAttribute(
configuration: PodcastIntent(mode: .nowPlaying),
context: .hardware(headphones: .connected)
)
)
// Show during workout (many people listen while exercising)
attributes.append(
WidgetRelevanceAttribute(
configuration: PodcastIntent(mode: .workout),
context: .fitness(.workoutActive)
)
)
// Show around bedtime (sleep podcast)
if hasSleepPodcast {
attributes.append(
WidgetRelevanceAttribute(
configuration: PodcastIntent(mode: .sleep),
context: .sleep(.bedtime)
)
)
}
return WidgetRelevance(attributes)
}RelevantContext.location(category:) accepts MKPointOfInterestCategory
values. The method returns RelevantContext? -- not all categories are
supported by the system. Always handle the nil case.
import MapKit
func locationRelevanceForCategory(
_ category: MKPointOfInterestCategory,
intent: MyIntent
) -> WidgetRelevanceAttribute<MyIntent>? {
guard let context = RelevantContext.location(category: category) else {
return nil
}
return WidgetRelevanceAttribute(configuration: intent, context: context)
}
// Usage
let categories: [MKPointOfInterestCategory] = [.cafe, .restaurant, .grocery]
let attributes = categories.compactMap { category in
locationRelevanceForCategory(category, intent: FoodIntent())
}When using the timeline provider path, relevance data can become stale
between timeline refreshes. Update RelevantIntentManager whenever
underlying data changes.
// In the main app, after data changes
func onEventsUpdated(_ events: [Event]) async {
let intents = events.map { event in
RelevantIntent(
EventWidgetIntent(eventID: event.id),
widgetKind: "com.example.events",
relevance: RelevantContext.date(
from: event.start,
to: event.end
)
)
}
try? await RelevantIntentManager.shared.updateRelevantIntents(intents)
// Also reload the timeline
WidgetCenter.shared.reloadTimelines(ofKind: "com.example.events")
}Enable Developer Mode. On the Apple Watch, go to Settings > Developer and enable WidgetKit Developer Mode. This bypasses rotation limits so relevant widgets appear immediately.
Use Xcode Previews. The three preview variants (entries, relevance, provider) let you verify appearance without deploying to a device.
Verify permissions. Test with location/health permissions both granted and denied. Widgets should degrade gracefully, not crash.
Test with context.isPreview. The preview branch in entry() is
called when the widget appears in system settings and the widget gallery.
Return representative data that helps the user understand what the widget
shows.
Check placeholder. The placeholder entry appears while the widget loads
data. Use .redacted(reason: .placeholder) or a loading indicator.
Inspect on device. The Simulator does not fully replicate Smart Stack behavior. Test on a real Apple Watch for accurate relevance triggering.
Audit for duplicates. If both a timeline widget and a relevant widget
exist for the same data, verify that .associatedKind(_:) is set and
working correctly.
skills
accessorysetupkit
references
activitykit
references
adattributionkit
references
alarmkit
references
app-clips
app-intents
references
app-store-optimization
app-store-review
apple-on-device-ai
appmigrationkit
references
audioaccessorykit
references
authentication
references
avkit
references
background-processing
references
browserenginekit
references
callkit
references
carplay
references
cloudkit
references
contacts-framework
references
core-bluetooth
references
core-data
core-motion
references
core-nfc
references
coreml
references
cryptokit
references
cryptotokenkit
references
debugging-instruments
device-integrity
references
dockkit
references
energykit
references
eventkit
references
financekit
references
focus-engine
gamekit
references
healthkit
references
homekit
references
ios-accessibility
ios-localization
ios-networking
ios-simulator
references
mapkit
metrickit
references
musickit
references
natural-language
references
paperkit
references
passkit
references
pdfkit
references
pencilkit
references
permissionkit
references
photokit
push-notifications
realitykit
references
relevancekit
references
scenekit
references
sensorkit
references
speech-recognition
spritekit
references
storekit
swift-api-design-guidelines
swift-architecture
swift-charts
references
swift-codable
swift-concurrency
swift-formatstyle
swift-language
swift-security
references
swift-testing
swiftdata
swiftlint
swiftui-animation
swiftui-gestures
references
swiftui-layout-components
swiftui-liquid-glass
references
swiftui-patterns
swiftui-performance
swiftui-uikit-interop
swiftui-webkit
tabletopkit
references
tipkit
references
vision-framework
weatherkit
references
widgetkit
references