🎉 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 */
|
/* 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;
|
||||||
|
|
|
||||||
|
|
@ -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 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)
|
|
||||||
Text("Hello, world!")
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#Preview {
|
// --- Input Fields ---
|
||||||
ContentView()
|
@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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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