diff --git a/RunnerTracker.xcodeproj/project.pbxproj b/RunnerTracker.xcodeproj/project.pbxproj
index c3365aa..ec1a273 100644
--- a/RunnerTracker.xcodeproj/project.pbxproj
+++ b/RunnerTracker.xcodeproj/project.pbxproj
@@ -127,6 +127,8 @@
/* Begin XCBuildConfiguration section */
B13C13192FA11DF600C7A367 /* Debug */ = {
isa = XCBuildConfiguration;
+ baseConfigurationReferenceAnchor = B13C13122FA11DF300C7A367 /* RunnerTracker */;
+ baseConfigurationReferenceRelativePath = config.xcconfig;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
@@ -190,6 +192,8 @@
};
B13C131A2FA11DF600C7A367 /* Release */ = {
isa = XCBuildConfiguration;
+ baseConfigurationReferenceAnchor = B13C13122FA11DF300C7A367 /* RunnerTracker */;
+ baseConfigurationReferenceRelativePath = config.xcconfig;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
@@ -246,13 +250,19 @@
};
B13C131C2FA11DF600C7A367 /* Debug */ = {
isa = XCBuildConfiguration;
+ baseConfigurationReferenceAnchor = B13C13122FA11DF300C7A367 /* RunnerTracker */;
+ baseConfigurationReferenceRelativePath = config.xcconfig;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_TEAM = Q39WZ6UXL3;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_FILE = "";
+ INFOPLIST_KEY_NSLocationAlwaysAndWhenInUseUsageDescription = "\"Die App trackt deinen Lauf auch, wenn das Handy gesperrt ist.\"";
+ INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "\"Deine Laufdaten werden für die Strecke benötigt.\"";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
@@ -277,13 +287,19 @@
};
B13C131D2FA11DF600C7A367 /* Release */ = {
isa = XCBuildConfiguration;
+ baseConfigurationReferenceAnchor = B13C13122FA11DF300C7A367 /* RunnerTracker */;
+ baseConfigurationReferenceRelativePath = config.xcconfig;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_TEAM = Q39WZ6UXL3;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_FILE = "";
+ INFOPLIST_KEY_NSLocationAlwaysAndWhenInUseUsageDescription = "\"Die App trackt deinen Lauf auch, wenn das Handy gesperrt ist.\"";
+ INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "\"Deine Laufdaten werden für die Strecke benötigt.\"";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
diff --git a/RunnerTracker.xcodeproj/xcuserdata/benji.xcuserdatad/xcschemes/xcschememanagement.plist b/RunnerTracker.xcodeproj/xcuserdata/benji.xcuserdatad/xcschemes/xcschememanagement.plist
new file mode 100644
index 0000000..9405eb0
--- /dev/null
+++ b/RunnerTracker.xcodeproj/xcuserdata/benji.xcuserdatad/xcschemes/xcschememanagement.plist
@@ -0,0 +1,14 @@
+
+
+
+
+ SchemeUserState
+
+ RunnerTracker.xcscheme_^#shared#^_
+
+ orderHint
+ 0
+
+
+
+
diff --git a/RunnerTracker/APIService.swift b/RunnerTracker/APIService.swift
new file mode 100644
index 0000000..7177f22
--- /dev/null
+++ b/RunnerTracker/APIService.swift
@@ -0,0 +1,46 @@
+import Foundation
+
+class APIService {
+ static let shared = APIService()
+
+ // Ersetze dies durch deine Cloudflare-URL oder lokale Pi-IP
+ let writeKey = (Bundle.main.object(forInfoDictionaryKey: "WriteKey") as? String) ?? ""
+ let baseURL = (Bundle.main.object(forInfoDictionaryKey: "BaseUrl") as? String) ?? ""
+
+ func createRun(eventName: String, type: String, completion: @escaping (Result) -> Void) {
+ guard let url = URL(string: "\(baseURL)/runs/create") else { return }
+
+ var request = URLRequest(url: url)
+ print(request)
+ request.httpMethod = "POST"
+ request.setValue("application/json", forHTTPHeaderField: "Content-Type")
+ request.setValue(writeKey, forHTTPHeaderField: "X-Write-Key")
+
+ // Das schicken wir an den Pi (u_id und e_id sind Platzhalter)
+ let body: [String: Any] = [
+ "u_id": (Bundle.main.object(forInfoDictionaryKey: "UserID") as? String) ?? "",
+ "e_id": "f0e061e2-4317-11f1-b48c-b827ebe636d1",
+ "run_type": type,
+ "event_name": eventName // Dein neues Feld!
+ ]
+
+ request.httpBody = try? JSONSerialization.data(withJSONObject: body)
+
+ URLSession.shared.dataTask(with: request) { data, response, error in
+ if let error = error {
+ completion(.failure(error))
+ return
+ }
+
+ guard let data = data,
+ let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
+ let runID = json["run_id"] as? String else {
+ let parseError = NSError(domain: "", code: 500, userInfo: [NSLocalizedDescriptionKey: "Fehler beim Lesen der RunID"])
+ completion(.failure(parseError))
+ return
+ }
+
+ completion(.success(runID))
+ }.resume()
+ }
+}
diff --git a/RunnerTracker/ContentView.swift b/RunnerTracker/ContentView.swift
index 9506d90..c828f76 100644
--- a/RunnerTracker/ContentView.swift
+++ b/RunnerTracker/ContentView.swift
@@ -1,24 +1,124 @@
-//
-// ContentView.swift
-// RunnerTracker
-//
-// Created by Markus Benjamin Tabler on 28.04.26.
-//
-
import SwiftUI
+import CoreLocation
struct ContentView: View {
+ // --- App States ---
+ @State private var appState: AppState = .idle
+ @State private var runID: String? = nil
+
+ // --- Input Fields ---
+ @State private var eventName: String = ""
+ @State private var selectedType: String = "training"
+
+ // --- Managers ---
+ @StateObject private var trackingManager = TrackingManager()
+
+ enum AppState {
+ case idle, creatingRun, readyToRun, tracking
+ }
+
var body: some View {
- VStack {
- Image(systemName: "globe")
- .imageScale(.large)
- .foregroundStyle(.tint)
- Text("Hello, world!")
+ NavigationView {
+ Form {
+ // SEKTION 1: SETUP
+ Section(header: Text("Lauf Vorbereiten")) {
+ TextField("Event Name (z.B. Feierabendrunde)", text: $eventName)
+ .disabled(appState == .tracking || appState == .readyToRun)
+
+ Picker("Typ", selection: $selectedType) {
+ Text("Training").tag("training")
+ Text("Wettkampf").tag("race")
+ }
+ .pickerStyle(.segmented)
+ .disabled(appState == .tracking || appState == .readyToRun)
+
+ if appState == .idle || appState == .creatingRun {
+ Button(action: createRun) {
+ HStack {
+ if appState == .creatingRun { ProgressView().padding(.trailing, 5) }
+ Text("Lauf in DB registrieren")
+ }
+ }
+ .disabled(eventName.isEmpty || appState == .creatingRun)
+ } else {
+ HStack {
+ Image(systemName: "checkmark.circle.fill").foregroundColor(.green)
+ Text("ID: \(runID?.prefix(8) ?? "")...")
+ .font(.caption).monospaced()
+ Spacer()
+ Button("Reset") { resetSetup() }
+ .font(.caption)
+ .foregroundColor(.red)
+ }
+ }
+ }
+
+ // SEKTION 2: TRACKING
+ Section(header: Text("Action")) {
+ Button(action: toggleTracking) {
+ HStack {
+ Image(systemName: appState == .tracking ? "stop.fill" : "play.fill")
+ Text(appState == .tracking ? "Tracking Stoppen" : "JETZT STARTEN")
+ }
+ .frame(maxWidth: .infinity, minHeight: 44)
+ }
+ .buttonStyle(.borderedProminent)
+ .tint(appState == .tracking ? .red : .green)
+ .disabled(appState != .readyToRun && appState != .tracking)
+
+ if appState == .tracking {
+ HStack {
+ VStack(alignment: .leading) {
+ Text("Lat: \(trackingManager.lastLocation?.coordinate.latitude ?? 0, specifier: "%.5f")")
+ Text("Lon: \(trackingManager.lastLocation?.coordinate.longitude ?? 0, specifier: "%.5f")")
+ }
+ .font(.caption).monospaced()
+ Spacer()
+ ProgressView()
+ }
+ }
+ }
+ }
+ .navigationTitle("Runner Tracker")
}
- .padding()
+ }
+
+ // --- Logik Funktionen ---
+
+ func createRun() {
+ appState = .creatingRun
+
+ // Hier dein API Call an /runs/create
+ APIService.shared.createRun(eventName: eventName, type: selectedType) { result in
+ DispatchQueue.main.async {
+ switch result {
+ case .success(let id):
+ self.runID = id
+ self.appState = .readyToRun
+ print("✅ Run bereit mit ID: \(id)")
+ case .failure(let error):
+ print("❌ Fehler: \(error.localizedDescription)")
+ self.appState = .idle
+ }
+ }
+ }
+ }
+
+ func toggleTracking() {
+ if appState == .tracking {
+ // Greife direkt auf das Objekt zu, NICHT auf das Binding (kein $)
+ trackingManager.stop()
+ appState = .idle
+ runID = nil
+ } else {
+ guard let id = runID else { return }
+ trackingManager.start(runID: id)
+ appState = .tracking
+ }
+ }
+
+ func resetSetup() {
+ appState = .idle
+ runID = nil
}
}
-
-#Preview {
- ContentView()
-}
diff --git a/RunnerTracker/Trackingmanager.swift b/RunnerTracker/Trackingmanager.swift
new file mode 100644
index 0000000..6a9cdfe
--- /dev/null
+++ b/RunnerTracker/Trackingmanager.swift
@@ -0,0 +1,92 @@
+import Foundation
+import CoreLocation
+import SwiftUI
+import Combine
+
+class TrackingManager: NSObject, ObservableObject, CLLocationManagerDelegate {
+
+ // Mit @Published signalisierst du der UI, wenn sich etwas ändert
+ @Published var lastLocation: CLLocation?
+
+ private let locationManager = CLLocationManager()
+ private var timer: Timer?
+ var currentRunID: String?
+
+ override init() {
+ super.init()
+ locationManager.delegate = self
+ locationManager.desiredAccuracy = kCLLocationAccuracyBest
+ locationManager.allowsBackgroundLocationUpdates = true
+ locationManager.pausesLocationUpdatesAutomatically = false
+ }
+
+ func start(runID: String) {
+ print("🚀 TrackingManager gestartet mit Run-ID: \(runID)")
+ self.currentRunID = runID
+ locationManager.requestAlwaysAuthorization()
+ locationManager.startUpdatingLocation()
+
+ // Timer für den 5-Sekunden-Takt
+ timer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: true) { _ in
+ self.sendCurrentLocation()
+ }
+ }
+
+ func stop() {
+ print("🛑 TrackingManager gestoppt")
+ locationManager.stopUpdatingLocation()
+ timer?.invalidate()
+ }
+
+ private func sendCurrentLocation() {
+ guard let location = lastLocation else {
+ print("⏳ Warte auf GPS-Signal (lastLocation ist noch nil)...")
+ return
+ }
+ guard let rID = currentRunID else {
+ print("❌ Fehler: Keine currentRunID vorhanden")
+ return
+ }
+
+ let base = (Bundle.main.object(forInfoDictionaryKey: "BaseUrl") as? String) ?? ""
+ guard let url = URL(string: base+"/logs/gps") else { return }
+
+ var request = URLRequest(url: url)
+ request.httpMethod = "POST"
+ request.setValue((Bundle.main.object(forInfoDictionaryKey: "WriteKey") as? String) ?? "", forHTTPHeaderField: "X-API-KEY")
+ request.setValue("application/json", forHTTPHeaderField: "Content-Type")
+
+ let logData: [String: Any] = [
+ "run_id": rID,
+ "lat": location.coordinate.latitude,
+ "lon": location.coordinate.longitude,
+ "alt": location.altitude,
+ "timestamp": ISO8601DateFormatter().string(from: Date())
+ ]
+
+ request.httpBody = try? JSONSerialization.data(withJSONObject: logData)
+
+ URLSession.shared.dataTask(with: request) { data, response, error in
+ if let error = error {
+ print("❌ Netzwerk-Fehler: \(error.localizedDescription)")
+ return
+ }
+
+ if let httpResponse = response as? HTTPURLResponse {
+ print("🌍 Pi-Antwort: Status \(httpResponse.statusCode)")
+
+ // Falls es kein Erfolg (200-299) war, drucken wir die Fehlermeldung vom Pi
+ if !(200...299).contains(httpResponse.statusCode), let data = data {
+ let msg = String(data: data, encoding: .utf8) ?? "Keine Fehlerdetails"
+ print("📩 Pi-Fehlermeldung: \(msg)")
+ } else if httpResponse.statusCode == 200 {
+ print("✅ Datenpunkt erfolgreich an Pi gesendet")
+ }
+ }
+ }.resume()
+ }
+
+ func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
+ self.lastLocation = locations.last
+ }
+}
diff --git a/RunnerTracker/config.xcconfig b/RunnerTracker/config.xcconfig
new file mode 100644
index 0000000..b10e801
--- /dev/null
+++ b/RunnerTracker/config.xcconfig
@@ -0,0 +1,10 @@
+//
+// config.xcconfig
+// RunnerTracker
+//
+// Created by Markus Benjamin Tabler on 28.04.26.
+//
+
+API_URL = https:/$()/runnerapi.bybenji.de
+WRITE_API_KEY = f27e1dffd25507c25955629b43313a20823276c0c0f64734f572ac5c7d6d1346
+USER_ID = a0e100c0-42e9-11f1-b48c-b827ebe636d1