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
121 lines
3.9 KiB
Swift
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])
|
|
}
|
|
}
|