🎉 Initial Commit

This commit is contained in:
Markus Benjamin Tabler 2026-06-26 19:55:52 +02:00
parent cd9348cb19
commit 80cdd08849
6 changed files with 295 additions and 17 deletions

View file

@ -127,6 +127,8 @@
/* Begin XCBuildConfiguration section */ /* Begin XCBuildConfiguration section */
B13C13192FA11DF600C7A367 /* Debug */ = { B13C13192FA11DF600C7A367 /* Debug */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReferenceAnchor = B13C13122FA11DF300C7A367 /* RunnerTracker */;
baseConfigurationReferenceRelativePath = config.xcconfig;
buildSettings = { buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO; ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
@ -190,6 +192,8 @@
}; };
B13C131A2FA11DF600C7A367 /* Release */ = { B13C131A2FA11DF600C7A367 /* Release */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReferenceAnchor = B13C13122FA11DF300C7A367 /* RunnerTracker */;
baseConfigurationReferenceRelativePath = config.xcconfig;
buildSettings = { buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO; ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
@ -246,13 +250,19 @@
}; };
B13C131C2FA11DF600C7A367 /* Debug */ = { B13C131C2FA11DF600C7A367 /* Debug */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReferenceAnchor = B13C13122FA11DF300C7A367 /* RunnerTracker */;
baseConfigurationReferenceRelativePath = config.xcconfig;
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = Q39WZ6UXL3;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = 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_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES;
@ -277,13 +287,19 @@
}; };
B13C131D2FA11DF600C7A367 /* Release */ = { B13C131D2FA11DF600C7A367 /* Release */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReferenceAnchor = B13C13122FA11DF300C7A367 /* RunnerTracker */;
baseConfigurationReferenceRelativePath = config.xcconfig;
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = Q39WZ6UXL3;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = 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_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES;

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>SchemeUserState</key>
<dict>
<key>RunnerTracker.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>0</integer>
</dict>
</dict>
</dict>
</plist>

View file

@ -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<String, Error>) -> 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()
}
}

View file

@ -1,24 +1,124 @@
//
// ContentView.swift
// RunnerTracker
//
// Created by Markus Benjamin Tabler on 28.04.26.
//
import SwiftUI import SwiftUI
import CoreLocation
struct ContentView: View { struct ContentView: View {
var body: some View { // --- App States ---
VStack { @State private var appState: AppState = .idle
Image(systemName: "globe") @State private var runID: String? = nil
.imageScale(.large)
.foregroundStyle(.tint) // --- Input Fields ---
Text("Hello, world!") @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 {
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)
} }
.padding()
} }
} }
#Preview { // SEKTION 2: TRACKING
ContentView() 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")
}
}
// --- 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
}
} }

View file

@ -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
}
}

View file

@ -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