Initiales HabitTracker-Projekt: SwiftUI + SwiftData Gewohnheiten-Tracker
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
This commit is contained in:
commit
22b8f5d806
24 changed files with 1448 additions and 0 deletions
84
HabitTracker/Models/Habit.swift
Normal file
84
HabitTracker/Models/Habit.swift
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
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
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue