ios-tracker/HabitTrackerWidget/HabitTrackerWidget.swift
rene 22b8f5d806 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
2026-05-29 21:12:45 +02:00

121 lines
3.9 KiB
Swift

import WidgetKit
import SwiftUI
struct HabitTimelineEntry: TimelineEntry {
let date: Date
let snapshot: WidgetSnapshot
}
struct Provider: TimelineProvider {
func placeholder(in context: Context) -> HabitTimelineEntry {
HabitTimelineEntry(date: .now, snapshot: .empty)
}
func getSnapshot(in context: Context, completion: @escaping (HabitTimelineEntry) -> Void) {
completion(HabitTimelineEntry(date: .now, snapshot: SharedStore.load()))
}
func getTimeline(in context: Context, completion: @escaping (Timeline<HabitTimelineEntry>) -> Void) {
let entry = HabitTimelineEntry(date: .now, snapshot: SharedStore.load())
// Reset "today" at the next midnight.
let nextMidnight = Calendar.current.startOfDay(for: .now.addingTimeInterval(86_400))
completion(Timeline(entries: [entry], policy: .after(nextMidnight)))
}
}
struct HabitTrackerWidgetEntryView: View {
var entry: Provider.Entry
@Environment(\.widgetFamily) private var family
private var items: [WidgetSnapshot.Item] { entry.snapshot.items }
private var doneCount: Int { items.filter(\.isDoneToday).count }
var body: some View {
if items.isEmpty {
emptyState
} else {
switch family {
case .systemSmall:
smallView
default:
mediumView
}
}
}
private var emptyState: some View {
VStack(spacing: 6) {
Image(systemName: "checklist")
.font(.title)
.foregroundStyle(.secondary)
Text("Keine Gewohnheiten")
.font(.caption)
.foregroundStyle(.secondary)
}
}
private var header: some View {
HStack {
Text("Heute")
.font(.caption.bold())
.foregroundStyle(.secondary)
Spacer()
Text("\(doneCount)/\(items.count)")
.font(.caption.bold())
.foregroundStyle(.secondary)
}
}
private var smallView: some View {
VStack(alignment: .leading, spacing: 8) {
header
Spacer(minLength: 0)
Text("\(doneCount)")
.font(.system(size: 44, weight: .bold))
+ Text(" / \(items.count)")
.font(.title3.weight(.semibold))
.foregroundColor(.secondary)
Text(doneCount == items.count ? "Alles erledigt 🎉" : "erledigt")
.font(.caption)
.foregroundStyle(.secondary)
}
}
private var mediumView: some View {
VStack(alignment: .leading, spacing: 8) {
header
ForEach(items.prefix(3)) { item in
HStack(spacing: 10) {
Image(systemName: item.symbolName)
.foregroundStyle(Color(hex: item.colorHex))
.frame(width: 22)
Text(item.name)
.font(.subheadline)
.lineLimit(1)
Spacer()
Image(systemName: item.isDoneToday ? "checkmark.circle.fill" : "circle")
.foregroundStyle(item.isDoneToday ? Color(hex: item.colorHex) : .secondary)
}
}
if items.count > 3 {
Text("+ \(items.count - 3) weitere")
.font(.caption2)
.foregroundStyle(.secondary)
}
}
}
}
struct HabitTrackerWidget: Widget {
let kind = "HabitTrackerWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: Provider()) { entry in
HabitTrackerWidgetEntryView(entry: entry)
.containerBackground(.fill.tertiary, for: .widget)
}
.configurationDisplayName("Gewohnheiten")
.description("Dein Fortschritt von heute.")
.supportedFamilies([.systemSmall, .systemMedium])
}
}