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:
rene 2026-05-29 21:12:45 +02:00
commit 22b8f5d806
24 changed files with 1448 additions and 0 deletions

View file

@ -0,0 +1,17 @@
import Foundation
import SwiftData
extension ModelContext {
/// Toggles completion for a habit on a given day: removes the entry if it
/// exists, otherwise inserts one normalized to the start of that day.
func toggleCompletion(for habit: Habit, on day: Date) {
let calendar = Calendar.current
if let entry = habit.entries.first(where: { calendar.isDate($0.date, inSameDayAs: day) }) {
delete(entry)
} else {
let entry = HabitEntry(date: calendar.startOfDay(for: day), habit: habit)
insert(entry)
}
WidgetSync.refresh(self)
}
}

View file

@ -0,0 +1,49 @@
import Foundation
import UserNotifications
@MainActor
enum NotificationManager {
/// Default reminder time (09:00) used when the user first enables a reminder.
static var defaultTime: Date {
Calendar.current.date(bySettingHour: 9, minute: 0, second: 0, of: .now) ?? .now
}
@discardableResult
static func requestAuthorization() async -> Bool {
do {
return try await UNUserNotificationCenter.current()
.requestAuthorization(options: [.alert, .sound, .badge])
} catch {
return false
}
}
/// Cancels any existing reminder for the habit and, if it has a reminder
/// time, schedules a new daily notification.
static func reschedule(for habit: Habit) {
let center = UNUserNotificationCenter.current()
let id = identifier(for: habit)
center.removePendingNotificationRequests(withIdentifiers: [id])
guard let time = habit.reminderTime else { return }
let content = UNMutableNotificationContent()
content.title = habit.name
content.body = "Zeit für deine Gewohnheit"
content.sound = .default
let components = Calendar.current.dateComponents([.hour, .minute], from: time)
let trigger = UNCalendarNotificationTrigger(dateMatching: components, repeats: true)
let request = UNNotificationRequest(identifier: id, content: content, trigger: trigger)
center.add(request)
}
static func cancel(for habit: Habit) {
UNUserNotificationCenter.current()
.removePendingNotificationRequests(withIdentifiers: [identifier(for: habit)])
}
private static func identifier(for habit: Habit) -> String {
"habit-\(habit.uuid.uuidString)"
}
}

View file

@ -0,0 +1,24 @@
import Foundation
import SwiftData
import WidgetKit
enum WidgetSync {
/// Writes the current habits to the shared store and reloads widget timelines.
static func refresh(_ context: ModelContext) {
let descriptor = FetchDescriptor<Habit>(sortBy: [SortDescriptor(\.createdAt)])
let habits = (try? context.fetch(descriptor)) ?? []
let items = habits.map { habit in
WidgetSnapshot.Item(
id: habit.uuid,
name: habit.name,
symbolName: habit.symbolName,
colorHex: habit.colorHex,
isDoneToday: habit.isCompletedToday
)
}
SharedStore.save(WidgetSnapshot(generatedAt: .now, items: items))
WidgetCenter.shared.reloadAllTimelines()
}
}