1.1: Home-Screen-Widget + Siri-Kurzbefehl „Gassi gehen"

App Group group.app.banyaro.ios verbindet App und Widget-Extension
(Entitlements in beiden Targets, CODE_SIGN_ENTITLEMENTS fürs Widget).

Home-Screen-Widget (D):
- BanYaroHomeWidget (klein + mittel): Tagesfoto, Hundename, nächster Termin.
- App schreibt beim Heim-Laden einen Snapshot (HomeWidgetData) in die App
  Group und triggert WidgetCenter-Reload; Snapshot wird bei Logout/401 geleert.

Siri-/Kurzbefehl (E):
- StartWalkIntent „Gassi gehen" + AppShortcutsProvider (öffnet die App).
- WalkLauncher überbrückt Intent → UI: Flag in der App Group, beim Aktivwerden
  eingelöst → Aufnehmen-Tab + Aufnahme-Start (TrackingView.startFresh).
- MainTabView mit Tab-Auswahl (Tags), BanYaroGoApp liest scenePhase.
This commit is contained in:
rene 2026-06-02 20:01:16 +02:00
parent a2646a18ef
commit d807db57a2
14 changed files with 307 additions and 6 deletions

View file

@ -5,5 +5,6 @@ import SwiftUI
struct BanYaroGoWidgetBundle: WidgetBundle {
var body: some Widget {
WalkLiveActivity()
BanYaroHomeWidget()
}
}

View file

@ -0,0 +1,88 @@
import WidgetKit
import SwiftUI
import UIKit
struct BanYaroEntry: TimelineEntry {
let date: Date
let data: HomeWidgetData?
}
struct BanYaroProvider: TimelineProvider {
func placeholder(in context: Context) -> BanYaroEntry {
BanYaroEntry(date: Date(), data: nil)
}
func getSnapshot(in context: Context, completion: @escaping (BanYaroEntry) -> Void) {
completion(BanYaroEntry(date: Date(), data: HomeWidgetStore.load()))
}
func getTimeline(in context: Context, completion: @escaping (Timeline<BanYaroEntry>) -> Void) {
let entry = BanYaroEntry(date: Date(), data: HomeWidgetStore.load())
// Die App pusht bei Updates reloadAllTimelines(); als Sicherheitsnetz
// stündlich neu laden.
let next = Calendar.current.date(byAdding: .hour, value: 1, to: Date())
?? Date().addingTimeInterval(3600)
completion(Timeline(entries: [entry], policy: .after(next)))
}
}
struct BanYaroHomeWidgetEntryView: View {
@Environment(\.widgetFamily) private var family
let entry: BanYaroEntry
var body: some View {
VStack(alignment: .leading, spacing: 4) {
Spacer()
Text(entry.data?.dogName ?? "Ban Yaro")
.font(family == .systemSmall ? .headline : .title3)
.bold()
.foregroundStyle(.white)
.shadow(radius: 3)
if family != .systemSmall {
if let appt = entry.data?.nextAppointment, !appt.isEmpty {
Label(appt, systemImage: "calendar")
.font(.caption)
.foregroundStyle(.white)
.shadow(radius: 3)
} else if let n = entry.data?.diaryCount, n > 0 {
Label("\(n) Tagebuch-Einträge", systemImage: "book")
.font(.caption)
.foregroundStyle(.white.opacity(0.9))
.shadow(radius: 3)
} else {
Text("Schön, dass du da bist 🐾")
.font(.caption)
.foregroundStyle(.white.opacity(0.9))
.shadow(radius: 3)
}
}
}
.frame(maxWidth: .infinity, alignment: .leading)
}
}
struct BanYaroHomeWidget: Widget {
let kind = "BanYaroHomeWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: BanYaroProvider()) { entry in
BanYaroHomeWidgetEntryView(entry: entry)
.containerBackground(for: .widget) {
if let jpeg = entry.data?.photoJPEG, let ui = UIImage(data: jpeg) {
ZStack {
Image(uiImage: ui).resizable().scaledToFill()
LinearGradient(
colors: [.black.opacity(0.0), .black.opacity(0.55)],
startPoint: .center, endPoint: .bottom
)
}
} else {
Color.accentColor.opacity(0.25)
}
}
}
.configurationDisplayName("Ban Yaro")
.description("Tagesfoto deines Hundes und nächster Termin.")
.supportedFamilies([.systemSmall, .systemMedium])
}
}

View file

@ -0,0 +1,22 @@
import Foundation
/// Kopie im Widget-Target (identisch zur App-Variante). Über die App Group
/// `group.app.banyaro.ios` liest das Widget den von der App geschriebenen Stand.
struct HomeWidgetData: Codable {
var dogName: String
var photoJPEG: Data?
var nextAppointment: String?
var diaryCount: Int?
var updatedAt: Date
}
enum HomeWidgetStore {
static let appGroup = "group.app.banyaro.ios"
static let key = "homeWidgetData"
static func load() -> HomeWidgetData? {
guard let defaults = UserDefaults(suiteName: appGroup),
let data = defaults.data(forKey: key) else { return nil }
return try? JSONDecoder().decode(HomeWidgetData.self, from: data)
}
}