/ BalanceKit / SettingsView.swift
SettingsView.swift
1 // 2 // SettingsView.swift 3 // BalanceKit 4 // 5 // Created by Alexander Kunau on 12.07.25. 6 // 7 8 import SwiftUI 9 import UniformTypeIdentifiers 10 11 // MARK: - Backup Data Structure 12 struct BalanceKitBackup: Codable { 13 let version: Int 14 let exportDate: Date 15 let foodItems: [FoodItem] 16 let mealPresets: [MealPreset] 17 let workouts: [Workout] 18 let profile: UserProfileData 19 let goals: PersistedGoalsData 20 let appearance: String 21 22 static let currentVersion = 1 23 } 24 25 struct SettingsView: View { 26 @EnvironmentObject var appearanceSettings: AppearanceSettings 27 @ObservedObject var dataManager: FoodDataManager 28 @ObservedObject var workoutManager: WorkoutManager 29 @ObservedObject var userProfile: UserProfile 30 @State private var showingOnboarding = false 31 32 // Export/Import States 33 @State private var showingExportSheet = false 34 @State private var showingImportPicker = false 35 @State private var exportURL: URL? 36 @State private var showingAlert = false 37 @State private var alertTitle = "" 38 @State private var alertMessage = "" 39 40 var body: some View { 41 List { 42 Section(header: Text("Erscheinungsbild")) { 43 ForEach(AppearanceMode.allCases) { mode in 44 Button(action: { 45 appearanceSettings.appearanceMode = mode 46 }) { 47 HStack { 48 Image(systemName: mode.icon) 49 .foregroundColor(.blue) 50 .frame(width: 30) 51 52 Text(mode.rawValue) 53 54 Spacer() 55 56 if appearanceSettings.appearanceMode == mode { 57 Image(systemName: "checkmark") 58 .foregroundColor(.blue) 59 } 60 } 61 .contentShape(Rectangle()) 62 } 63 .buttonStyle(PlainButtonStyle()) 64 } 65 } 66 67 Section(header: Text("Persönliche Einstellungen")) { 68 Button { 69 showingOnboarding = true 70 } label: { 71 HStack { 72 Image(systemName: "person.crop.circle.fill") 73 .foregroundColor(.blue) 74 .frame(width: 30) 75 Text("Persönliche Daten bearbeiten") 76 } 77 } 78 79 NavigationLink(destination: GoalSettingsView(dataManager: dataManager)) { 80 HStack { 81 Image(systemName: "target") 82 .foregroundColor(.blue) 83 .frame(width: 30) 84 Text("Ernährungsziele anpassen") 85 } 86 } 87 } 88 89 Section(header: Text("Datensicherung")) { 90 Button { 91 exportData() 92 } label: { 93 HStack { 94 Image(systemName: "square.and.arrow.up") 95 .foregroundColor(.blue) 96 .frame(width: 30) 97 Text("Daten exportieren") 98 } 99 } 100 101 Button { 102 showingImportPicker = true 103 } label: { 104 HStack { 105 Image(systemName: "square.and.arrow.down") 106 .foregroundColor(.blue) 107 .frame(width: 30) 108 Text("Daten importieren") 109 } 110 } 111 } 112 113 Section(header: Text("Über die App")) { 114 HStack { 115 Text("Version") 116 Spacer() 117 Text("1.0") 118 .foregroundColor(.secondary) 119 } 120 121 Link(destination: URL(string: "https://foodi.neocities.org/")!) { 122 HStack { 123 Text("Datenschutz") 124 Spacer() 125 Image(systemName: "arrow.up.right.square") 126 .foregroundColor(.blue) 127 } 128 } 129 130 NavigationLink(destination: GoalSettingsView(dataManager: dataManager)) { 131 Text("Ernährungsziele") 132 } 133 } 134 135 Section { 136 Button(action: resetAppData) { 137 HStack { 138 Spacer() 139 Text("Alle Daten zurücksetzen") 140 .foregroundColor(.red) 141 Spacer() 142 } 143 } 144 } 145 } 146 .navigationTitle("Einstellungen") 147 .sheet(isPresented: $showingOnboarding) { 148 OnboardingView(userProfile: userProfile, dataManager: dataManager) 149 } 150 .sheet(isPresented: $showingExportSheet) { 151 if let url = exportURL { 152 ShareSheet(items: [url]) 153 } 154 } 155 .fileImporter( 156 isPresented: $showingImportPicker, 157 allowedContentTypes: [UTType.json], 158 allowsMultipleSelection: false 159 ) { result in 160 handleImport(result: result) 161 } 162 .alert(alertTitle, isPresented: $showingAlert) { 163 Button("OK", role: .cancel) { } 164 } message: { 165 Text(alertMessage) 166 } 167 } 168 169 // MARK: - Export/Import Functions 170 private func exportData() { 171 do { 172 // Sammle alle Daten 173 let backup = BalanceKitBackup( 174 version: BalanceKitBackup.currentVersion, 175 exportDate: Date(), 176 foodItems: dataManager.foodItems, 177 mealPresets: dataManager.mealPresets, 178 workouts: workoutManager.workouts, 179 profile: UserProfileData( 180 isOnboardingCompleted: userProfile.isOnboardingCompleted, 181 age: userProfile.age, 182 gender: userProfile.gender, 183 height: userProfile.height, 184 weight: userProfile.weight, 185 targetWeight: userProfile.targetWeight, 186 activityLevel: userProfile.activityLevel, 187 weightGoal: userProfile.weightGoal 188 ), 189 goals: PersistedGoalsData( 190 version: PersistedGoalsData.currentVersion, 191 calorieGoal: dataManager.dailyCalorieGoal, 192 proteinGoal: dataManager.dailyProteinGoal, 193 carbsGoal: dataManager.dailyCarbsGoal, 194 fatGoal: dataManager.dailyFatGoal 195 ), 196 appearance: appearanceSettings.appearanceMode.rawValue 197 ) 198 199 // Encode zu JSON 200 let encoder = JSONEncoder() 201 encoder.dateEncodingStrategy = .iso8601 202 encoder.outputFormatting = [.prettyPrinted, .sortedKeys] 203 let jsonData = try encoder.encode(backup) 204 205 // Speichere temporär 206 let dateFormatter = DateFormatter() 207 dateFormatter.dateFormat = "yyyy-MM-dd_HHmm" 208 let dateString = dateFormatter.string(from: Date()) 209 let filename = "Foodi_Backup_\(dateString).json" 210 211 let tempURL = FileManager.default.temporaryDirectory.appendingPathComponent(filename) 212 try jsonData.write(to: tempURL) 213 214 exportURL = tempURL 215 showingExportSheet = true 216 217 print("✅ Export erfolgreich: \(filename)") 218 219 } catch { 220 alertTitle = "Export fehlgeschlagen" 221 alertMessage = "Fehler beim Exportieren der Daten: \(error.localizedDescription)" 222 showingAlert = true 223 print("❌ Export Fehler: \(error)") 224 } 225 } 226 227 private func handleImport(result: Result<[URL], Error>) { 228 switch result { 229 case .success(let urls): 230 guard let url = urls.first else { return } 231 232 do { 233 // Lese JSON Datei 234 let jsonData = try Data(contentsOf: url) 235 236 // Decode Backup 237 let decoder = JSONDecoder() 238 decoder.dateDecodingStrategy = .iso8601 239 let backup = try decoder.decode(BalanceKitBackup.self, from: jsonData) 240 241 // Validiere Version 242 if backup.version > BalanceKitBackup.currentVersion { 243 alertTitle = "Inkompatible Version" 244 alertMessage = "Diese Backup-Datei wurde mit einer neueren App-Version erstellt und kann nicht importiert werden." 245 showingAlert = true 246 return 247 } 248 249 // Bestätige Import (überschreibt alle Daten) 250 confirmImport(backup: backup) 251 252 } catch { 253 alertTitle = "Import fehlgeschlagen" 254 alertMessage = "Fehler beim Lesen der Backup-Datei: \(error.localizedDescription)" 255 showingAlert = true 256 print("❌ Import Fehler: \(error)") 257 } 258 259 case .failure(let error): 260 alertTitle = "Dateiauswahl fehlgeschlagen" 261 alertMessage = error.localizedDescription 262 showingAlert = true 263 } 264 } 265 266 private func confirmImport(backup: BalanceKitBackup) { 267 let alert = UIAlertController( 268 title: "Daten importieren", 269 message: "Möchtest du wirklich alle aktuellen Daten durch das Backup vom \(formatDate(backup.exportDate)) ersetzen?\n\nDies überschreibt:\n• \(backup.foodItems.count) Nahrungsmittel\n• \(backup.mealPresets.count) Presets\n• \(backup.workouts.count) Workouts\n• Dein Profil und Ziele", 270 preferredStyle: .alert 271 ) 272 273 alert.addAction(UIAlertAction(title: "Abbrechen", style: .cancel)) 274 alert.addAction(UIAlertAction(title: "Importieren", style: .default) { _ in 275 executeImport(backup: backup) 276 }) 277 278 if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, 279 let rootViewController = windowScene.windows.first?.rootViewController { 280 rootViewController.present(alert, animated: true) 281 } 282 } 283 284 private func executeImport(backup: BalanceKitBackup) { 285 // Importiere alle Daten 286 dataManager.foodItems = backup.foodItems 287 dataManager.mealPresets = backup.mealPresets 288 workoutManager.workouts = backup.workouts 289 290 // Importiere Goals 291 dataManager.dailyCalorieGoal = backup.goals.calorieGoal 292 dataManager.dailyProteinGoal = backup.goals.proteinGoal 293 dataManager.dailyCarbsGoal = backup.goals.carbsGoal 294 dataManager.dailyFatGoal = backup.goals.fatGoal 295 296 // Importiere Profil 297 userProfile.isOnboardingCompleted = backup.profile.isOnboardingCompleted 298 userProfile.age = backup.profile.age 299 userProfile.gender = backup.profile.gender 300 userProfile.height = backup.profile.height 301 userProfile.weight = backup.profile.weight 302 userProfile.targetWeight = backup.profile.targetWeight 303 userProfile.activityLevel = backup.profile.activityLevel 304 userProfile.weightGoal = backup.profile.weightGoal 305 userProfile.save() 306 307 // Importiere Appearance 308 if let mode = AppearanceMode(rawValue: backup.appearance) { 309 appearanceSettings.appearanceMode = mode 310 } 311 312 alertTitle = "Import erfolgreich" 313 alertMessage = "Alle Daten wurden erfolgreich wiederhergestellt." 314 showingAlert = true 315 316 print("✅ Import erfolgreich: \(backup.foodItems.count) Items, \(backup.workouts.count) Workouts") 317 } 318 319 private func formatDate(_ date: Date) -> String { 320 let formatter = DateFormatter() 321 formatter.dateStyle = .medium 322 formatter.timeStyle = .short 323 return formatter.string(from: date) 324 } 325 326 // MARK: - Reset Function (komplett überarbeitet) 327 // MARK: - Reset Function (komplett überarbeitet) 328 private func resetAppData() { 329 let alert = UIAlertController( 330 title: "Daten zurücksetzen", 331 message: "Bist du sicher, dass du alle deine eingetragenen Daten löschen möchtest? Diese Aktion kann nicht rückgängig gemacht werden.\n\nGelöscht werden:\n• Alle Nahrungsmittel\n• Alle Presets\n• Alle Workouts\n• Ernährungsziele\n• Profil (Onboarding wird erneut angezeigt)", 332 preferredStyle: .alert 333 ) 334 335 alert.addAction(UIAlertAction(title: "Abbrechen", style: .cancel)) 336 alert.addAction(UIAlertAction(title: "Alles löschen", style: .destructive) { _ in 337 // Lösche alle FoodItems 338 dataManager.foodItems = [] 339 340 // Lösche alle Presets 341 dataManager.mealPresets = [] 342 343 // Lösche alle Workouts 344 workoutManager.workouts = [] 345 346 // Setze Goals zurück auf Defaults 347 dataManager.dailyCalorieGoal = 2000 348 dataManager.dailyProteinGoal = 100.0 349 dataManager.dailyCarbsGoal = 250.0 350 dataManager.dailyFatGoal = 70.0 351 352 // Setze Profil zurück 353 let freshProfile = UserProfile() 354 freshProfile.isOnboardingCompleted = false 355 freshProfile.save() 356 357 // Setze Appearance zurück auf System 358 appearanceSettings.appearanceMode = .system 359 360 print("✅ Alle Daten wurden zurückgesetzt") 361 362 // Zeige Bestätigung 363 alertTitle = "Daten gelöscht" 364 alertMessage = "Alle Daten wurden erfolgreich zurückgesetzt. Die App wird beim nächsten Start das Onboarding anzeigen." 365 showingAlert = true 366 }) 367 368 if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, 369 let rootViewController = windowScene.windows.first?.rootViewController { 370 rootViewController.present(alert, animated: true) 371 } 372 } 373 } 374 375 // MARK: - ShareSheet für Export 376 struct ShareSheet: UIViewControllerRepresentable { 377 let items: [Any] 378 379 func makeUIViewController(context: Context) -> UIActivityViewController { 380 let controller = UIActivityViewController(activityItems: items, applicationActivities: nil) 381 return controller 382 } 383 384 func updateUIViewController(_ uiViewController: UIActivityViewController, context: Context) {} 385 } 386 387 #Preview { 388 NavigationStack { 389 SettingsView(dataManager: FoodDataManager(), workoutManager: WorkoutManager(), userProfile: UserProfile()) 390 .environmentObject(AppearanceSettings()) 391 } 392 }