Skip to main content

Streaks

Track daily engagement with automatic streak counting, break detection, and longest-streak records.

Recording Activity

Record Streak Activity
// Record today's activity for the "daily" streak
try await manager.recordStreakActivity("daily")
  • First call creates the streak record
  • Same-day calls are no-ops (safe to call multiple times)
  • Consecutive-day calls extend the streak
  • Missed days reset the streak to 1 (longest is preserved)

Querying Streaks

Streak Queries
// Current streak count
let current = try await manager.currentStreak("daily")

// Longest streak ever
let longest = try await manager.longestStreak("daily")

// Full streak record
let record = try await manager.streakRecord("daily")
print(record.currentStreak)     // 7
print(record.longestStreak)     // 14
print(record.totalActiveDays)   // 42
print(record.lastActivityDate)  // 2026-05-05

PrismStreakSnapshot

PropertyTypeDescription
streakIDStringStreak identifier
currentStreakIntConsecutive days
longestStreakIntAll-time record
lastActivityDateDate?Last recorded day
totalActiveDaysIntLifetime active days

Resetting Streaks

Reset Streak
try await manager.resetStreak("daily")
// currentStreak → 0, longestStreak preserved

Streak Events

The manager emits events when streaks change:
Streak Events
for await event in manager.events {
    switch event {
    case .streakExtended(let id, let count):
        print("\(id) streak: \(count) days!")
    case .streakBroken(let id, let previous):
        print("\(id) streak broken at \(previous) days")
    case .newStreakRecord(let id, let longest):
        print("New record for \(id): \(longest) days!")
    default: break
    }
}

Badges

A tiered badge system with automatic unlock evaluation based on challenge completion, points, or streaks.

The PrismBadge Protocol

Protocol
public protocol PrismBadge: RawRepresentable, CaseIterable, Hashable, Sendable
where RawValue == String {
    var title: String { get }
    var badgeDescription: String { get }
    var iconName: String? { get }           // default: nil
    var tier: PrismBadgeTier { get }
    var condition: PrismBadgeCondition { get }
}

Badge Tiers

Five tiers from lowest to highest — Comparable for sorting:
Tiers
public enum PrismBadgeTier: String, Comparable {
    case bronze      // Entry level
    case silver      // Intermediate
    case gold        // Advanced
    case platinum    // Expert
    case diamond     // Elite
}

Unlock Conditions

Conditions
public enum PrismBadgeCondition {
    case challengeCompleted(challengeID: String)     // Specific challenge done
    case pointsReached(threshold: Int)               // Total points >= threshold
    case streakReached(streakID: String, days: Int)  // Streak >= days
    case custom(id: String)                          // Manual unlock only
}

Defining Badges

Badge Enum
enum AppBadge: String, PrismBadge, CaseIterable {
    case earlyAdopter
    case fitnessFreak
    case streakMaster
    case vip

    var title: String {
        switch self {
        case .earlyAdopter: "Early Adopter"
        case .fitnessFreak: "Fitness Freak"
        case .streakMaster: "Streak Master"
        case .vip: "VIP Member"
        }
    }

    var badgeDescription: String {
        switch self {
        case .earlyAdopter: "Complete your first challenge"
        case .fitnessFreak: "Earn 100 points"
        case .streakMaster: "Maintain a 30-day streak"
        case .vip: "Exclusive VIP badge"
        }
    }

    var tier: PrismBadgeTier {
        switch self {
        case .earlyAdopter: .bronze
        case .fitnessFreak: .silver
        case .streakMaster: .gold
        case .vip: .diamond
        }
    }

    var condition: PrismBadgeCondition {
        switch self {
        case .earlyAdopter: .challengeCompleted(challengeID: "firstLogin")
        case .fitnessFreak: .pointsReached(threshold: 100)
        case .streakMaster: .streakReached(streakID: "daily", days: 30)
        case .vip: .custom(id: "vip")
        }
    }
}

Registration

Register Badges
try await manager.registerBadges(AppBadge.self)

Auto-Evaluation

Evaluate all badges and auto-unlock those whose conditions are met:
Evaluate
let totalPoints = try await manager.totalPoints(AppChallenge.self)
let unlocked = try await manager.evaluateBadges(
    AppBadge.self,
    currentPoints: totalPoints
)

for badge in unlocked {
    print("Unlocked: \(badge.badgeID) (\(badge.tierRawValue))")
}
Call evaluateBadges after significant events — challenge completions, point changes, or streak milestones. Badges with .custom conditions are never auto-unlocked.

Manual Unlock

Manual Unlock
try await manager.unlockBadge(AppBadge.vip)

Querying Badges

Badge Queries
// Check if unlocked
let unlocked = try await manager.isBadgeUnlocked(AppBadge.earlyAdopter)

// Single badge progress
let badge = try await manager.badgeProgress(for: AppBadge.fitnessFreak)

// All badges
let all = try await manager.allBadges()
let locked = all.filter { !$0.isUnlocked }

PrismBadgeSnapshot

PropertyTypeDescription
badgeIDStringBadge identifier
isUnlockedBoolWhether unlocked
tierRawValueStringTier name (bronze, silver, etc.)
unlockedAtDate?When unlocked
createdAtDateWhen registered

Badge Events

Badge Events
for await event in manager.events {
    if case .badgeUnlocked(let id, let tier) = event {
        print("\(tier) badge \(id) unlocked!")
    }
}