From e26830439dfdb97c1d685bf5cda05a5d2df9f21f Mon Sep 17 00:00:00 2001 From: Arturs Artamonovs Date: Mon, 17 Nov 2025 00:44:09 +0000 Subject: refactor experiment --- LearnMapKit/Managers/ConfigurationManager.swift | 322 ++++++++++++++++++++++++ 1 file changed, 322 insertions(+) create mode 100644 LearnMapKit/Managers/ConfigurationManager.swift (limited to 'LearnMapKit/Managers') diff --git a/LearnMapKit/Managers/ConfigurationManager.swift b/LearnMapKit/Managers/ConfigurationManager.swift new file mode 100644 index 0000000..15ae274 --- /dev/null +++ b/LearnMapKit/Managers/ConfigurationManager.swift @@ -0,0 +1,322 @@ +// +// 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) + } +} \ No newline at end of file -- cgit v1.2.3