banyaro-ios/BanYaroGo/Views/TrackingView.swift
rene 0b95e3e6d1 Phase 2: Live-GPS-Tracking + neues Icon
- BanYaroGo-Info.plist (explizit, statt INFOPLIST_KEY_*): UIBackgroundModes
  location, NSLocationWhenInUseUsageDescription,
  NSLocationAlwaysAndWhenInUseUsageDescription
- LocationTracker: CLLocationManager-Wrapper (@Observable @MainActor), Distanz
  via CLLocation.distance, Permission-Handling, Background-Updates
- RouteCreateBody + Encoder mit convertToSnakeCase für POST /api/routes
- TrackingView: Start-Hero-Screen + Live-Karte mit MapPolyline + Stats-Karte
- FinishWalkSheet: Name + Hunde-Multiselect + POST /api/routes
- MainTabView: neuer Aufnehmen-Tab zwischen Touren und Hunde
- AppIcon: neues Hund-mit-GPS-Pin (vom User bereitgestellt, weiße Ränder
  weggeschnitten + Ecken mit Hintergrundfarbe gefüllt)
2026-05-30 10:08:02 +02:00

176 lines
5.7 KiB
Swift

import SwiftUI
import MapKit
struct TrackingView: View {
@State private var tracker = LocationTracker()
@State private var now: Date = .now
@State private var showFinishSheet = false
private let ticker = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View {
NavigationStack {
Group {
if tracker.isTracking {
activeTracking
} else {
startScreen
}
}
.navigationTitle("Aufnehmen")
.navigationBarTitleDisplayMode(.inline)
}
.onReceive(ticker) { now = $0 }
.sheet(isPresented: $showFinishSheet) {
FinishWalkSheet(
points: tracker.points,
durationSeconds: durationSeconds,
distanceMeters: tracker.totalDistanceMeters,
onDiscard: { resetTracker() },
onSaved: { resetTracker() }
)
}
}
// MARK: - Active tracking
private var activeTracking: some View {
ZStack(alignment: .top) {
Map {
UserAnnotation()
if tracker.points.count >= 2 {
MapPolyline(coordinates: tracker.points.map {
CLLocationCoordinate2D(latitude: $0.lat, longitude: $0.lon)
})
.stroke(Color.accentColor, style: StrokeStyle(lineWidth: 5, lineJoin: .round))
}
}
.mapStyle(.standard(elevation: .flat, pointsOfInterest: .excludingAll))
.mapControlVisibility(.hidden)
.ignoresSafeArea(edges: .bottom)
VStack {
statsCard
Spacer()
stopButton
.padding(.bottom, 12)
}
.padding(.horizontal)
}
}
private var statsCard: some View {
HStack(spacing: 0) {
stat(value: String(format: "%.2f", tracker.totalDistanceMeters / 1000), unit: "km", label: "Distanz")
divider
stat(value: formatDuration(durationSeconds), unit: "", label: "Dauer")
divider
stat(value: "\(tracker.points.count)", unit: "", label: "Punkte")
}
.padding()
.background(.regularMaterial, in: RoundedRectangle(cornerRadius: 16))
}
private var divider: some View {
Divider().frame(height: 36)
}
private func stat(value: String, unit: String, label: String) -> some View {
VStack(spacing: 4) {
HStack(alignment: .firstTextBaseline, spacing: 2) {
Text(value).font(.title2.bold().monospacedDigit())
if !unit.isEmpty {
Text(unit).font(.callout).foregroundStyle(.secondary)
}
}
Text(label).font(.caption).foregroundStyle(.secondary)
}
.frame(maxWidth: .infinity)
}
private var stopButton: some View {
Button {
tracker.stop()
showFinishSheet = true
} label: {
HStack {
Image(systemName: "stop.circle.fill")
Text("Aufnahme stoppen").bold()
}
.frame(maxWidth: .infinity, minHeight: 56)
}
.background(.red, in: Capsule())
.foregroundStyle(.white)
}
// MARK: - Start screen
private var startScreen: some View {
VStack(spacing: 28) {
Spacer()
Image(systemName: "figure.walk.circle.fill")
.font(.system(size: 96))
.foregroundStyle(Color.accentColor)
VStack(spacing: 6) {
Text("Bereit für die nächste Gassi?")
.font(.title3.bold())
Text("Tippe auf Start, um deine Tour aufzuzeichnen — auch wenn dein iPhone gesperrt ist.")
.multilineTextAlignment(.center)
.foregroundStyle(.secondary)
.padding(.horizontal)
}
if tracker.permissionDenied {
permissionWarning
}
Button {
tracker.startOrRequest()
} label: {
HStack {
Image(systemName: "play.fill")
Text("Aufnahme starten").bold()
}
.frame(maxWidth: .infinity, minHeight: 56)
}
.background(Color.accentColor, in: Capsule())
.foregroundStyle(.white)
.padding(.horizontal)
Spacer()
}
}
private var permissionWarning: some View {
VStack(spacing: 6) {
Label("Standortzugriff fehlt", systemImage: "location.slash.fill")
.font(.subheadline.bold())
.foregroundStyle(.red)
Text("Bitte erlaube den Standortzugriff in den iOS-Einstellungen unter Datenschutz → Ortungsdienste → Ban Yaro Go.")
.font(.footnote)
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
.padding(.horizontal)
}
}
// MARK: - Helpers
private var durationSeconds: Int {
guard let startedAt = tracker.startedAt else { return 0 }
return Int(now.timeIntervalSince(startedAt))
}
private func formatDuration(_ seconds: Int) -> String {
let h = seconds / 3600
let m = (seconds % 3600) / 60
let s = seconds % 60
if h > 0 { return String(format: "%d:%02d:%02d", h, m, s) }
return String(format: "%d:%02d", m, s)
}
private func resetTracker() {
// Fresh tracker for the next walk; old `points` arrays are released with it.
tracker = LocationTracker()
}
}