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() } }