// // ConfigurationManager.swift // LearnMapKit // // Centralized configuration management // import Foundation import Combine import MapKit // MARK: - App Configuration struct AppConfiguration: Equatable { let dataSource: DataSourceConfiguration let displaySettings: DisplaySettings let mapSettings: MapSettings let networkSettings: NetworkSettings static let `default` = AppConfiguration( dataSource: NetworkDataSourceConfiguration( hostname: "64.226.23.78", port: 30003 ), displaySettings: DisplaySettings(), mapSettings: MapSettings(), networkSettings: NetworkSettings() ) } // MARK: - Display Settings struct DisplaySettings: Equatable { var showAircraftLabels: Bool = true var showAltitude: Bool = true var showSpeed: Bool = false var aircraftHistoryLength: Int = 10 var updateInterval: TimeInterval = 1.0 var theme: AppTheme = .system enum AppTheme: String, CaseIterable { case light, dark, system } } // MARK: - Map Settings struct MapSettings: Equatable { var defaultCenter: CLLocationCoordinate2D = CLLocationCoordinate2D( latitude: 55.90159, longitude: -3.53154 ) var defaultSpan: MKCoordinateSpan = MKCoordinateSpan( latitudeDelta: 0.1, longitudeDelta: 0.1 ) var mapStyle: MapStyle = .hybrid var showTerrain: Bool = true enum MapStyle: String, CaseIterable { case standard, hybrid, satellite var mkMapType: MKMapType { switch self { case .standard: return .standard case .hybrid: return .hybrid case .satellite: return .satellite } } } // Custom equality to handle CLLocationCoordinate2D static func == (lhs: MapSettings, rhs: MapSettings) -> Bool { lhs.defaultCenter.latitude == rhs.defaultCenter.latitude && lhs.defaultCenter.longitude == rhs.defaultCenter.longitude && lhs.defaultSpan.latitudeDelta == rhs.defaultSpan.latitudeDelta && lhs.defaultSpan.longitudeDelta == rhs.defaultSpan.longitudeDelta && lhs.mapStyle == rhs.mapStyle && lhs.showTerrain == rhs.showTerrain } } // MARK: - Network Settings struct NetworkSettings: Equatable { var connectionTimeout: TimeInterval = 10.0 var reconnectAttempts: Int = 3 var bufferSize: Int = 1024 var enableHeartbeat: Bool = true var heartbeatInterval: TimeInterval = 30.0 } // MARK: - Configuration Manager Protocol protocol ConfigurationManager: AnyObject { var currentConfiguration: AnyPublisher { get } var hasUnsavedChanges: AnyPublisher { get } func getCurrentConfiguration() -> AppConfiguration func updateConfiguration(_ configuration: AppConfiguration) async func updateDataSource(_ dataSource: DataSourceConfiguration) async func updateDisplaySettings(_ settings: DisplaySettings) async func updateMapSettings(_ settings: MapSettings) async func updateNetworkSettings(_ settings: NetworkSettings) async func resetToDefaults() async func saveConfiguration() async throws func validateConfiguration(_ configuration: AppConfiguration) -> ConfigurationValidationResult } // MARK: - Validation struct ConfigurationValidationResult { let isValid: Bool let errors: [String] let warnings: [String] static let valid = ConfigurationValidationResult(isValid: true, errors: [], warnings: []) } // MARK: - Default Implementation @MainActor class DefaultConfigurationManager: ConfigurationManager { // MARK: - Published Properties var currentConfiguration: AnyPublisher { configurationSubject.eraseToAnyPublisher() } var hasUnsavedChanges: AnyPublisher { hasChangesSubject.eraseToAnyPublisher() } // MARK: - Private Properties private let configurationSubject: CurrentValueSubject private let hasChangesSubject = CurrentValueSubject(false) private var savedConfiguration: AppConfiguration private let userDefaults = UserDefaults.standard // MARK: - Keys for UserDefaults private enum Keys { static let configuration = "app_configuration" } init() { // Load configuration from UserDefaults or use default if let data = userDefaults.data(forKey: Keys.configuration), let configuration = try? JSONDecoder().decode(AppConfiguration.self, from: data) { self.savedConfiguration = configuration } else { self.savedConfiguration = AppConfiguration.default } self.configurationSubject = CurrentValueSubject(savedConfiguration) // Monitor for changes setupChangeMonitoring() } // MARK: - ConfigurationManager Implementation func getCurrentConfiguration() -> AppConfiguration { configurationSubject.value } func updateConfiguration(_ configuration: AppConfiguration) async { configurationSubject.send(configuration) checkForChanges() } func updateDataSource(_ dataSource: DataSourceConfiguration) async { var current = getCurrentConfiguration() current = AppConfiguration( dataSource: dataSource, displaySettings: current.displaySettings, mapSettings: current.mapSettings, networkSettings: current.networkSettings ) await updateConfiguration(current) } func updateDisplaySettings(_ settings: DisplaySettings) async { var current = getCurrentConfiguration() current = AppConfiguration( dataSource: current.dataSource, displaySettings: settings, mapSettings: current.mapSettings, networkSettings: current.networkSettings ) await updateConfiguration(current) } func updateMapSettings(_ settings: MapSettings) async { var current = getCurrentConfiguration() current = AppConfiguration( dataSource: current.dataSource, displaySettings: current.displaySettings, mapSettings: settings, networkSettings: current.networkSettings ) await updateConfiguration(current) } func updateNetworkSettings(_ settings: NetworkSettings) async { var current = getCurrentConfiguration() current = AppConfiguration( dataSource: current.dataSource, displaySettings: current.displaySettings, mapSettings: current.mapSettings, networkSettings: settings ) await updateConfiguration(current) } func resetToDefaults() async { await updateConfiguration(AppConfiguration.default) } func saveConfiguration() async throws { let current = getCurrentConfiguration() let data = try JSONEncoder().encode(current) userDefaults.set(data, forKey: Keys.configuration) savedConfiguration = current hasChangesSubject.send(false) } func validateConfiguration(_ configuration: AppConfiguration) -> ConfigurationValidationResult { var errors: [String] = [] var warnings: [String] = [] // Validate data source switch configuration.dataSource.sourceType { case .file(let path): if !FileManager.default.fileExists(atPath: path) { errors.append("File does not exist at path: \(path)") } case .network(let host, let port): if host.isEmpty { errors.append("Network hostname cannot be empty") } if port <= 0 || port > 65535 { errors.append("Invalid port number: \(port)") } case .test: warnings.append("Using test data source") } // Validate display settings if configuration.displaySettings.updateInterval < 0.1 { errors.append("Update interval must be at least 0.1 seconds") } if configuration.displaySettings.aircraftHistoryLength < 1 { errors.append("Aircraft history length must be at least 1") } // Validate network settings if configuration.networkSettings.connectionTimeout < 1.0 { errors.append("Connection timeout must be at least 1 second") } if configuration.networkSettings.reconnectAttempts < 0 { errors.append("Reconnect attempts cannot be negative") } return ConfigurationValidationResult( isValid: errors.isEmpty, errors: errors, warnings: warnings ) } // MARK: - Private Methods private func setupChangeMonitoring() { configurationSubject .dropFirst() // Skip initial value .sink { [weak self] _ in self?.checkForChanges() } .store(in: &cancellables) } private func checkForChanges() { let hasChanges = getCurrentConfiguration() != savedConfiguration hasChangesSubject.send(hasChanges) } private var cancellables = Set() } // MARK: - Extensions for Codable Support extension AppConfiguration: Codable { // Custom coding keys and implementation would be needed here // for proper serialization of the configuration } extension DisplaySettings: Codable {} extension NetworkSettings: Codable {} extension MapSettings: Codable { enum CodingKeys: String, CodingKey { case defaultCenterLatitude, defaultCenterLongitude case defaultSpanLatitudeDelta, defaultSpanLongitudeDelta case mapStyle, showTerrain } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) let latitude = try container.decode(Double.self, forKey: .defaultCenterLatitude) let longitude = try container.decode(Double.self, forKey: .defaultCenterLongitude) defaultCenter = CLLocationCoordinate2D(latitude: latitude, longitude: longitude) let latDelta = try container.decode(Double.self, forKey: .defaultSpanLatitudeDelta) let lonDelta = try container.decode(Double.self, forKey: .defaultSpanLongitudeDelta) defaultSpan = MKCoordinateSpan(latitudeDelta: latDelta, longitudeDelta: lonDelta) mapStyle = try container.decode(MapStyle.self, forKey: .mapStyle) showTerrain = try container.decode(Bool.self, forKey: .showTerrain) } func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(defaultCenter.latitude, forKey: .defaultCenterLatitude) try container.encode(defaultCenter.longitude, forKey: .defaultCenterLongitude) try container.encode(defaultSpan.latitudeDelta, forKey: .defaultSpanLatitudeDelta) try container.encode(defaultSpan.longitudeDelta, forKey: .defaultSpanLongitudeDelta) try container.encode(mapStyle, forKey: .mapStyle) try container.encode(showTerrain, forKey: .showTerrain) } }