Last active
January 9, 2025 09:42
-
-
Save Codelaby/ec26d47a547bdc0e11831f0ed986bd4e to your computer and use it in GitHub Desktop.
Streak Manager Swift
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* | |
| Third Library based -> https://github.com/lukerobertsapps/LRStreakKit | |
| */ | |
| import Foundation | |
| /// Data for a streak, codable so it can be persisted | |
| struct Streak: Codable { | |
| /// The length of the streak in days | |
| var length: Int = 0 | |
| /// When the streak was last updated | |
| var lastDate: Date? | |
| /// Determines an outcome for a given date and whether a given date is valid to continue the streak | |
| /// - Parameter date: The date to test against | |
| /// - Returns: An outcome such as nextDay, sameDay or invalid | |
| func determineOutcome(for date: Date) -> Outcome { | |
| guard let lastDate else { return .streakContinues } | |
| let calendar = Calendar.current | |
| if calendar.isDate(date, inSameDayAs: lastDate) { | |
| return .alreadyCompletedToday | |
| } | |
| if let dayAfterLast = calendar.date(byAdding: .day, value: 1, to: lastDate) { | |
| if calendar.isDate(date, inSameDayAs: dayAfterLast) { | |
| return .streakContinues | |
| } | |
| } | |
| return .streakBroken | |
| } | |
| /// Represents different outcomes for a streak | |
| enum Outcome { | |
| /// The streak was already completed for the day | |
| case alreadyCompletedToday | |
| /// The streak should continue | |
| case streakContinues | |
| /// The streak has been broken | |
| case streakBroken | |
| } | |
| } | |
| actor StreakActor { | |
| private let key: String | |
| init(key: String) { | |
| self.key = key | |
| } | |
| public func updateStreak(onDate date: Date = .now) async { | |
| var streak = await loadStreak() | |
| let outcome = streak.determineOutcome(for: date) | |
| switch outcome { | |
| case .alreadyCompletedToday: | |
| break | |
| case .streakContinues: | |
| streak.length += 1 | |
| streak.lastDate = date | |
| case .streakBroken: | |
| streak.length = 1 | |
| streak.lastDate = date | |
| } | |
| await save(streak: streak) | |
| } | |
| public func getStreakLength(forDate date: Date = .now) async -> Int { | |
| let streak = await loadStreak() | |
| return streak.length | |
| } | |
| public func hasCompletedStreak(onDate date: Date = .now) async -> Bool { | |
| let streak = await loadStreak() | |
| let outcome = streak.determineOutcome(for: date) | |
| return outcome == .alreadyCompletedToday | |
| } | |
| public func resetStreak() async { | |
| await save(streak: Streak()) | |
| } | |
| private func loadStreak(decoder: JSONDecoder = .init()) async -> Streak { | |
| guard let data = UserDefaults.standard.data(forKey: key) else { | |
| return Streak() | |
| } | |
| do { | |
| let fetched = try decoder.decode(Streak.self, from: data) | |
| return fetched | |
| } catch { | |
| print("Failed to decode streak. Error: \(error.localizedDescription)") | |
| return Streak() | |
| } | |
| } | |
| private func save(streak: Streak, encoder: JSONEncoder = .init()) async { | |
| guard let encoded = try? encoder.encode(streak) else { | |
| print("Failed to encode current streak") | |
| return | |
| } | |
| UserDefaults.standard.set(encoded, forKey: key) | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import SwiftUI | |
| @MainActor | |
| final class StreakManager: ObservableObject { | |
| @Published var streakLength: Int = 0 | |
| @Published var hasCompletedToday: Bool = false | |
| private var streakActor: StreakActor | |
| init(key: String) { | |
| self.streakActor = StreakActor(key: key) | |
| loadStreakData() | |
| } | |
| func updateStreak() { | |
| Task { | |
| //await streakActor.updateStreak(onDate: Date().addingTimeInterval(-1 * 24 * 60 * 60)) // 1 day ago | |
| await streakActor.updateStreak() //today | |
| loadStreakData() | |
| } | |
| } | |
| func resetStreak() { | |
| Task { | |
| await streakActor.resetStreak() | |
| loadStreakData() | |
| } | |
| } | |
| private func loadStreakData() { | |
| Task { | |
| streakLength = await streakActor.getStreakLength() | |
| hasCompletedToday = await streakActor.hasCompletedStreak() | |
| } | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import SwiftUI | |
| extension View { | |
| func stroke(color: Color, width: CGFloat = 1) -> some View { | |
| modifier(StrokeModifier(strokeSize: width, strokeColor: color)) | |
| } | |
| } | |
| struct StrokeModifier: ViewModifier { | |
| private let id = UUID() | |
| var strokeSize: CGFloat = 1 | |
| var strokeColor: Color = .blue | |
| func body(content: Content) -> some View { | |
| if strokeSize > 0 { | |
| appliedStrokeBackground(content: content) | |
| } else { | |
| content | |
| } | |
| } | |
| private func appliedStrokeBackground(content: Content) -> some View { | |
| content | |
| .padding(strokeSize * 2) | |
| .background( | |
| Rectangle() | |
| .foregroundColor(strokeColor) | |
| .mask(alignment: .center) { | |
| mask(content: content) | |
| } | |
| ) | |
| } | |
| func mask(content: Content) -> some View { | |
| Canvas { context, size in | |
| context.addFilter(.alphaThreshold(min: 0.01)) | |
| if let resolvedView = context.resolveSymbol(id: id) { | |
| context.draw(resolvedView, at: .init(x: size.width/2, y: size.height/2)) | |
| } | |
| } symbols: { | |
| content | |
| .tag(id) | |
| .blur(radius: strokeSize) | |
| } | |
| } | |
| } | |
| struct StreakView: View { | |
| @StateObject private var streakManager = StreakManager(key: "DailyStreak") | |
| var body: some View { | |
| VStack { | |
| Spacer() | |
| Image(systemName: "flame.fill") | |
| .resizable() | |
| .aspectRatio(contentMode: .fit) | |
| .frame(width: 128) | |
| .foregroundStyle(.orange.gradient) | |
| //.symbolEffect(.bounce) | |
| .overlay(alignment: .bottom) { | |
| VStack(spacing: 0) { | |
| Text("\(streakManager.streakLength)") | |
| .font(.system(size: 72, weight: .heavy, design: .rounded)) | |
| .fixedSize() | |
| .foregroundStyle(.white) | |
| .stroke(color: .black, width: 2) | |
| Text("day streak!") | |
| } | |
| .alignmentGuide(.bottom) { dim in | |
| dim.height / 2 | |
| } | |
| } | |
| Spacer() | |
| if streakManager.hasCompletedToday { | |
| Text("Congratulations, you completed day!") | |
| .foregroundStyle(.green) | |
| } else { | |
| Text("Complete a lesson every day to build your streak") | |
| } | |
| Spacer() | |
| HStack { | |
| Button("reset") { | |
| streakManager.resetStreak() | |
| } | |
| Button("update streak") { | |
| streakManager.updateStreak() // for update streak | |
| } | |
| } | |
| } | |
| .padding() | |
| } | |
| } | |
| #Preview { | |
| StreakView() | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment