| name | streak-tracker |
| description | Generates a streak tracking system with timezone-aware day boundaries, streak freeze protection, and streak-at-risk push notifications. Use when user wants daily/weekly engagement streaks, consecutive day tracking, or habit tracking. |
| allowed-tools | ["Read","Write","Edit","Glob","Grep","Bash","AskUserQuestion"] |
Streak Tracker Generator
Generate a production streak tracking system that records consecutive days of user activity, calculates current and longest streaks, handles timezone-aware day boundaries, supports streak freeze/protection passes, and schedules streak-at-risk local notifications.
When This Skill Activates
Use this skill when the user:
- Asks to "add streaks" or "daily streak" tracking
- Wants "streak tracking" or "consecutive days" counting
- Mentions "engagement streaks" or "habit tracking"
- Asks about "streak freeze" or "streak protection"
- Wants "streak-at-risk" notifications or reminders
- Mentions "login streak" or "activity streak"
Pre-Generation Checks
1. Project Context Detection
2. Conflict Detection
Search for existing streak or habit tracking:
Glob: **/*Streak*.swift, **/*Habit*.swift, **/*DailyTrack*.swift
Grep: "streak" or "consecutiveDays" or "habitTrack" or "dailyStreak"
If existing streak/habit system found:
- Ask if user wants to replace or extend it
- If extending, generate only the missing components
3. Platform Detection
Determine if generating for iOS (UNUserNotificationCenter) or macOS (UNUserNotificationCenter available on macOS 11+) or both.
Configuration Questions
Ask user via AskUserQuestion:
-
Streak type?
- Daily (consecutive calendar days) -- recommended
- Weekly (at least one activity per calendar week)
- Custom interval (every N hours/days)
-
Storage backend?
- SwiftData (recommended for iOS 17+ / macOS 14+)
- UserDefaults (lightweight, no model container needed)
-
Include streak freeze/protection?
- Yes — users get limited freeze passes to preserve streaks on missed days
- No — strict consecutive tracking only
-
Streak-at-risk notifications?
- Yes — schedule a local notification (e.g., 8 PM) if no activity recorded today
- No — no notifications
-
Additional features? (multi-select)
- Calendar heat map view (visual grid of activity days)
- Streak badge view (compact count with animation)
- Milestone celebrations (7-day, 30-day, 100-day, etc.)
Generation Process
Step 1: Read Templates
Read templates.md for production Swift code.
Step 2: Create Core Files
Generate these files:
StreakRecord.swift — SwiftData @Model for activity records
StreakManager.swift — @Observable class: record activity, calculate streaks, manage freezes
StreakError.swift — Error types for streak operations
Step 3: Create UI Files
StreakCalendarView.swift — Grid showing days with/without activity
StreakBadgeView.swift — Compact badge with streak count and animation
Step 4: Create Optional Files
Based on configuration:
StreakFreeze.swift — If streak freeze selected
StreakNotificationScheduler.swift — If notifications selected
Step 5: Determine File Location
Check project structure:
- If
Sources/ exists -> Sources/StreakTracking/
- If
App/ exists -> App/StreakTracking/
- Otherwise ->
StreakTracking/
Output Format
After generation, provide:
Files Created
StreakTracking/
├── StreakRecord.swift # SwiftData model for activity records
├── StreakManager.swift # Core streak calculation engine
├── StreakError.swift # Error types
├── StreakCalendarView.swift # Calendar heat map view
├── StreakBadgeView.swift # Compact animated badge
├── StreakFreeze.swift # Freeze/protection passes (optional)
└── StreakNotificationScheduler.swift # Streak-at-risk reminders (optional)
Integration Steps
Set up the model container (SwiftData):
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(for: [StreakRecord.self, StreakFreeze.self])
}
}
Record activity on user action:
struct LessonCompleteView: View {
@Environment(\.modelContext) private var modelContext
@State private var streakManager: StreakManager?
var body: some View {
Button("Complete Lesson") {
Task {
try await streakManager?.recordActivity(type: "lesson")
}
}
.onAppear {
streakManager = StreakManager(modelContext: modelContext)
}
}
}
Display the current streak:
struct ProfileView: View {
@Environment(\.modelContext) private var modelContext
@State private var streakManager: StreakManager?
var body: some View {
VStack {
if let manager = streakManager {
StreakBadgeView(streak: manager.currentStreak)
Text("Longest: \(manager.longestStreak) days")
.foregroundStyle(.secondary)
}
}
.onAppear {
streakManager = StreakManager(modelContext: modelContext)
Task { await streakManager?.refresh() }
}
}
}
Show the calendar heat map:
struct StatsView: View {
@Environment(\.modelContext) private var modelContext
@State private var streakManager: StreakManager?
var body: some View {
if let manager = streakManager {
StreakCalendarView(manager: manager)
}
}
}
Use a streak freeze:
Button("Use Streak Freeze") {
if streakManager.useStreakFreeze() {
} else {
}
}
Testing
import Testing
import SwiftData
@Test
func recordingActivityIncrementsStreak() async throws {
let container = try ModelContainer(
for: StreakRecord.self, StreakFreeze.self,
configurations: ModelConfiguration(isStoredInMemoryOnly: true)
)
let context = ModelContext(container)
let manager = StreakManager(modelContext: context)
try await manager.recordActivity(type: "workout")
await manager.refresh()
#expect(manager.currentStreak == 1)
}
@Test
func consecutiveDaysBuildStreak() async throws {
let container = try ModelContainer(
for: StreakRecord.self, StreakFreeze.self,
configurations: ModelConfiguration(isStoredInMemoryOnly: true)
)
let context = ModelContext(container)
let manager = StreakManager(modelContext: context)
let calendar = Calendar.current
for daysAgo in (0...2).reversed() {
let date = calendar.date(byAdding: .day, value: -daysAgo, to: .now)!
try await manager.recordActivity(type: "workout", date: date)
}
await manager.refresh()
#expect(manager.currentStreak == 3)
#expect(manager.longestStreak == 3)
}
@Test
func missedDayBreaksStreak() async throws {
let container = try ModelContainer(
for: StreakRecord.self, StreakFreeze.self,
configurations: ModelConfiguration(isStoredInMemoryOnly: true)
)
let context = ModelContext(container)
let manager = StreakManager(modelContext: context)
let calendar = Calendar.current
let threeDaysAgo = calendar.date(byAdding: .day, value: -3, to: .now)!
let twoDaysAgo = calendar.date(byAdding: .day, value: -2, to: .now)!
try await manager.recordActivity(type: "workout", date: threeDaysAgo)
try await manager.recordActivity(type: "workout", date: twoDaysAgo)
try await manager.recordActivity(type: "workout", date: .now)
await manager.refresh()
#expect(manager.currentStreak == 1)
#expect(manager.longestStreak == 2)
}
@Test
func streakFreezePreservesStreak() async throws {
let container = try ModelContainer(
for: StreakRecord.self, StreakFreeze.self,
configurations: ModelConfiguration(isStoredInMemoryOnly: true)
)
let context = ModelContext(container)
let manager = StreakManager(modelContext: context)
let calendar = Calendar.current
let twoDaysAgo = calendar.date(byAdding: .day, value: -2, to: .now)!
try await manager.recordActivity(type: "workout", date: twoDaysAgo)
manager.addStreakFreeze(count: 1)
let used = manager.useStreakFreeze()
try await manager.recordActivity(type: "workout", date: .now)
await manager.refresh()
#expect(used == true)
#expect(manager.currentStreak == 3)
}
Common Patterns
Check In for Today
try await streakManager.recordActivity(type: "daily-check-in")
Query Streak State
let current = streakManager.currentStreak
let longest = streakManager.longestStreak
let atRisk = streakManager.isStreakAtRisk
let hasActivity = streakManager.hasActivityToday
Streak Freeze
streakManager.addStreakFreeze(count: 2)
let success = streakManager.useStreakFreeze()
Gotchas & Edge Cases
Timezone and Day Boundary Handling
Always use Calendar.current.startOfDay(for:) to normalize dates. Never compare raw Date values for "same day" checks — a user active at 11:59 PM and 12:01 AM has activity on two different calendar days but should not get a gap. The StreakManager uses the user's local calendar for all day boundary calculations.
Midnight Edge Cases
If a user completes an activity exactly at midnight, startOfDay(for:) may place it on the new day. The templates handle this by recording the timestamp as well as the normalized date, so the raw data is preserved for debugging.
Device Clock Manipulation
Users can change their device clock to fake streak activity. Mitigations:
- Store
createdAt timestamps alongside activityDate and flag anomalies (e.g., createdAt is before a previously recorded createdAt)
- For server-synced apps, validate streaks server-side
- For local-only apps, accept that determined users can game it — focus on honest users
Calendar vs Gregorian Day Changes
Different calendars (Islamic, Hebrew, Japanese) have different day boundaries and week structures. The templates use Calendar.current which respects the user's locale. If your app requires a fixed calendar (e.g., Gregorian for global leaderboards), pass an explicit Calendar(identifier: .gregorian) to StreakManager.
Duplicate Activity on Same Day
StreakManager.recordActivity is idempotent per calendar day per activity type. Calling it multiple times on the same day creates only one StreakRecord. This prevents accidental double-counting.
App Reinstall / Data Loss
SwiftData stores persist across app updates but are lost on reinstall. For critical streak data, consider syncing to CloudKit or a backend. The templates include a lastSyncDate hook in StreakManager for this purpose.
App Store Guidelines
- Guideline 4.5.4: Streak-reminder pushes ("don't lose your 12-day streak!") are marketing notifications — they require explicit opt-in consent in your UI plus an in-app opt-out, not just the system permission prompt.
- Guideline 3.1.1: If streak freezes or repairs are sold, they are digital content and must go through IAP — never an external purchase link.
- Guideline 4.3 (Spam): A streak mechanic doesn't differentiate a thin app. If this is one of several similar apps on your account, run
app-store/originality-check before submitting.
References
- templates.md — All production Swift templates for streak tracking
- Related:
generators/push-notifications — Push notification setup for streak reminders
- Related:
generators/milestone-celebration — Celebrate streak milestones with animations