Natives iOS-App-Gerüst (Xcode 26, synchronisierte Ordner, iOS 18+). Features: - Gewohnheiten anlegen (Name, SF-Symbol, Farbe), heute abhaken, Streaks, Löschen - Detailansicht mit Monatskalender (Tage nachtragbar) und Statistiken - Tägliche Erinnerungen via lokale Notifications - Home-Screen-Widget (klein/mittel) mit App-Group-Datenaustausch
84 lines
2.4 KiB
Swift
84 lines
2.4 KiB
Swift
import Foundation
|
|
import SwiftData
|
|
|
|
@Model
|
|
final class Habit {
|
|
var uuid: UUID = UUID()
|
|
var name: String
|
|
var symbolName: String
|
|
var colorHex: String
|
|
var createdAt: Date
|
|
/// Time of day for the daily reminder; nil means no reminder.
|
|
var reminderTime: Date?
|
|
|
|
@Relationship(deleteRule: .cascade, inverse: \HabitEntry.habit)
|
|
var entries: [HabitEntry] = []
|
|
|
|
init(
|
|
name: String,
|
|
symbolName: String = "star.fill",
|
|
colorHex: String = "#34C759",
|
|
createdAt: Date = .now,
|
|
reminderTime: Date? = nil
|
|
) {
|
|
self.name = name
|
|
self.symbolName = symbolName
|
|
self.colorHex = colorHex
|
|
self.createdAt = createdAt
|
|
self.reminderTime = reminderTime
|
|
}
|
|
}
|
|
|
|
extension Habit {
|
|
/// Whether this habit was checked off on the given day.
|
|
func isCompleted(on day: Date, calendar: Calendar = .current) -> Bool {
|
|
entries.contains { calendar.isDate($0.date, inSameDayAs: day) }
|
|
}
|
|
|
|
var isCompletedToday: Bool {
|
|
isCompleted(on: .now)
|
|
}
|
|
|
|
/// Number of consecutive days (counting back from today) the habit was done.
|
|
var currentStreak: Int {
|
|
let calendar = Calendar.current
|
|
let doneDays = Set(entries.map { calendar.startOfDay(for: $0.date) })
|
|
var day = calendar.startOfDay(for: .now)
|
|
|
|
// A streak stays alive if today isn't done yet but yesterday was.
|
|
if !doneDays.contains(day) {
|
|
day = calendar.date(byAdding: .day, value: -1, to: day)!
|
|
}
|
|
|
|
var streak = 0
|
|
while doneDays.contains(day) {
|
|
streak += 1
|
|
day = calendar.date(byAdding: .day, value: -1, to: day)!
|
|
}
|
|
return streak
|
|
}
|
|
|
|
/// Longest run of consecutive completed days, ever.
|
|
var longestStreak: Int {
|
|
let calendar = Calendar.current
|
|
let days = Set(entries.map { calendar.startOfDay(for: $0.date) }).sorted()
|
|
guard !days.isEmpty else { return 0 }
|
|
|
|
var longest = 1
|
|
var run = 1
|
|
for i in 1..<days.count {
|
|
let expectedPrev = calendar.date(byAdding: .day, value: -1, to: days[i])!
|
|
if calendar.isDate(expectedPrev, inSameDayAs: days[i - 1]) {
|
|
run += 1
|
|
} else {
|
|
run = 1
|
|
}
|
|
longest = max(longest, run)
|
|
}
|
|
return longest
|
|
}
|
|
|
|
var totalCompletions: Int {
|
|
entries.count
|
|
}
|
|
}
|