Phase 4.A.1: Live Activity + Dynamic Island für laufende Gassi-Tour

Neues Widget-Extension-Target BanYaroGoWidgetExtension:
- Bundle-ID app.banyaro.ios.BanYaroGoWidget
- NSExtensionPointIdentifier = com.apple.widgetkit-extension
- Synced root group + explizite Info.plist + Embed-Phase in App-Target
- Cross-Membership der Shared/WalkActivityAttributes.swift in beiden Targets

Shared/WalkActivityAttributes.swift:
- ActivityAttributes mit startedAt (fix)
- ContentState mit distanceMeters, elapsedSeconds, pointCount, isPaused, isAutoPaused

BanYaroGoWidget/WalkLiveActivity.swift:
- Lock-Screen-View: Pfote-Icon + Status-Pille (Live/Pause/Auto-Pause) +
  Stats-Spalten (Distanz/Dauer/Punkte)
- Dynamic Island compact: Pfote leading, Distanz trailing
- Dynamic Island minimal: nur Pfote
- Dynamic Island expanded: Distanz/Dauer/Status mit Pfote zentriert

WalkActivityController:
- @MainActor Facade um ActivityKit
- start() prüft areActivitiesEnabled, killt orphaned current, request mit Initial-State
- update() async via Task
- end() mit dismissalPolicy.immediate

TrackingView:
- .onChange(of: tracker.isTracking) → Start/End
- persistTicker (5s) → update
- .onChange(of: tracker.isPaused/isAutoPaused) → sofort update für saubere UX

BanYaroGo-Info.plist: NSSupportsLiveActivities = true
This commit is contained in:
rene 2026-05-30 11:35:43 +02:00
parent fec7c79b05
commit 500a645bfd
8 changed files with 458 additions and 0 deletions

View file

@ -34,7 +34,17 @@ struct TrackingView: View {
.onReceive(persistTicker) { _ in
tracker.checkAutoPause()
persistActive()
updateLiveActivity()
}
.onChange(of: tracker.isTracking) { _, isTracking in
if isTracking {
startLiveActivity()
} else {
WalkActivityController.end()
}
}
.onChange(of: tracker.isPaused) { _, _ in updateLiveActivity() }
.onChange(of: tracker.isAutoPaused) { _, _ in updateLiveActivity() }
.onAppear { offerResumeIfNeeded() }
.sheet(isPresented: $showFinishSheet) {
FinishWalkSheet(
@ -320,6 +330,29 @@ struct TrackingView: View {
// MARK: - Helpers
private func startLiveActivity() {
guard let startedAt = tracker.startedAt else { return }
WalkActivityController.start(
startedAt: startedAt,
initialState: liveActivityState()
)
}
private func updateLiveActivity() {
guard tracker.isTracking else { return }
WalkActivityController.update(liveActivityState())
}
private func liveActivityState() -> WalkActivityAttributes.WalkActivityState {
WalkActivityAttributes.WalkActivityState(
distanceMeters: tracker.totalDistanceMeters,
elapsedSeconds: tracker.effectiveElapsedSeconds,
pointCount: tracker.points.count,
isPaused: tracker.isPaused,
isAutoPaused: tracker.isAutoPaused
)
}
private func formatDuration(_ seconds: Int) -> String {
let h = seconds / 3600
let m = (seconds % 3600) / 60