WidgetSnapshot-Bridge App ↔ Widget via App-Group-UserDefaults (`group.ev.mana.moodlit`). MoodStore.refreshWidgetSnapshot läuft nach loadAll + toggleFavorite und pingt WidgetCenter. Widget-Extension (`ev.mana.moodlit.widget`, iOS-only app-extension): - Small: Last-Played oder erstes Favorit als Glow-Tile + Name + Animation-Slug - Medium: 2×2-Grid, bis zu 4 Favoriten, jede Kachel hat eigene Link-Destination zum App-Player - Large: 3×3-Grid, bis zu 9 Favoriten + Footer mit Total-Count - Empty-State, wenn keine Favoriten gesetzt sind Deep-Links: - `moodlit://play/<id>` (Custom-Scheme aus Widget-Tap): `url.host == "play"`, ID aus pathComponents - `https://moodlit.mana.how/play/<id>` (Universal-Link via AASA): pathComponents == ["/", "play", "<id>"] Beide öffnen MoodPlayerView als fullScreenCover direkt auf RootView (unabhängig vom aktiven Tab). Wegen Widget-Target-Sharing: `Mood.swiftUIColors` aus HexColor.swift nach Mood+SwiftUI.swift extrahiert (Widget kennt den Mood-Type nicht). xcodebuild iOS-Sim + macOS beide BUILD SUCCEEDED. Widget-Extension korrekt eingebettet in `MoodlitNative.app/PlugIns/`. 11 Unit-Tests weiter grün. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
64 lines
2.2 KiB
Swift
64 lines
2.2 KiB
Swift
import Foundation
|
|
|
|
/// Schreib-Lese-Bridge zwischen App und Widget-Extension via shared
|
|
/// App-Group-UserDefaults.
|
|
///
|
|
/// Die App schreibt nach jedem `MoodStore.loadAll`-Refresh und nach
|
|
/// `toggleFavorite`; das Widget liest in seinem Timeline-Provider.
|
|
///
|
|
/// Bewusst flach gehalten: kein SwiftData / kein Core Data — ein
|
|
/// Codable-Dokument in einem Key reicht für ~10 Favoriten + Last-
|
|
/// Played. Größenlimit App-Group-UserDefaults ist locker genug.
|
|
public struct WidgetSnapshot: Codable, Equatable, Sendable {
|
|
public let favorites: [WidgetMood]
|
|
public let lastPlayed: WidgetMood?
|
|
public let updatedAt: Date
|
|
|
|
public static let appGroup = "group.ev.mana.moodlit"
|
|
public static let storageKey = "moodlit.widgetSnapshot"
|
|
|
|
public init(favorites: [WidgetMood], lastPlayed: WidgetMood?, updatedAt: Date = Date()) {
|
|
self.favorites = favorites
|
|
self.lastPlayed = lastPlayed
|
|
self.updatedAt = updatedAt
|
|
}
|
|
}
|
|
|
|
/// Minimal-Repräsentation für Widgets. Hex-Strings statt SwiftUI
|
|
/// Color (Widget-Process kann keine `Color` aus App-Bundle-Resources
|
|
/// dereferenzieren) und nur der Animation-Slug.
|
|
public struct WidgetMood: Codable, Equatable, Identifiable, Sendable {
|
|
public let id: String
|
|
public let name: String
|
|
public let colors: [String]
|
|
public let animation: String
|
|
|
|
public init(id: String, name: String, colors: [String], animation: String) {
|
|
self.id = id
|
|
self.name = name
|
|
self.colors = colors
|
|
self.animation = animation
|
|
}
|
|
}
|
|
|
|
/// Lesen/Schreiben des Snapshots aus dem App-Group-Container.
|
|
/// Beide Seiten (App + Widget) konsumieren `WidgetSnapshot.appGroup`.
|
|
public enum WidgetSnapshotStore {
|
|
private static var defaults: UserDefaults? {
|
|
UserDefaults(suiteName: WidgetSnapshot.appGroup)
|
|
}
|
|
|
|
public static func read() -> WidgetSnapshot? {
|
|
guard let data = defaults?.data(forKey: WidgetSnapshot.storageKey) else { return nil }
|
|
return try? JSONDecoder().decode(WidgetSnapshot.self, from: data)
|
|
}
|
|
|
|
public static func write(_ snapshot: WidgetSnapshot) {
|
|
guard let data = try? JSONEncoder().encode(snapshot) else { return }
|
|
defaults?.set(data, forKey: WidgetSnapshot.storageKey)
|
|
}
|
|
|
|
public static func clear() {
|
|
defaults?.removeObject(forKey: WidgetSnapshot.storageKey)
|
|
}
|
|
}
|