Claude
Skills
Sign in
Back

streak-tracker

Included with Lifetime
$97 forever

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.

Data & Analytics

What this skill does


# 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
- [ ] Check Swift version (requires Swift 5.9+)
- [ ] Check deployment target (iOS 17+ / macOS 14+ for @Observable and SwiftData)
- [ ] Check for SwiftData availability and existing model container setup
- [ ] Identify source file locations

### 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:

1. **Streak type?**
   - Daily (consecutive calendar days) -- recommended
   - Weekly (at least one activity per calendar week)
   - Custom interval (every N hours/days)

2. **Storage backend?**
   - SwiftData (recommended for iOS 17+ / macOS 14+)
   - UserDefaults (lightweight, no model container needed)

3. **Include streak freeze/protection?**
   - Yes — users get limited freeze passes to preserve streaks on missed days
   - No — strict consecutive tracking only

4. **Streak-at-risk notifications?**
   - Yes — schedule a local notification (e.g., 8 PM) if no activity recorded today
   - No — no notifications

5. **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:
1. `StreakRecord.swift` — SwiftData @Model for activity records
2. `StreakManager.swift` — @Observable class: record activity, calculate streaks, manage freezes
3. `StreakError.swift` — Error types for streak operations

### Step 3: Create UI Files
4. `StreakCalendarView.swift` — Grid showing days with/without activity
5. `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):**
```swift
@main
struct MyApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .modelContainer(for: [StreakRecord.self, StreakFreeze.self])
    }
}
```

**Record activity on user action:**
```swift
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:**
```swift
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:**
```swift
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:**
```swift
Button("Use Streak Freeze") {
    if streakManager.useStreakFreeze() {
        // Freeze applied — streak preserved
    } else {
        // No freezes available
    }
}
```

### Testing

```swift
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)

    // Record activity for today
    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)

    // Simulate 3 consecutive days
    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
    // Day 3 ago and Day 2 ago (streak of 2), then skip Day 1, record today
    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) // Gap broke the streak
    #expect(manager.longestStreak == 2) // Previous streak preserved
}

@Test
func streakFreezePreservesStreak() async throws {
  

Related in Data & Analytics