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
17
HabitTracker/Support/ModelContext+Habit.swift
Normal file
17
HabitTracker/Support/ModelContext+Habit.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
49
HabitTracker/Support/NotificationManager.swift
Normal file
49
HabitTracker/Support/NotificationManager.swift
Normal 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)"
|
||||
}
|
||||
}
|
||||
24
HabitTracker/Support/WidgetSync.swift
Normal file
24
HabitTracker/Support/WidgetSync.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue