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.
npx skills add https://github.com/rshankras/claude-code-apple-skills --skill streak-tracker
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.
Use this skill when the user:
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:
Determine if generating for iOS (UNUserNotificationCenter) or macOS (UNUserNotificationCenter available on macOS 11+) or both.
Ask user via AskUserQuestion:
Read templates.md for production Swift code.
Generate these files:
StreakRecord.swift — SwiftData @Model for activity recordsStreakManager.swift — @Observable class: record activity, calculate streaks, manage freezesStreakError.swift — Error types for streak operationsStreakCalendarView.swift — Grid showing days with/without activityStreakBadgeView.swift — Compact badge with streak count and animationBased on configuration:
StreakFreeze.swift — If streak freeze selectedStreakNotificationScheduler.swift — If notifications selectedCheck project structure:
Sources/ exists -> Sources/StreakTracking/App/ exists -> App/StreakTracking/StreakTracking/After generation, provide:
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)
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() {
// Freeze applied — streak preserved
} else {
// No freezes available
}
}
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 {
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
// Record 2 days ago and today — gap of 1 day
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) // 2 days ago + freeze + today
}
// Record a single check-in per day (idempotent)
try await streakManager.recordActivity(type: "daily-check-in")
// StreakManager deduplicates — calling twice on the same day is safe
let current = streakManager.currentStreak // Consecutive days up to today
let longest = streakManager.longestStreak // All-time best
let atRisk = streakManager.isStreakAtRisk // No activity today yet
let hasActivity = streakManager.hasActivityToday
// Grant freeze passes (e.g., as a reward or purchase)
streakManager.addStreakFreeze(count: 2)
// Use a freeze to cover a missed day
let success = streakManager.useStreakFreeze()
// Returns false if no freezes remaining or no gap to fill
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.
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.
Users can change their device clock to fake streak activity. Mitigations:
createdAt timestamps alongside activityDate and flag anomalies (e.g., createdAt is before a previously recorded createdAt)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.
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.
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.
monetization/external-purchases); everywhere else the rule stands.app-store/originality-check before submitting.generators/push-notifications — Push notification setup for streak remindersgenerators/milestone-celebration — Celebrate streak milestones with animationsIntegration with protocols.io API for managing scientific protocols. This skill should be used when working with protocols.io to search, create, update, or publish protocols; manage protocol steps and materials; handle discussions and comments; organize workspaces; upload and manage files; or integrate protocols.io functionality into workflows. Applicable for protocol discovery, collaborative protocol development, experiment tracking, lab protocol management, and scientific documentation.
Analyzes job descriptions and generates tailored resumes that highlight relevant experience, skills, and achievements to maximize interview chances
Generate Excalidraw diagrams from natural language descriptions. Use when asked to "create a diagram", "make a flowchart", "visualize a process", "draw a system architecture", "create a mind map", or "generate an Excalidraw file". Supports flowcharts, relationship diagrams, mind maps, and system architecture diagrams. Outputs .excalidraw JSON files that can be opened directly in Excalidraw.
Build and distribute Expo development clients locally or via TestFlight
Use when you have a written implementation plan to execute in a separate session with review checkpoints
Data structure for annotated matrices in single-cell analysis. Use when working with .h5ad files or integrating with the scverse ecosystem. This is the data format skill—for analysis workflows use scanpy; for probabilistic models use scvi-tools; for population-scale queries use cellxgene-census.
Benchling R&D platform integration. Access registry (DNA, proteins), inventory, ELN entries, workflows via API, build Benchling Apps, query Data Warehouse, for lab data management automation.
Comprehensive molecular biology toolkit. Use for sequence manipulation, file parsing (FASTA/GenBank/PDB), phylogenetics, and programmatic NCBI/PubMed access (Bio.Entrez). Best for batch processing, custom bioinformatics pipelines, BLAST automation. For quick lookups use gget; for multi-service integration use bioservices.
Take rshankras/streak-tracker from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.