banyaro-ios/BanYaroGo/Support/WalkHealthSync.swift
rene c01e3d6be7 Phase 3.6: B+C+D komplett + HealthKit Sync
D.10 401-Handling: APIError.unauthorized, NotificationCenter-Bridge,
  AuthSession.logout() bei 401 → User landet wieder im Login

D.12 PWA-Deep-Links: Settings-Section mit Forum/Hunde/Walks/Settings
  öffnet Safari per https://banyaro.app/#fragment

B.4 Auto-Pause: 2-min-Inaktivität → isAutoPaused, automatischer Resume bei
  nächstem GPS-Update. Settings-Toggle, im UI eigenes Badge "Auto-Pause"
  (grau vs. Pause orange).

C.7 Edit/Delete: RouteUpdateBody + APIClient.patch + APIClient.delete,
  EditRouteSheet (Name/Beschreibung/Public), Menu in Toolbar (nur eigene
  Touren), Alert für Delete.

C.9 Statistik-Tab: neuer Tab "Statistik" zwischen Hunde und Mehr. Filtert
  /api/routes auf meine Touren, rechnet Woche/Monat/Allzeit (Distanz, Dauer,
  Touren), Längste Tour, aktuelle Streak (Tage in Folge).

B.5 Walk-Review: Map-Header an die Spitze des FinishWalkSheet-Forms.

B.6 Geo-Fotos: CapturedPhoto (Data + GPSPoint?), PhotoLocation @Model in
  SwiftData. Kamera während Walk taggt mit tracker.points.last. Nach Upload:
  foto_url aus Response → PhotoLocation persistiert. MiniRouteMap rendert
  Annotations mit Tap-Callback, PhotoViewerSheet zeigt Foto fullscreen.

C.8 Share PNG+GPX: RouteShareImage (MKMapSnapshotter + Polyline overlay +
  SwiftUI ShareCard via ImageRenderer), GPXExporter (Tempfile mit XML),
  ShareSheet (UIActivityViewController-Wrapper), Menu in Route-Toolbar.

D.11 Icon-Varianten: AppIcon-Dark (0.45 Brightness), AppIcon-Tinted
  (Grayscale + Kontrastverstärkung), Contents.json mit appearance entries.

A.2 HealthKit: BanYaroGo.entitlements (com.apple.developer.healthkit),
  NSHealthShare/UpdateUsageDescription. WalkHealthSync.shared mit
  HKWorkoutBuilder (.walking) + HKWorkoutRouteBuilder, Timestamps gleichmäßig
  über Walk-Dauer verteilt. Settings-Toggle mit Permission-Request.
2026-05-30 11:19:53 +02:00

85 lines
3 KiB
Swift

import Foundation
import HealthKit
import CoreLocation
/// Saves a completed walk as an HKWorkout(.walking) with HKWorkoutRoute so it
/// shows up in Apple Health and counts toward the user's Activity rings.
@MainActor
final class WalkHealthSync {
static let shared = WalkHealthSync()
private let store = HKHealthStore()
var isAvailable: Bool { HKHealthStore.isHealthDataAvailable() }
/// Requests share authorization for workouts + workout routes.
@discardableResult
func requestAuthorization() async -> Bool {
guard isAvailable else { return false }
let types: Set<HKSampleType> = [
HKObjectType.workoutType(),
HKSeriesType.workoutRoute()
]
do {
try await store.requestAuthorization(toShare: types, read: [])
return true
} catch {
print("WalkHealthSync auth failed: \(error)")
return false
}
}
func saveWalk(
points: [GPSPoint],
startedAt: Date,
endedAt: Date,
distanceMeters: Double
) async {
guard isAvailable, points.count >= 2 else { return }
let configuration = HKWorkoutConfiguration()
configuration.activityType = .walking
configuration.locationType = .outdoor
let builder = HKWorkoutBuilder(
healthStore: store,
configuration: configuration,
device: .local()
)
do {
try await builder.beginCollection(at: startedAt)
let distanceQuantity = HKQuantity(unit: .meter(), doubleValue: distanceMeters)
let distanceSample = HKQuantitySample(
type: HKQuantityType(.distanceWalkingRunning),
quantity: distanceQuantity,
start: startedAt,
end: endedAt
)
try await builder.addSamples([distanceSample])
try await builder.endCollection(at: endedAt)
guard let workout = try await builder.finishWorkout() else { return }
// Distribute timestamps evenly across the recorded period our
// GPSPoint doesn't carry per-point timing.
let totalSeconds = endedAt.timeIntervalSince(startedAt)
let interval = totalSeconds / Double(max(1, points.count - 1))
let locations = points.enumerated().map { i, p in
CLLocation(
coordinate: CLLocationCoordinate2D(latitude: p.lat, longitude: p.lon),
altitude: p.alt ?? 0,
horizontalAccuracy: 5,
verticalAccuracy: 5,
timestamp: startedAt.addingTimeInterval(Double(i) * interval)
)
}
let routeBuilder = HKWorkoutRouteBuilder(healthStore: store, device: .local())
try await routeBuilder.insertRouteData(locations)
_ = try await routeBuilder.finishRoute(with: workout, metadata: nil)
} catch {
print("WalkHealthSync save failed: \(error)")
}
}
}