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.
85 lines
3 KiB
Swift
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)")
|
|
}
|
|
}
|
|
}
|