| name | generators-usage-insights |
| description | Generates user-facing usage statistics, activity summaries, and personalized insights dashboards (weekly recaps, year-in-review, Spotify Wrapped-style). Use when user wants to show usage stats, activity insights, or shareable recap screens. Different from analytics-setup which sends data to a backend — this shows insights to the USER on-device. |
First step: Tell the user: "generators-usage-insights skill loaded."
Usage Insights Generator
Generate a production usage insights system that records user activity events with SwiftData, computes personalized insights (streaks, most active day, top categories), and displays them in a dashboard with insight cards, period pickers, trend indicators, and optional shareable recap screens.
When This Skill Activates
Use this skill when the user:
- Asks to "show usage statistics" or "add usage stats"
- Wants "user insights" or "activity insights"
- Mentions "activity summary" or "weekly summary"
- Asks about a "usage dashboard" or "insights dashboard"
- Wants a "weekly recap" or "monthly recap"
- Mentions "year in review" or "year-in-review"
- Asks for "Spotify Wrapped style" or "Wrapped-style recap"
- Wants to "show the user their activity" or "personalized stats"
Pre-Generation Checks
1. Project Context Detection
2. Conflict Detection
Search for existing usage tracking or insights code:
Glob: **/*UsageEvent*.swift, **/*Insight*.swift, **/*Recap*.swift, **/*ActivityLog*.swift
Grep: "UsageEvent" or "InsightCalculator" or "activitySummary" or "SwiftData" and "event"
If existing analytics/tracking found:
- Ask if user wants to build insights on top of existing event data
- If yes, adapt
InsightCalculator to work with existing models
- If no, generate fresh event recording alongside existing code
3. Data Layer Detection
Check for SwiftData usage:
Grep: "import SwiftData" or "@Model" or "ModelContainer"
If SwiftData already in use:
- Integrate
UsageEvent into existing ModelContainer
- Use existing schema migration strategy
If no SwiftData:
- Generate full setup including
ModelContainer configuration
Configuration Questions
Ask user via AskUserQuestion:
-
Insight period?
- Daily (today's activity breakdown)
- Weekly (7-day recap with day-by-day comparison) — recommended
- Monthly (30-day trends with weekly rollups)
- Yearly (year-in-review with monthly highlights)
- All of the above (period picker lets user switch)
-
Visualization style?
- Cards only (simple stat cards with trend indicators)
- Charts only (Swift Charts bar/line graphs)
- Both cards and charts — recommended
-
Include shareable recap card?
- Yes (generates a recap view that can be rendered to an image and shared)
- No (dashboard only, no sharing)
-
Data source?
- SwiftData (generate
UsageEvent model and recorder) — recommended
- Custom (user provides their own event data; generate calculator and views only)
Generation Process
Step 1: Read Templates
Read templates.md for production Swift code.
Step 2: Create Core Data Files
Generate these files:
UsageEvent.swift — SwiftData @Model for recording user activity events
InsightResult.swift — Model for computed insights (title, value, trend, visualization type)
Step 3: Create Calculation Engine
InsightCalculator.swift — Pure functions that aggregate events into insights
Step 4: Create UI Files
InsightsDashboardView.swift — Main dashboard with grid of insight cards and period picker
InsightCardView.swift — Individual insight card with icon, value, trend indicator, sparkline
Step 5: Create Optional Files
Based on configuration:
UsageRecapView.swift — If shareable recap selected (paged summary with share card generation)
UsageEventRecorder.swift — If SwiftData data source selected (convenience class for recording events)
Step 6: Determine File Location
Check project structure:
- If
Sources/ exists -> Sources/UsageInsights/
- If
App/ exists -> App/UsageInsights/
- Otherwise ->
UsageInsights/
Output Format
After generation, provide:
Files Created
UsageInsights/
├── UsageEvent.swift # SwiftData @Model for activity events
├── InsightResult.swift # Computed insight model
├── InsightCalculator.swift # Aggregation engine
├── InsightsDashboardView.swift # Dashboard with period picker
├── InsightCardView.swift # Individual insight card
├── UsageRecapView.swift # Shareable recap (optional)
└── UsageEventRecorder.swift # Event recording helper (optional)
Integration Steps
Add ModelContainer (if not already present):
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(for: [UsageEvent.self])
}
}
Record events from anywhere in the app:
struct TaskDetailView: View {
@Environment(\.modelContext) private var modelContext
@State private var recorder: UsageEventRecorder?
var body: some View {
Button("Complete Task") {
completeTask()
recorder?.record(
.taskCompleted,
metadata: ["category": "work", "priority": "high"]
)
}
.onAppear {
recorder = UsageEventRecorder(modelContext: modelContext)
}
}
}
Show the insights dashboard:
NavigationLink("My Insights") {
InsightsDashboardView()
}
Show a weekly recap:
struct WeeklyRecapSheet: View {
@Environment(\.modelContext) private var modelContext
@State private var showRecap = false
var body: some View {
Button("View Weekly Recap") {
showRecap = true
}
.sheet(isPresented: $showRecap) {
UsageRecapView(period: .week)
}
}
}
Record events with duration tracking:
let sessionStart = Date()
recorder?.record(
.sessionCompleted,
metadata: ["screen": "editor"],
duration: Date().timeIntervalSince(sessionStart)
)
Testing
@Test
func calculatesWeeklySummaryCorrectly() async throws {
let calendar = Calendar.current
let now = Date()
let events: [UsageEvent] = (0..<7).flatMap { dayOffset in
let date = calendar.date(byAdding: .day, value: -dayOffset, to: now)!
return (0..<(dayOffset == 2 ? 5 : 2)).map { _ in
UsageEvent(eventType: "taskCompleted", timestamp: date)
}
}
let calculator = InsightCalculator()
let insights = calculator.weeklySummary(from: events, referenceDate: now)
let totalInsight = insights.first { $0.title == "Total Events" }
#expect(totalInsight != nil)
#expect(totalInsight?.value == "19")
}
@Test
func () {
calendar .current
now ()
events: [] []
dayOffset {
date calendar.date(byAdding: .day, value: dayOffset, to: now)
count calendar.component(.weekday, from: date) :
count {
events.append((eventType: , timestamp: date))
}
}
calculator ()
insights calculator.weeklySummary(from: events, referenceDate: now)
mostActive insights.first { .title }
#expect(mostActive.value )
}
() {
calculator ()
insights calculator.weeklySummary(from: [], referenceDate: ())
#expect(insights.isEmpty)
totalInsight insights.first { .title }
#expect(totalInsight.value )
}
() {
config (isStoredInMemoryOnly: )
container (for: ., configurations: config)
context (container)
recorder (modelContext: context)
i {
recorder.record(.custom())
}
recorder.flush()
descriptor <>()
count context.fetchCount(descriptor)
#expect(count )
}
Common Patterns
Record an Event
recorder?.record(.featureUsed, metadata: ["feature": "darkMode"])
Calculate a Weekly Summary
let calculator = InsightCalculator()
let events = try modelContext.fetch(FetchDescriptor<UsageEvent>())
let insights = calculator.weeklySummary(from: events, referenceDate: Date())
Render Insight Cards in a Grid
LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: 16) {
ForEach(insights) { insight in
InsightCardView(insight: insight)
}
}
Generate and Share a Recap Image
let renderer = ImageRenderer(content: UsageRecapView(period: .week))
renderer.scale = 2.0
if let image = renderer.uiImage {
}
Gotchas
Privacy: All Data Stays On-Device
- Usage events are stored in SwiftData on the user's device only
- Never transmit usage data to a server (that's what
analytics-setup is for)
- Clearly communicate to users that insights are computed locally
- Consider adding a "Delete My Data" option in settings
Performance with Large Datasets
- Fetch only the events needed for the current period (use
#Predicate with date range)
- For yearly summaries, pre-aggregate monthly totals instead of scanning all events
- Batch event recording to avoid excessive SwiftData writes (use
UsageEventRecorder)
- Set a retention policy — auto-delete events older than 1-2 years
Calendar-Based Period Calculations
- Always use
Calendar.current for date math — never assume 7 days = 1 week
- Respect the user's first-day-of-week setting (
calendar.firstWeekday)
- Use
calendar.dateInterval(of: .weekOfYear, for: date) for accurate week boundaries
- Handle timezone changes gracefully — store timestamps in UTC, display in local time
Swift Charts Considerations
- Swift Charts is iOS 16+ / macOS 13+, but works best with iOS 17+ for latest features
- Keep chart data points reasonable (7 for weekly, 30 for monthly, 12 for yearly)
- Use
.chartYScale(domain:) to prevent axis from starting at a misleading value
- Provide VoiceOver descriptions with
.accessibilityLabel on chart marks
Trend Comparison Edge Cases
- First week of use has no "previous period" — show "New!" instead of a trend arrow
- Handle periods with zero events gracefully (don't divide by zero for averages)
- Percentage changes can be misleading for small numbers (1 -> 2 = +100%)
References
- templates.md — All production Swift templates
- Related:
generators-share-card — Render recap views as shareable images
- Related:
generators-analytics-setup — Backend analytics (complements on-device insights)
- Related:
generators-streak-tracker — Streak tracking pairs well with usage insights
- Related:
generators-milestone-celebration — Celebrate milestones surfaced by insights