🎉 Initial Commit
This commit is contained in:
parent
cd9348cb19
commit
80cdd08849
6 changed files with 295 additions and 17 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
46
RunnerTracker/APIService.swift
Normal file
46
RunnerTracker/APIService.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,24 +1,124 @@
|
|||
//
|
||||
// ContentView.swift
|
||||
// RunnerTracker
|
||||
//
|
||||
// Created by Markus Benjamin Tabler on 28.04.26.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import CoreLocation
|
||||
|
||||
struct ContentView: View {
|
||||
var body: some View {
|
||||
VStack {
|
||||
Image(systemName: "globe")
|
||||
.imageScale(.large)
|
||||
.foregroundStyle(.tint)
|
||||
Text("Hello, world!")
|
||||
// --- 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 {
|
||||
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 {
|
||||
ContentView()
|
||||
// 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")
|
||||
}
|
||||
}
|
||||
|
||||
// --- 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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
92
RunnerTracker/Trackingmanager.swift
Normal file
92
RunnerTracker/Trackingmanager.swift
Normal 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
|
||||
}
|
||||
}
|
||||
10
RunnerTracker/config.xcconfig
Normal file
10
RunnerTracker/config.xcconfig
Normal 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
|
||||
Loading…
Reference in a new issue