Repository: inderdhir/DatWeatherDoe Branch: main Commit: 983df24d0f69 Files: 91 Total size: 296.0 KB Directory structure: gitextract_t2ij2trf/ ├── .github/ │ ├── FUNDING.yml │ └── workflows/ │ └── lint.yml ├── .gitignore ├── .swiftformat ├── .swiftlint.yml ├── CONTRIBUTING.md ├── DatWeatherDoe/ │ ├── API/ │ │ ├── NetworkClient.swift │ │ ├── Response/ │ │ │ ├── AirQuality.swift │ │ │ ├── ForecastData.swift │ │ │ ├── ForecastTemperatureData.swift │ │ │ ├── SunriseSunsetData.swift │ │ │ ├── TemperatureData.swift │ │ │ ├── WeatherAPIResponse.swift │ │ │ ├── WeatherAPIResponseParser.swift │ │ │ └── WindData.swift │ │ ├── WeatherData.swift │ │ └── WeatherError.swift │ ├── Config/ │ │ ├── APIKeyParser.swift │ │ ├── ConfigManager.swift │ │ └── ConfigOptions.swift │ ├── DatWeatherDoeApp.swift │ ├── Localization/ │ │ └── en.xcloc/ │ │ ├── Localized Contents/ │ │ │ └── en.xliff │ │ ├── Source Contents/ │ │ │ └── DatWeatherDoe/ │ │ │ ├── Resources/ │ │ │ │ └── en.lproj/ │ │ │ │ └── InfoPlist.strings │ │ │ └── UI/ │ │ │ └── Base.lproj/ │ │ │ └── MainMenu.xib │ │ └── contents.json │ ├── Reachability/ │ │ └── WeatherReachability.swift │ ├── Resources/ │ │ ├── Assets.xcassets/ │ │ │ ├── AppIcon.appiconset/ │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ ├── DatWeatherDoe.entitlements │ │ ├── DevelopmentAssets/ │ │ │ └── TestData.swift │ │ ├── Info.plist │ │ └── Localization/ │ │ └── Localizable.xcstrings │ ├── UI/ │ │ ├── Configure/ │ │ │ ├── ConfigureOptionsView.swift │ │ │ ├── ConfigureUnitOptionsView.swift │ │ │ ├── ConfigureValueSeparatorOptionsView.swift │ │ │ ├── ConfigureView.swift │ │ │ ├── ConfigureViewModel.swift │ │ │ ├── ConfigureWeatherOptionsView.swift │ │ │ └── Options/ │ │ │ ├── MeasurementUnit.swift │ │ │ ├── RefreshInterval.swift │ │ │ ├── TemperatureUnit.swift │ │ │ ├── WeatherConditionPosition.swift │ │ │ └── WeatherSource.swift │ │ ├── Decorator/ │ │ │ ├── Condition/ │ │ │ │ ├── WeatherCondition.swift │ │ │ │ ├── WeatherConditionBuilder.swift │ │ │ │ └── WeatherConditionTextMapper.swift │ │ │ ├── Text/ │ │ │ │ ├── HumidityTextBuilder.swift │ │ │ │ ├── SunriseAndSunsetTextBuilder.swift │ │ │ │ ├── Temperature/ │ │ │ │ │ ├── TemperatureForecastTextBuilder.swift │ │ │ │ │ ├── TemperatureFormatter.swift │ │ │ │ │ ├── TemperatureTextBuilder.swift │ │ │ │ │ └── TemperatureWithDegreesCreator.swift │ │ │ │ ├── UVIndexTextBuilder.swift │ │ │ │ └── WeatherTextBuilder.swift │ │ │ └── WeatherDataBuilder.swift │ │ ├── Forecaster/ │ │ │ └── WeatherForecaster.swift │ │ ├── Menu Bar/ │ │ │ ├── CustomButton.swift │ │ │ ├── DropdownIcon.swift │ │ │ ├── MenuOptionsView.swift │ │ │ ├── MenuView.swift │ │ │ ├── NonInteractiveMenuOptionView.swift │ │ │ └── WindSpeedFormatter.swift │ │ └── Status Bar/ │ │ └── StatusBarView.swift │ └── ViewModel/ │ ├── Parser/ │ │ ├── CityWeatherResultParser.swift │ │ └── ZipCodeWeatherResultParser.swift │ ├── Repository/ │ │ ├── Coordinates/ │ │ │ ├── LocationCoordinatesWeatherRepository.swift │ │ │ ├── LocationParser.swift │ │ │ └── LocationValidator.swift │ │ ├── System/ │ │ │ ├── SystemLocationFetcher.swift │ │ │ ├── SystemLocationWeatherRepository.swift │ │ │ └── Task+Retry.swift │ │ ├── WeatherRepositoryFactory.swift │ │ ├── WeatherRepositoryType.swift │ │ ├── WeatherURLBuilder.swift │ │ └── WeatherValidatorType.swift │ ├── WeatherDataFormatter.swift │ ├── WeatherViewModel.swift │ └── WeatherViewModelType.swift ├── DatWeatherDoe.xcodeproj/ │ ├── project.pbxproj │ ├── project.xcworkspace/ │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata/ │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm/ │ │ └── Package.resolved │ └── xcshareddata/ │ └── xcschemes/ │ └── DatWeatherDoe.xcscheme ├── DatWeatherDoeTests/ │ ├── API/ │ │ └── Repository/ │ │ ├── Location/ │ │ │ └── Coordinates/ │ │ │ └── LocationValidatorTests.swift │ │ └── WeatherURLBuilderTests.swift │ ├── DatWeatherDoe.xctestplan │ └── UI/ │ └── Configure/ │ └── Options/ │ ├── RefreshIntervalTests.swift │ ├── TemperatureUnitTests.swift │ └── WeatherSourceTests.swift ├── LICENSE └── README.md ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms github: inderdhir ================================================ FILE: .github/workflows/lint.yml ================================================ name: SwiftLint on: pull_request: paths: - '.github/workflows/swiftlint.yml' - '.swiftlint.yml' - '**/*.swift' jobs: lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 - name: GitHub Action for SwiftLint (Different working directory) uses: norio-nomura/action-swiftlint@3.2.1 env: WORKING_DIRECTORY: Source ================================================ FILE: .gitignore ================================================ # Xcode .DS_Store # Backup files *~ # JetBrains .idea/ ## Build generated build/ DerivedData ## Various settings *.pbxuser !default.pbxuser *.mode1v3 !default.mode1v3 *.mode2v3 !default.mode2v3 *.perspectivev3 !default.perspectivev3 xcuserdata ## Other *.xccheckout *.moved-aside *.xcuserstate *.xcscmblueprint ## Obj-C/Swift specific *.hmap *.ipa # Thumbnails ._* # Files that might appear in the root of a volume .DocumentRevisions-V100 .fseventsd .Spotlight-V100 .TemporaryItems .Trashes .VolumeIcon.icns # Directories potentially created on remote AFP share .AppleDB .AppleDesktop Network Trash Folder Temporary Items .apdisk The above adds CocoaPods, Pods/ Podfile.lock # Keys DatWeatherDoe/Resources/Keys.plist # Secrets Config.xcconfig ================================================ FILE: .swiftformat ================================================ --disable trailingCommas ================================================ FILE: .swiftlint.yml ================================================ disabled_rules: # rule identifiers to exclude from running - colon - comma - control_statement - trailing_whitespace - vertical_parameter_alignment - opening_brace opt_in_rules: # some rules are only opt-in - empty_count # Find all the available rules by running: # swiftlint rules included: # paths to include during linting. `--path` is ignored if present. - DatWeatherDoe excluded: # paths to ignore during linting. Takes precedence over `included`. - Pods # configurable rules can be customized from this configuration file # configurable rules can be customized from this configuration file # binary rules can set their severity level cyclomatic_complexity: ignores_case_statements: true force_cast: warning # implicitly force_try: severity: warning # explicitly # rules that have both warning and error levels, can set just the warning level # implicitly line_length: 110 # they can set both implicitly with an array type_body_length: - 300 # warning - 400 # error # or they can set both explicitly file_length: warning: 500 error: 1200 # naming rules can set warnings/errors for min_length and max_length # additionally they can set excluded names type_name: min_length: 4 # only warning max_length: # warning and error warning: 40 error: 50 excluded: iPhone # excluded via string identifier_name: min_length: # only min_length error: 2 # only error excluded: # excluded via string array - id - URL - GlobalAPIKey function_body_length: warning: 50 error: 100 reporter: "xcode" # reporter type (xcode, json, csv, checkstyle, junit, html, emoji) ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing Contributions / pull requests are welcome! Please note that the goal of this project to provide a lightweight menu bar app for weather at a glance on macOS (similar to weather app indicators on Ubuntu). **MacOS 11 provides a weather widget that can be used in conjunction with this app so a macOS widget is NOT on the roadmap.** ================================================ FILE: DatWeatherDoe/API/NetworkClient.swift ================================================ // // NetworkClient.swift // DatWeatherDoe // // Created by Inder Dhir on 1/14/22. // Copyright © 2022 Inder Dhir. All rights reserved. // import Foundation protocol NetworkClientType { func performRequest(url: URL) async throws -> Data } final class NetworkClient: NetworkClientType { func performRequest(url: URL) async throws -> Data { do { return try await URLSession.shared.data(from: url).0 } catch { throw WeatherError.networkError } } } ================================================ FILE: DatWeatherDoe/API/Response/AirQuality.swift ================================================ // // AirQuality.swift // DatWeatherDoe // // Created by Inder Dhir on 7/1/24. // Copyright © 2024 Inder Dhir. All rights reserved. // import Foundation // US - EPA standard enum AirQualityIndex: Int, Decodable { case good = 1 case moderate = 2 case unhealthyForSensitive = 3 case unhealthy = 4 case veryUnhealthy = 5 case hazardous = 6 var description: String { switch self { case .good: String(localized: "Good") case .moderate: String(localized: "Moderate") case .unhealthyForSensitive: String(localized: "Unhealthy for sensitive groups") case .unhealthy: String(localized: "Unhealthy") case .veryUnhealthy: String(localized: "Very unhealthy") case .hazardous: String(localized: "Hazardous") } } } ================================================ FILE: DatWeatherDoe/API/Response/ForecastData.swift ================================================ // // ForecastData.swift // DatWeatherDoe // // Created by Inder Dhir on 6/23/24. // Copyright © 2024 Inder Dhir. All rights reserved. // import Foundation struct Forecast: Decodable { let dayDataArr: [ForecastDayData] private enum CodingKeys: String, CodingKey { case dayDataArr = "forecastday" } } struct ForecastDayData: Decodable { let temperatureData: ForecastTemperatureData let astro: SunriseSunsetData let hour: [HourlyUVIndex] private enum CodingKeys: String, CodingKey { case temperatureData = "day" case astro case hour } } struct HourlyUVIndex: Decodable { // swiftlint:disable:next identifier_name let uv: Double } ================================================ FILE: DatWeatherDoe/API/Response/ForecastTemperatureData.swift ================================================ // // ForecastTemperatureData.swift // DatWeatherDoe // // Created by Inder Dhir on 6/23/24. // Copyright © 2024 Inder Dhir. All rights reserved. // import Foundation struct ForecastTemperatureData: Decodable { let maxTempC: Double let maxTempF: Double let minTempC: Double let minTempF: Double private enum CodingKeys: String, CodingKey { case maxTempC = "maxtemp_c" case maxTempF = "maxtemp_f" case minTempC = "mintemp_c" case minTempF = "mintemp_f" } } ================================================ FILE: DatWeatherDoe/API/Response/SunriseSunsetData.swift ================================================ // // SunriseSunsetData.swift // DatWeatherDoe // // Created by Inder Dhir on 6/22/24. // Copyright © 2024 Inder Dhir. All rights reserved. // import Foundation struct SunriseSunsetData: Decodable { let sunrise: String let sunset: String } ================================================ FILE: DatWeatherDoe/API/Response/TemperatureData.swift ================================================ // // TemperatureData.swift // DatWeatherDoe // // Created by Inder Dhir on 6/23/24. // Copyright © 2024 Inder Dhir. All rights reserved. // import Foundation struct TemperatureData: Decodable { let tempCelsius: Double let feelsLikeTempCelsius: Double let tempFahrenheit: Double let feelsLikeTempFahrenheit: Double private enum CodingKeys: String, CodingKey { case tempCelsius = "temp_c" case feelsLikeTempCelsius = "feelslike_c" case tempFahrenheit = "temp_f" case feelsLikeTempFahrenheit = "feelslike_f" } } ================================================ FILE: DatWeatherDoe/API/Response/WeatherAPIResponse.swift ================================================ // // WeatherAPIResponse.swift // DatWeatherDoe // // Created by Inder Dhir on 2/3/18. // Copyright © 2018 Inder Dhir. All rights reserved. // import Foundation struct WeatherAPIResponse: Decodable { let locationName: String let temperatureData: TemperatureData let isDay: Bool let weatherConditionCode: Int let humidity: Int let windData: WindData let uvIndex: Double let forecastDayData: ForecastDayData let airQualityIndex: AirQualityIndex private enum RootKeys: String, CodingKey { case location, current, forecast } private enum LocationKeys: String, CodingKey { case name } private enum CurrentKeys: String, CodingKey { case isDay = "is_day" case condition, humidity case airQuality = "air_quality" case uvIndex = "uv" } private enum WeatherConditionKeys: String, CodingKey { case code } private enum ForecastKeys: String, CodingKey { case forecastDay = "forecastday" } private enum ForecastDayKeys: String, CodingKey { case day, astro } private enum AirQualityKeys: String, CodingKey { case usEpaIndex = "us-epa-index" } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: RootKeys.self) let locationContainer = try container.nestedContainer(keyedBy: LocationKeys.self, forKey: .location) locationName = try locationContainer.decode(String.self, forKey: .name) temperatureData = try container.decode(TemperatureData.self, forKey: .current) let currentContainer = try container.nestedContainer(keyedBy: CurrentKeys.self, forKey: .current) let isDayInt = try currentContainer.decode(Int.self, forKey: .isDay) isDay = isDayInt > 0 let weatherConditionContainer = try currentContainer.nestedContainer( keyedBy: WeatherConditionKeys.self, forKey: .condition ) weatherConditionCode = try weatherConditionContainer.decode(Int.self, forKey: .code) humidity = try currentContainer.decode(Int.self, forKey: .humidity) windData = try container.decode(WindData.self, forKey: .current) uvIndex = try currentContainer.decode(Double.self, forKey: .uvIndex) let forecast = try container.decode(Forecast.self, forKey: .forecast) if let dayData = forecast.dayDataArr.first { forecastDayData = dayData } else { throw DecodingError.dataCorruptedError( forKey: .forecast, in: container, debugDescription: "Missing forecast day data" ) } let airQualityContainer = try currentContainer.nestedContainer(keyedBy: AirQualityKeys.self, forKey: .airQuality) airQualityIndex = try airQualityContainer.decode(AirQualityIndex.self, forKey: .usEpaIndex) } init( locationName: String, temperatureData: TemperatureData, isDay: Bool, weatherConditionCode: Int, humidity: Int, windData: WindData, uvIndex: Double, forecastDayData: ForecastDayData, airQualityIndex: AirQualityIndex ) { self.locationName = locationName self.temperatureData = temperatureData self.isDay = isDay self.weatherConditionCode = weatherConditionCode self.humidity = humidity self.windData = windData self.uvIndex = uvIndex self.forecastDayData = forecastDayData self.airQualityIndex = airQualityIndex } // hour = [0-23] func getHourlyUVIndex(hour: Int) -> Double { forecastDayData.hour[safe: hour]?.uv ?? uvIndex } } private extension Array { subscript(safe index: Index) -> Element? { indices ~= index ? self[index] : nil } } ================================================ FILE: DatWeatherDoe/API/Response/WeatherAPIResponseParser.swift ================================================ // // WeatherAPIResponseParser.swift // DatWeatherDoe // // Created by Inder Dhir on 1/10/22. // Copyright © 2022 Inder Dhir. All rights reserved. // import Foundation protocol WeatherAPIResponseParserType { func parse(_ data: Data) throws -> WeatherAPIResponse } final class WeatherAPIResponseParser: WeatherAPIResponseParserType { func parse(_ data: Data) throws -> WeatherAPIResponse { try JSONDecoder().decode(WeatherAPIResponse.self, from: data) } } ================================================ FILE: DatWeatherDoe/API/Response/WindData.swift ================================================ // // WindData.swift // DatWeatherDoe // // Created by Inder Dhir on 6/23/24. // Copyright © 2024 Inder Dhir. All rights reserved. // import Foundation struct WindData: Decodable { let speedMph: Double let degrees: Int let direction: String private enum CodingKeys: String, CodingKey { case speedMph = "wind_mph" case degrees = "wind_degree" case direction = "wind_dir" } } ================================================ FILE: DatWeatherDoe/API/WeatherData.swift ================================================ // // WeatherData.swift // DatWeatherDoe // // Created by Inder Dhir on 5/22/21. // Copyright © 2021 Inder Dhir. All rights reserved. // import Foundation struct WeatherData { let showWeatherIcon: Bool let textualRepresentation: String? let weatherCondition: WeatherCondition let response: WeatherAPIResponse } ================================================ FILE: DatWeatherDoe/API/WeatherError.swift ================================================ // // WeatherError.swift // DatWeatherDoe // // Created by Inder Dhir on 5/22/21. // Copyright © 2021 Inder Dhir. All rights reserved. // import Foundation enum WeatherError: LocalizedError { case unableToConstructUrl case locationError case latLongIncorrect case networkError var errorDescription: String? { switch self { case .unableToConstructUrl: "Unable to construct URL" case .locationError: String(localized: "❗️Location") case .latLongIncorrect: String(localized: "❗️Lat/Long") case .networkError: "🖧" } } } ================================================ FILE: DatWeatherDoe/Config/APIKeyParser.swift ================================================ // // APIKeyParser.swift // DatWeatherDoe // // Created by Inder Dhir on 1/11/22. // Copyright © 2022 Inder Dhir. All rights reserved. // import Foundation final class APIKeyParser { func parse() -> String { guard let apiKey = Bundle.main.infoDictionary?["WEATHER_API_KEY"] as? String else { fatalError("Unable to find OPENWEATHERMAP_APP_ID in `Config.xcconfig`") } return apiKey } } ================================================ FILE: DatWeatherDoe/Config/ConfigManager.swift ================================================ // // ConfigManager.swift // DatWeatherDoe // // Created by Inder Dhir on 1/29/16. // Copyright © 2016 Inder Dhir. All rights reserved. // import Foundation import SwiftUI protocol ConfigManagerType: AnyObject { var measurementUnit: String { get set } var weatherSource: String { get set } var weatherSourceText: String { get set } var refreshInterval: TimeInterval { get set } var isShowingWeatherIcon: Bool { get set } var isShowingHumidity: Bool { get set } var isShowingUVIndex: Bool { get set } var isRoundingOffData: Bool { get set } var isUnitLetterOff: Bool { get set } var isUnitSymbolOff: Bool { get set } var valueSeparator: String { get set } var isWeatherConditionAsTextEnabled: Bool { get set } var weatherConditionPosition: String { get set } func updateWeatherSource(_ source: WeatherSource, sourceText: String) func setConfigOptions(_ options: ConfigOptions) var parsedMeasurementUnit: MeasurementUnit { get } } final class ConfigManager: ConfigManagerType { @AppStorage("measurementUnit") public var measurementUnit = MeasurementUnit.imperial.rawValue @AppStorage("weatherSource") public var weatherSource = WeatherSource.location.rawValue @AppStorage("weatherSourceText") public var weatherSourceText = "" @AppStorage("refreshInterval") public var refreshInterval = RefreshInterval.fifteenMinutes.rawValue @AppStorage("isShowingWeatherIcon") public var isShowingWeatherIcon = true @AppStorage("isShowingHumidity") public var isShowingHumidity = false @AppStorage("isShowingUVIndex") public var isShowingUVIndex = false @AppStorage("isRoundingOffData") public var isRoundingOffData = false @AppStorage("isUnitLetterOff") public var isUnitLetterOff = false @AppStorage("isUnitSymbolOff") public var isUnitSymbolOff = false @AppStorage("valueSeparator") public var valueSeparator = "\u{007C}" @AppStorage("isWeatherConditionAsTextEnabled") public var isWeatherConditionAsTextEnabled = false @AppStorage("weatherConditionPosition") public var weatherConditionPosition = WeatherConditionPosition.beforeTemperature.rawValue func updateWeatherSource(_ source: WeatherSource, sourceText: String) { weatherSource = source.rawValue weatherSourceText = source == .location ? "" : sourceText } func setConfigOptions(_ options: ConfigOptions) { refreshInterval = options.refreshInterval.rawValue isShowingHumidity = options.isShowingHumidity isShowingUVIndex = options.isShowingUVIndex isRoundingOffData = options.isRoundingOffData isUnitLetterOff = options.isUnitLetterOff isUnitSymbolOff = options.isUnitSymbolOff valueSeparator = options.valueSeparator isWeatherConditionAsTextEnabled = options.isWeatherConditionAsTextEnabled } var parsedMeasurementUnit: MeasurementUnit { MeasurementUnit(rawValue: measurementUnit) ?? .imperial } } ================================================ FILE: DatWeatherDoe/Config/ConfigOptions.swift ================================================ // // ConfigOptions.swift // DatWeatherDoe // // Created by Inder Dhir on 8/3/23. // Copyright © 2023 Inder Dhir. All rights reserved. // import Foundation struct ConfigOptions { let refreshInterval: RefreshInterval let isShowingHumidity: Bool let isShowingUVIndex: Bool let isRoundingOffData: Bool let isUnitLetterOff: Bool let isUnitSymbolOff: Bool let valueSeparator: String let isWeatherConditionAsTextEnabled: Bool } ================================================ FILE: DatWeatherDoe/DatWeatherDoeApp.swift ================================================ // // DatWeatherDoeApp.swift // DatWeatherDoe // // Created by Inder Dhir on 6/17/24. // Copyright © 2024 Inder Dhir. All rights reserved. // import MenuBarExtraAccess import OSLog import SwiftUI @main struct DatWeatherDoeApp: App { @State private var configManager: ConfigManager @ObservedObject private var configureViewModel = ConfigureViewModel(configManager: ConfigManager()) @ObservedObject private var viewModel: WeatherViewModel @State private var isMenuPresented = false @State private var statusItem: NSStatusItem? init() { configManager = ConfigManager() let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "bundleID", category: "main") viewModel = WeatherViewModel( locationFetcher: SystemLocationFetcher(logger: logger), weatherFactory: WeatherRepositoryFactory( appId: APIKeyParser().parse(), networkClient: NetworkClient(), logger: logger ), configManager: ConfigManager(), logger: logger ) viewModel.setup(with: WeatherDataFormatter(configManager: configManager)) } var body: some Scene { MenuBarExtra( content: { MenuView( viewModel: viewModel, configureViewModel: configureViewModel, onSeeWeather: { viewModel.seeForecastForCurrentCity() closePopover() }, onRefresh: { viewModel.getUpdatedWeatherAfterRefresh() closePopover() }, onSave: { closePopover() } ) }, label: { StatusBarView(weatherResult: viewModel.weatherResult) .onAppear { viewModel.getUpdatedWeatherAfterRefresh() } } ) .menuBarExtraAccess(isPresented: $isMenuPresented) { statusItem in self.statusItem = statusItem } .onChange(of: isMenuPresented) { newValue in if !newValue { configureViewModel.saveConfig() viewModel.getUpdatedWeatherAfterRefresh() } } .windowStyle(.hiddenTitleBar) .menuBarExtraStyle(.window) } private func closePopover() { statusItem?.togglePresented() } } ================================================ FILE: DatWeatherDoe/Localization/en.xcloc/Localized Contents/en.xliff ================================================
DatWeatherDoe DatWeatherDoe Bundle name Copyright © 2016 Inder Dhir. All rights reserved. Copyright © 2016 Inder Dhir. All rights reserved. Copyright (human-readable) DatWeatherDoe optionally uses your current location to get the weather DatWeatherDoe optionally uses your current location to get the weather Privacy - Location When In Use Usage Description
Customize Toolbar… Customize Toolbar… Class = "NSMenuItem"; title = "Customize Toolbar…"; ObjectID = "1UK-8n-QPP"; DatWeatherDoe DatWeatherDoe Class = "NSMenuItem"; title = "DatWeatherDoe"; ObjectID = "1Xt-HY-uBw"; Find Find Class = "NSMenu"; title = "Find"; ObjectID = "1b7-l0-nxx"; Lower Lower Class = "NSMenuItem"; title = "Lower"; ObjectID = "1tx-W0-xDw"; Raise Raise Class = "NSMenuItem"; title = "Raise"; ObjectID = "2h7-ER-AoG"; Transformations Transformations Class = "NSMenuItem"; title = "Transformations"; ObjectID = "2oI-Rn-ZJC"; Spelling Spelling Class = "NSMenu"; title = "Spelling"; ObjectID = "3IN-sU-3Bg"; Use Default Use Default Class = "NSMenuItem"; title = "Use Default"; ObjectID = "3Om-Ey-2VK"; Speech Speech Class = "NSMenu"; title = "Speech"; ObjectID = "3rS-ZA-NoH"; Find Find Class = "NSMenuItem"; title = "Find"; ObjectID = "4EN-yA-p0u"; Quit DatWeatherDoe Quit DatWeatherDoe Class = "NSMenuItem"; title = "Quit DatWeatherDoe"; ObjectID = "4sb-4s-VLi"; Edit Edit Class = "NSMenuItem"; title = "Edit"; ObjectID = "5QF-Oa-p0T"; Copy Style Copy Style Class = "NSMenuItem"; title = "Copy Style"; ObjectID = "5Vv-lz-BsD"; About DatWeatherDoe About DatWeatherDoe Class = "NSMenuItem"; title = "About DatWeatherDoe"; ObjectID = "5kV-Vb-QxS"; Redo Redo Class = "NSMenuItem"; title = "Redo"; ObjectID = "6dh-zS-Vam"; Writing Direction Writing Direction Class = "NSMenu"; title = "Writing Direction"; ObjectID = "8mr-sm-Yjd"; Substitutions Substitutions Class = "NSMenuItem"; title = "Substitutions"; ObjectID = "9ic-FL-obx"; Smart Copy/Paste Smart Copy/Paste Class = "NSMenuItem"; title = "Smart Copy/Paste"; ObjectID = "9yt-4B-nSM"; Tighten Tighten Class = "NSMenuItem"; title = "Tighten"; ObjectID = "46P-cB-AYj"; Correct Spelling Automatically Correct Spelling Automatically Class = "NSMenuItem"; title = "Correct Spelling Automatically"; ObjectID = "78Y-hA-62v"; Main Menu Main Menu Class = "NSMenu"; title = "Main Menu"; ObjectID = "AYu-sK-qS6"; Preferences… Preferences… Class = "NSMenuItem"; title = "Preferences…"; ObjectID = "BOF-NM-1cW"; Left to Right Left to Right Class = "NSMenuItem"; title = "\tLeft to Right"; ObjectID = "BgM-ve-c93"; Save As… Save As… Class = "NSMenuItem"; title = "Save As…"; ObjectID = "Bw7-FT-i3A"; Close Close Class = "NSMenuItem"; title = "Close"; ObjectID = "DVo-aG-piG"; Spelling and Grammar Spelling and Grammar Class = "NSMenuItem"; title = "Spelling and Grammar"; ObjectID = "Dv1-io-Yv7"; Help Help Class = "NSMenu"; title = "Help"; ObjectID = "F2S-fz-NVQ"; DatWeatherDoe Help DatWeatherDoe Help Class = "NSMenuItem"; title = "DatWeatherDoe Help"; ObjectID = "FKE-Sm-Kum"; Text Text Class = "NSMenuItem"; title = "Text"; ObjectID = "Fal-I4-PZk"; Substitutions Substitutions Class = "NSMenu"; title = "Substitutions"; ObjectID = "FeM-D8-WVr"; Bold Bold Class = "NSMenuItem"; title = "Bold"; ObjectID = "GB9-OM-e27"; Format Format Class = "NSMenu"; title = "Format"; ObjectID = "GEO-Iw-cKr"; Use Default Use Default Class = "NSMenuItem"; title = "Use Default"; ObjectID = "GUa-eO-cwY"; Font Font Class = "NSMenuItem"; title = "Font"; ObjectID = "Gi5-1S-RQB"; Writing Direction Writing Direction Class = "NSMenuItem"; title = "Writing Direction"; ObjectID = "H1b-Si-o9J"; View View Class = "NSMenuItem"; title = "View"; ObjectID = "H8h-7b-M4v"; Text Replacement Text Replacement Class = "NSMenuItem"; title = "Text Replacement"; ObjectID = "HFQ-gK-NFA"; Show Spelling and Grammar Show Spelling and Grammar Class = "NSMenuItem"; title = "Show Spelling and Grammar"; ObjectID = "HFo-cy-zxI"; View View Class = "NSMenu"; title = "View"; ObjectID = "HyV-fh-RgO"; Subscript Subscript Class = "NSMenuItem"; title = "Subscript"; ObjectID = "I0S-gh-46l"; Open… Open… Class = "NSMenuItem"; title = "Open…"; ObjectID = "IAo-SY-fd9"; Justify Justify Class = "NSMenuItem"; title = "Justify"; ObjectID = "J5U-5w-g23"; Use None Use None Class = "NSMenuItem"; title = "Use None"; ObjectID = "J7y-lM-qPV"; Revert to Saved Revert to Saved Class = "NSMenuItem"; title = "Revert to Saved"; ObjectID = "KaW-ft-85H"; Show All Show All Class = "NSMenuItem"; title = "Show All"; ObjectID = "Kd2-mp-pUS"; Bring All to Front Bring All to Front Class = "NSMenuItem"; title = "Bring All to Front"; ObjectID = "LE2-aR-0XJ"; Paste Ruler Paste Ruler Class = "NSMenuItem"; title = "Paste Ruler"; ObjectID = "LVM-kO-fVI"; Left to Right Left to Right Class = "NSMenuItem"; title = "\tLeft to Right"; ObjectID = "Lbh-J2-qVU"; Copy Ruler Copy Ruler Class = "NSMenuItem"; title = "Copy Ruler"; ObjectID = "MkV-Pr-PK5"; Services Services Class = "NSMenuItem"; title = "Services"; ObjectID = "NMo-om-nkz"; Default Default Class = "NSMenuItem"; title = "\tDefault"; ObjectID = "Nop-cj-93Q"; Minimize Minimize Class = "NSMenuItem"; title = "Minimize"; ObjectID = "OY7-WF-poV"; Baseline Baseline Class = "NSMenuItem"; title = "Baseline"; ObjectID = "OaQ-X3-Vso"; Hide DatWeatherDoe Hide DatWeatherDoe Class = "NSMenuItem"; title = "Hide DatWeatherDoe"; ObjectID = "Olw-nP-bQN"; Find Previous Find Previous Class = "NSMenuItem"; title = "Find Previous"; ObjectID = "OwM-mh-QMV"; Stop Speaking Stop Speaking Class = "NSMenuItem"; title = "Stop Speaking"; ObjectID = "Oyz-dy-DGm"; Bigger Bigger Class = "NSMenuItem"; title = "Bigger"; ObjectID = "Ptp-SP-VEL"; Show Fonts Show Fonts Class = "NSMenuItem"; title = "Show Fonts"; ObjectID = "Q5e-8K-NDq"; DatWeatherDoe DatWeatherDoe Class = "NSWindow"; title = "DatWeatherDoe"; ObjectID = "QvC-M9-y7g"; Zoom Zoom Class = "NSMenuItem"; title = "Zoom"; ObjectID = "R4o-n2-Eq4"; Right to Left Right to Left Class = "NSMenuItem"; title = "\tRight to Left"; ObjectID = "RB4-Sm-HuC"; Superscript Superscript Class = "NSMenuItem"; title = "Superscript"; ObjectID = "Rqc-34-cIF"; Select All Select All Class = "NSMenuItem"; title = "Select All"; ObjectID = "Ruw-6m-B2m"; Jump to Selection Jump to Selection Class = "NSMenuItem"; title = "Jump to Selection"; ObjectID = "S0p-oC-mLd"; Window Window Class = "NSMenu"; title = "Window"; ObjectID = "Td7-aD-5lo"; Capitalize Capitalize Class = "NSMenuItem"; title = "Capitalize"; ObjectID = "UEZ-Bs-lqG"; Center Center Class = "NSMenuItem"; title = "Center"; ObjectID = "VIY-Ag-zcb"; Hide Others Hide Others Class = "NSMenuItem"; title = "Hide Others"; ObjectID = "Vdr-fp-XzO"; Italic Italic Class = "NSMenuItem"; title = "Italic"; ObjectID = "Vjx-xi-njq"; Edit Edit Class = "NSMenu"; title = "Edit"; ObjectID = "W48-6f-4Dl"; Underline Underline Class = "NSMenuItem"; title = "Underline"; ObjectID = "WRG-CD-K1S"; New New Class = "NSMenuItem"; title = "New"; ObjectID = "Was-JA-tGl"; Paste and Match Style Paste and Match Style Class = "NSMenuItem"; title = "Paste and Match Style"; ObjectID = "WeT-3V-zwk"; Find… Find… Class = "NSMenuItem"; title = "Find…"; ObjectID = "Xz5-n4-O0W"; Find and Replace… Find and Replace… Class = "NSMenuItem"; title = "Find and Replace…"; ObjectID = "YEy-JH-Tfz"; Default Default Class = "NSMenuItem"; title = "\tDefault"; ObjectID = "YGs-j5-SAR"; Start Speaking Start Speaking Class = "NSMenuItem"; title = "Start Speaking"; ObjectID = "Ynk-f8-cLZ"; Align Left Align Left Class = "NSMenuItem"; title = "Align Left"; ObjectID = "ZM1-6Q-yy1"; Paragraph Paragraph Class = "NSMenuItem"; title = "Paragraph"; ObjectID = "ZvO-Gk-QUH"; Print… Print… Class = "NSMenuItem"; title = "Print…"; ObjectID = "aTl-1u-JFS"; Window Window Class = "NSMenuItem"; title = "Window"; ObjectID = "aUF-d1-5bR"; Font Font Class = "NSMenu"; title = "Font"; ObjectID = "aXa-aM-Jaq"; Use Default Use Default Class = "NSMenuItem"; title = "Use Default"; ObjectID = "agt-UL-0e3"; Show Colors Show Colors Class = "NSMenuItem"; title = "Show Colors"; ObjectID = "bgn-CT-cEk"; File File Class = "NSMenu"; title = "File"; ObjectID = "bib-Uj-vzu"; Use Selection for Find Use Selection for Find Class = "NSMenuItem"; title = "Use Selection for Find"; ObjectID = "buJ-ug-pKt"; Transformations Transformations Class = "NSMenu"; title = "Transformations"; ObjectID = "c8a-y6-VQd"; Use None Use None Class = "NSMenuItem"; title = "Use None"; ObjectID = "cDB-IK-hbR"; Selection Selection Class = "NSMenuItem"; title = "Selection"; ObjectID = "cqv-fj-IhA"; Smart Links Smart Links Class = "NSMenuItem"; title = "Smart Links"; ObjectID = "cwL-P1-jid"; Make Lower Case Make Lower Case Class = "NSMenuItem"; title = "Make Lower Case"; ObjectID = "d9M-CD-aMd"; Text Text Class = "NSMenu"; title = "Text"; ObjectID = "d9c-me-L2H"; File File Class = "NSMenuItem"; title = "File"; ObjectID = "dMs-cI-mzQ"; Undo Undo Class = "NSMenuItem"; title = "Undo"; ObjectID = "dRJ-4n-Yzg"; Paste Paste Class = "NSMenuItem"; title = "Paste"; ObjectID = "gVA-U4-sdL"; Smart Quotes Smart Quotes Class = "NSMenuItem"; title = "Smart Quotes"; ObjectID = "hQb-2v-fYv"; Check Document Now Check Document Now Class = "NSMenuItem"; title = "Check Document Now"; ObjectID = "hz2-CU-CR7"; Services Services Class = "NSMenu"; title = "Services"; ObjectID = "hz9-B4-Xy5"; Smaller Smaller Class = "NSMenuItem"; title = "Smaller"; ObjectID = "i1d-Er-qST"; Baseline Baseline Class = "NSMenu"; title = "Baseline"; ObjectID = "ijk-EB-dga"; Kern Kern Class = "NSMenuItem"; title = "Kern"; ObjectID = "jBQ-r6-VK2"; Right to Left Right to Left Class = "NSMenuItem"; title = "\tRight to Left"; ObjectID = "jFq-tB-4Kx"; Format Format Class = "NSMenuItem"; title = "Format"; ObjectID = "jxT-CU-nIS"; Check Grammar With Spelling Check Grammar With Spelling Class = "NSMenuItem"; title = "Check Grammar With Spelling"; ObjectID = "mK6-2p-4JG"; Ligatures Ligatures Class = "NSMenuItem"; title = "Ligatures"; ObjectID = "o6e-r0-MWq"; Open Recent Open Recent Class = "NSMenu"; title = "Open Recent"; ObjectID = "oas-Oc-fiZ"; Loosen Loosen Class = "NSMenuItem"; title = "Loosen"; ObjectID = "ogc-rX-tC1"; Delete Delete Class = "NSMenuItem"; title = "Delete"; ObjectID = "pa3-QI-u2k"; Save… Save… Class = "NSMenuItem"; title = "Save…"; ObjectID = "pxx-59-PXV"; Find Next Find Next Class = "NSMenuItem"; title = "Find Next"; ObjectID = "q09-fT-Sye"; Page Setup… Page Setup… Class = "NSMenuItem"; title = "Page Setup…"; ObjectID = "qIS-W8-SiK"; Check Spelling While Typing Check Spelling While Typing Class = "NSMenuItem"; title = "Check Spelling While Typing"; ObjectID = "rbD-Rh-wIN"; Smart Dashes Smart Dashes Class = "NSMenuItem"; title = "Smart Dashes"; ObjectID = "rgM-f4-ycn"; Show Toolbar Show Toolbar Class = "NSMenuItem"; title = "Show Toolbar"; ObjectID = "snW-S8-Cw5"; Data Detectors Data Detectors Class = "NSMenuItem"; title = "Data Detectors"; ObjectID = "tRr-pd-1PS"; Open Recent Open Recent Class = "NSMenuItem"; title = "Open Recent"; ObjectID = "tXI-mr-wws"; Kern Kern Class = "NSMenu"; title = "Kern"; ObjectID = "tlD-Oa-oAM"; DatWeatherDoe DatWeatherDoe Class = "NSMenu"; title = "DatWeatherDoe"; ObjectID = "uQy-DD-JDr"; Cut Cut Class = "NSMenuItem"; title = "Cut"; ObjectID = "uRl-iY-unG"; Paste Style Paste Style Class = "NSMenuItem"; title = "Paste Style"; ObjectID = "vKC-jM-MkH"; Show Ruler Show Ruler Class = "NSMenuItem"; title = "Show Ruler"; ObjectID = "vLm-3I-IUL"; Clear Menu Clear Menu Class = "NSMenuItem"; title = "Clear Menu"; ObjectID = "vNY-rz-j42"; Make Upper Case Make Upper Case Class = "NSMenuItem"; title = "Make Upper Case"; ObjectID = "vmV-6d-7jI"; Ligatures Ligatures Class = "NSMenu"; title = "Ligatures"; ObjectID = "w0m-vy-SC9"; Align Right Align Right Class = "NSMenuItem"; title = "Align Right"; ObjectID = "wb2-vD-lq4"; Help Help Class = "NSMenuItem"; title = "Help"; ObjectID = "wpr-3q-Mcd"; Copy Copy Class = "NSMenuItem"; title = "Copy"; ObjectID = "x3v-GG-iWU"; Use All Use All Class = "NSMenuItem"; title = "Use All"; ObjectID = "xQD-1f-W4t"; Speech Speech Class = "NSMenuItem"; title = "Speech"; ObjectID = "xrE-MZ-jX0"; Show Substitutions Show Substitutions Class = "NSMenuItem"; title = "Show Substitutions"; ObjectID = "z6F-FW-3nz";
================================================ FILE: DatWeatherDoe/Localization/en.xcloc/Source Contents/DatWeatherDoe/Resources/en.lproj/InfoPlist.strings ================================================ /* Bundle name */ "CFBundleName" = "DatWeatherDoe"; /* Copyright (human-readable) */ "NSHumanReadableCopyright" = "Copyright © 2016 Inder Dhir. All rights reserved."; /* Privacy - Location When In Use Usage Description */ "NSLocationWhenInUseUsageDescription" = "DatWeatherDoe optionally uses your current location to get the weather"; ================================================ FILE: DatWeatherDoe/Localization/en.xcloc/Source Contents/DatWeatherDoe/UI/Base.lproj/MainMenu.xib ================================================ Default Left to Right Right to Left Default Left to Right Right to Left ================================================ FILE: DatWeatherDoe/Localization/en.xcloc/contents.json ================================================ { "developmentRegion" : "en", "project" : "DatWeatherDoe.xcodeproj", "targetLocale" : "en", "toolInfo" : { "toolBuildNumber" : "12E262", "toolID" : "com.apple.dt.xcode", "toolName" : "Xcode", "toolVersion" : "12.5" }, "version" : "1.0" } ================================================ FILE: DatWeatherDoe/Reachability/WeatherReachability.swift ================================================ // // WeatherReachability.swift // DatWeatherDoe // // Created by Inder Dhir on 1/11/22. // Copyright © 2022 Inder Dhir. All rights reserved. // import OSLog import Reachability final class NetworkReachability { private let logger: Logger private var reachability: Reachability? private var retryWhenReachable = false init( logger: Logger, onBecomingReachable: @escaping () -> Void ) { self.logger = logger setup(callback: onBecomingReachable) } private func setup(callback: @escaping () -> Void) { do { reachability = try Reachability() try reachability?.startNotifier() updateReachabilityWhenReachable(callback: callback) updateReachabilityWhenUnreachable() } catch { logger.error("Reachability error!") } } private func updateReachabilityWhenReachable(callback: @escaping () -> Void) { reachability?.whenReachable = { [weak self] _ in self?.logger.debug("Reachability status: Reachable") if self?.retryWhenReachable == true { self?.retryWhenReachable = false callback() } } } private func updateReachabilityWhenUnreachable() { reachability?.whenUnreachable = { [weak self] _ in self?.logger.debug("Reachability status: Unreachable") self?.retryWhenReachable = true } } } ================================================ FILE: DatWeatherDoe/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "filename" : "16.png", "idiom" : "mac", "scale" : "1x", "size" : "16x16" }, { "filename" : "32-1.png", "idiom" : "mac", "scale" : "2x", "size" : "16x16" }, { "filename" : "32.png", "idiom" : "mac", "scale" : "1x", "size" : "32x32" }, { "filename" : "64.png", "idiom" : "mac", "scale" : "2x", "size" : "32x32" }, { "filename" : "128.png", "idiom" : "mac", "scale" : "1x", "size" : "128x128" }, { "filename" : "256-1.png", "idiom" : "mac", "scale" : "2x", "size" : "128x128" }, { "filename" : "256.png", "idiom" : "mac", "scale" : "1x", "size" : "256x256" }, { "filename" : "512-1.png", "idiom" : "mac", "scale" : "2x", "size" : "256x256" }, { "filename" : "512.png", "idiom" : "mac", "scale" : "1x", "size" : "512x512" }, { "filename" : "1024.png", "idiom" : "mac", "scale" : "2x", "size" : "512x512" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: DatWeatherDoe/Resources/Assets.xcassets/Contents.json ================================================ { "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: DatWeatherDoe/Resources/DatWeatherDoe.entitlements ================================================ ================================================ FILE: DatWeatherDoe/Resources/DevelopmentAssets/TestData.swift ================================================ // // TestData.swift // DatWeatherDoe // // Created by Inder Dhir on 6/25/24. // Copyright © 2024 Inder Dhir. All rights reserved. // import Foundation let uvIndicesFor24Hours: [HourlyUVIndex] = { var arr: [HourlyUVIndex] = [] // swiftlint:disable:next identifier_name for i in 1 ... 24 { arr.append(HourlyUVIndex(uv: 0)) } return arr }() let response = WeatherAPIResponse( locationName: "New York City", temperatureData: .init( tempCelsius: 31.1, feelsLikeTempCelsius: 29.1, tempFahrenheit: 88.0, feelsLikeTempFahrenheit: 84.4 ), isDay: true, weatherConditionCode: 1000, humidity: 45, windData: .init(speedMph: 12.3, degrees: 305, direction: "NW"), uvIndex: 7.0, forecastDayData: .init( temperatureData: .init( maxTempC: 32.8, maxTempF: 91.0, minTempC: 20.6, minTempF: 69.2 ), astro: .init(sunrise: "05:26 AM", sunset: "08:31 PM"), hour: uvIndicesFor24Hours ), airQualityIndex: .good ) ================================================ FILE: DatWeatherDoe/Resources/Info.plist ================================================ WEATHER_API_KEY ${WEATHER_API_KEY} CFBundleDevelopmentRegion en CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIconFile CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 CFBundleName $(PRODUCT_NAME) CFBundlePackageType APPL CFBundleShortVersionString $(MARKETING_VERSION) CFBundleSignature ???? CFBundleVersion $(CURRENT_PROJECT_VERSION) LSApplicationCategoryType public.app-category.weather LSMinimumSystemVersion $(MACOSX_DEPLOYMENT_TARGET) LSUIElement NSHumanReadableCopyright Copyright © 2016 Inder Dhir. All rights reserved. NSLocationWhenInUseUsageDescription DatWeatherDoe optionally uses your current location to get the weather NSMainNibFile MainMenu NSPrincipalClass NSApplication ================================================ FILE: DatWeatherDoe/Resources/Localization/Localizable.xcstrings ================================================ { "sourceLanguage" : "en", "strings" : { "" : { }, "[latitude],[longitude]" : { "comment" : "Placeholder hint for entering Lat/Long", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "[Breite],[Länge]" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "[latitude],[longitude]" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "[latitudine],[longitudine]" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "[緯度],[経度]" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "[纬度]、[经度]" } } } }, "❗️City" : { "comment" : "City error when fetching weather", "extractionState" : "manual", "localizations" : { "en" : { "stringUnit" : { "state" : "translated", "value" : "❗️City" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "❗️Città" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "市のエラー" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "城市" } } } }, "❗️Lat/Long" : { "comment" : "Lat/Long error when fetching weather", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Fehler mit Breite/Länge" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Erreur de Lat/Lon" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "❗️ Lat/Long" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "緯度/経度のエラー" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "经纬度" } } } }, "❗️Location" : { "comment" : "Location error when fetching weather", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Positionsfehler" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Erreur d’emplacement" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "❗️Posizione" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "所在地のエラー" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "位置" } } } }, "5 min" : { "comment" : "5 min refresh interval", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "5 Min" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "5 min" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "5 min" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "5分" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "5分钟" } } } }, "15 min" : { "comment" : "15 min refresh interval", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "15 Min" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "15 min" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "15 min" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "15分" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "15分钟" } } } }, "30 min" : { "comment" : "30 min refresh interval", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "30 Min" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "30 min" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "30 min" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "30分" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "30分钟" } } } }, "60 min" : { "comment" : "60 min refresh interval", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "60 Min" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "60 min" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "60 min" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "60分" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "60分钟" } } } }, "After Temperature" : { "comment" : "Weather condition after temperature", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Nach Temperatur" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Après température" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Dopo la temperatura" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "温度後" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "在温度后" } } } }, "All" : { "comment" : "Show all temperature units", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Alle" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Tous" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Entrambe" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "全て" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "全部" } } } }, "AQI" : { "comment" : "Air Quality Index", "extractionState" : "manual", "localizations" : { "ja" : { "stringUnit" : { "state" : "translated", "value" : "空気質指数" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "空气质量" } } }, "shouldTranslate" : false }, "Before Temperature" : { "comment" : "Weather condition before temperature", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Vor der Temperatur" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Avant la température" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Prima della temperatura" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "温度前" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "在温度前" } } } }, "CFBundleName" : { "comment" : "Bundle name", "extractionState" : "manual", "localizations" : { "ja" : { "stringUnit" : { "state" : "translated", "value" : "DatWeatherDoe" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "DatWeatherDoe" } } }, "shouldTranslate" : false }, "Clear" : { "comment" : "Clear at night weather condition", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Klar" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Temps clair" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Sereno" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "クリア" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "晴朗" } } } }, "Cloudy" : { "comment" : "Cloudy weather condition", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Bedeckt" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Nuageux" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Nuvoloso" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "曇り" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "多云" } } } }, "Configure" : { "comment" : "Configure app", "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Konfigurieren" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Configure" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Configurer" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Configura" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "設定する" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "配置" } } } }, "Done" : { "comment" : "Finish configuring app", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Fertig" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Terminé" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Fatto" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "完了" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "完成" } } } }, "Fog" : { "comment" : "Fog weather condition", "localizations" : { "it" : { "stringUnit" : { "state" : "translated", "value" : "Nebbia" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "霧" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "雾" } } } }, "Freezing rain" : { "comment" : "Freezing rain weather condition", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Gefrierender Regen" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Pluie verglaçante" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Nevischio" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "雨氷" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "冻雨" } } } }, "Good" : { "comment" : "Air Quality Index: Good", "extractionState" : "manual", "localizations" : { "it" : { "stringUnit" : { "state" : "translated", "value" : "Buona" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "良好" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "优" } } } }, "Hazardous" : { "comment" : "Air Quality Index: Hazardous", "extractionState" : "manual", "localizations" : { "it" : { "stringUnit" : { "state" : "translated", "value" : "Pericolosa" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "危険" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "良" } } } }, "Heavy rain" : { "comment" : "Heavy rain weather condition", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Starkregen" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Forte pluie" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Pioggia abbondante" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "豪雨" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "大雨" } } } }, "Hide unit ° symbol" : { "localizations" : { "it" : { "stringUnit" : { "state" : "translated", "value" : "Nascondi simbolo °" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "°単位記号を非表示にする" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "隐藏单位符号 °" } } } }, "Hide unit letter" : { "localizations" : { "it" : { "stringUnit" : { "state" : "translated", "value" : "Nascondi unità C" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "単位文字を非表示にする" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "隐藏单位字母 C" } } } }, "Imperial" : { "localizations" : { "it" : { "stringUnit" : { "state" : "translated", "value" : "Imperiale" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "帝国単位" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "英制" } } } }, "Lat/Long" : { "comment" : "Weather based on Lat/Long", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Breite/Länge" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Lat/Long" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Lat/Long" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "緯度/経度" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "纬度/经度" } } } }, "Launch at Login" : { "comment" : "Launch app at login", "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Beim Einloggen starten" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Launch at Login" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Lancer à la connexion" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Avvia al Login" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "ログイン時に起動" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "自动启动" } } } }, "Light rain" : { "comment" : "Light rain weather condition", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Leichter Regen" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Pluie légère" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Lieve pioggia" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "小雨" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "小雨" } } } }, "Location" : { "comment" : "Weather based on location", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Position" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Emplacement" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Posizione corrente" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "所在地" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "定位" } } } }, "Metric" : { "localizations" : { "it" : { "stringUnit" : { "state" : "translated", "value" : "Metrico" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "メートル法" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "公制" } } } }, "Mist" : { "comment" : "Mist weather condition", "localizations" : { "it" : { "stringUnit" : { "state" : "translated", "value" : "Foschia" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "靄" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "薄雾" } } } }, "Moderate" : { "comment" : "Air Quality Index: Moderate", "extractionState" : "manual", "localizations" : { "it" : { "stringUnit" : { "state" : "translated", "value" : "Moderata" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "中等度" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "轻度污染" } } } }, "NSHumanReadableCopyright" : { "comment" : "Copyright (human-readable)", "extractionState" : "manual", "localizations" : { "ja" : { "stringUnit" : { "state" : "translated", "value" : "Copyright © 2016 Inder Dhir. All rights reserved." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "Copyright © 2016 Inder Dhir. Alle Rechte vorbehalten." } } }, "shouldTranslate" : false }, "NSLocationWhenInUseUsageDescription" : { "comment" : "Privacy - Location When In Use Usage Description", "extractionState" : "manual", "localizations" : { "ja" : { "stringUnit" : { "state" : "translated", "value" : "DatWeatherDoeは、リクエストに応じて地理的位置を使用して天気を判断します。" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "DatWeatherDoe根据请求使用您的地理位置来确定天气." } } }, "shouldTranslate" : false }, "Partly cloudy" : { "comment" : "Partly cloudy weather condition", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Wolkig" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Partiellement couvert" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Parzialmente nuvoloso" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "晴れ時々曇り" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "局部多云" } } } }, "Partly cloudy with rain" : { "comment" : "Partly cloudy with rain weather condition", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Bewölkt mit Regen" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Partiellement couvert avec pluie" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Pioggia, parzialmente nuvoloso" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "部分的に曇り雨" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "多云转阴有雨" } } } }, "Quit" : { "comment" : "Quit app", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Beenden" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Quitter" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Esci" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "終了する" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "退出" } } } }, "Refresh" : { "comment" : "Refresh weather", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Aktualisieren" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Rafraîchir" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Aggiorna" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "再読み込みする" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "刷新" } } } }, "Refresh Interval" : { "comment" : "Weather refresh interval", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Aktualisierungsintervall" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Intervalle de rafraîchissement" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Intervallo Aggiornamento" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "再読み込み間隔" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "刷新间隔" } } } }, "Round-off Data" : { "comment" : "Round-off Data (temperature)", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Daten runden" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Arrondir les données" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Arrotonda Dati" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "データを四捨五入する" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "数据四舍五入" } } } }, "See Full Weather" : { "comment" : "See Full Weather", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Komplettes Wetter anzeigen" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Voir la météo complète" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Meteo Completo" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "全ての天気情報を見る" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "查看完整天气" } } } }, "Separate values with" : { "localizations" : { "it" : { "stringUnit" : { "state" : "translated", "value" : "Separa valori con" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "〜で値を区切る" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "分隔符号" } } } }, "Show Humidity" : { "comment" : "Show humidity", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Luftfeuchtigkeit anzeigen" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Afficher l’humidité" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Mostra Umidità" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "湿度を表示する" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "显示湿度" } } } }, "Show UV Index" : { "localizations" : { "it" : { "stringUnit" : { "state" : "translated", "value" : "Mostra Indice UV" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "UVインデックスを表示する" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "紫外线指数" } } } }, "Show Weather Icon" : { "comment" : "\"Show Weather Icon\"", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Weather Icon anzeigen" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Afficher l'icône météo" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Mostra Icona Meteo" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "天気アイコンを表示する" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "显示天气图标" } } } }, "Snow" : { "comment" : "Snow weather condition", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Schnee" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Neige" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Neve" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "雪" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "雪" } } } }, "Sunny" : { "comment" : "Sunny weather condition", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Sonnig" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Ensoleillé" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Sole" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "晴れ" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "阳光" } } } }, "Thunderstorm" : { "comment" : "Thunderstorm weather condition", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Gewitter" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Orage" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Temporale" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "雷雨" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "雷雨" } } } }, "Unhealthy" : { "comment" : "Air Quality Index: Unhealthy", "extractionState" : "manual", "localizations" : { "it" : { "stringUnit" : { "state" : "translated", "value" : "Malsana" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "不健康" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "中度污染" } } } }, "Unhealthy for sensitive groups" : { "comment" : "Air Quality Index: Unhealthy for sensitive groups", "extractionState" : "manual", "localizations" : { "it" : { "stringUnit" : { "state" : "translated", "value" : "Malsana per soggetti sensibili" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "敏感な人々にとって不健康" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "重度污染" } } } }, "Unit" : { "comment" : "Temperature unit", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Einheit" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Unité" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Sistema" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "単位" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "单位" } } } }, "Unknown" : { "comment" : "Unknown location", "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Unbekannt" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Unknown" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Inconnu" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Sconosciuta" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "不明" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "未知" } } } }, "Unknown Location" : { "comment" : "Unknown weather location", "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Unbekannte Position" } }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Unknown Location" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Emplacement inconnu" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Posizione Sconosciuta" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "所在地不明" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "未知地区" } } } }, "UV Index" : { "comment" : "UV Index", "extractionState" : "manual", "localizations" : { "it" : { "stringUnit" : { "state" : "translated", "value" : "Indice UV" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "UVインデックス" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "紫外线指数" } } } }, "Very unhealthy" : { "comment" : "Air Quality Index: Very unhealthy", "extractionState" : "manual", "localizations" : { "it" : { "stringUnit" : { "state" : "translated", "value" : "Molto bassa" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "非常に不健康" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "严重污染" } } } }, "Weather Condition Position" : { "localizations" : { "it" : { "stringUnit" : { "state" : "translated", "value" : "Posizione Condizioni Meteo" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "気象条件の位置" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "天气文本位置" } } } }, "Weather Condition Text" : { "comment" : "Weather Condition Text", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Aktuelles Wetter Text" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Condition météo texte" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Testo Condizioni Meteo" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "気象条件テキスト" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "显示天气文本信息" } } } }, "Weather Source" : { "comment" : "Source for fetching weather", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Wetterquelle" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Source de la météo" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Fonte Meteo" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "気象情報源" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "获取天气方式" } } } } }, "version" : "1.0" } ================================================ FILE: DatWeatherDoe/UI/Configure/ConfigureOptionsView.swift ================================================ // // ConfigureOptionsView.swift // DatWeatherDoe // // Created by Inder Dhir on 8/3/23. // Copyright © 2023 Inder Dhir. All rights reserved. // import SwiftUI struct ConfigureOptionsView: View { @ObservedObject var viewModel: ConfigureViewModel var body: some View { Grid(verticalSpacing: 16) { HStack { Text(LocalizedStringKey("Unit")) Spacer() Picker("", selection: $viewModel.measurementUnit) { Text(LocalizedStringKey("Metric")).tag(MeasurementUnit.metric) Text(LocalizedStringKey("Imperial")).tag(MeasurementUnit.imperial) Text(LocalizedStringKey("All")).tag(MeasurementUnit.all) } .frame(width: 120) } ConfigureWeatherOptionsView(viewModel: viewModel) HStack { Text(LocalizedStringKey("Refresh Interval")) Spacer() Picker("", selection: $viewModel.refreshInterval) { Text(LocalizedStringKey("5 min")).tag(RefreshInterval.fiveMinutes) Text(LocalizedStringKey("15 min")).tag(RefreshInterval.fifteenMinutes) Text(LocalizedStringKey("30 min")).tag(RefreshInterval.thirtyMinutes) Text(LocalizedStringKey("60 min")).tag(RefreshInterval.sixtyMinutes) } .frame(width: 120) } HStack { Text(LocalizedStringKey("Show Weather Icon")) Spacer() Toggle(isOn: $viewModel.isShowingWeatherIcon) {} } HStack { Text(LocalizedStringKey("Show Humidity")) Spacer() Toggle(isOn: $viewModel.isShowingHumidity) {} } HStack { Text(LocalizedStringKey("Show UV Index")) Spacer() Toggle(isOn: $viewModel.isShowingUVIndex) {} } HStack { Text(LocalizedStringKey("Round-off Data")) Spacer() Toggle(isOn: $viewModel.isRoundingOffData) {} } ConfigureUnitOptionsView(viewModel: viewModel) ConfigureValueSeparatorOptionsView(viewModel: viewModel) HStack { Text(LocalizedStringKey("Weather Condition Text")) Spacer() Toggle(isOn: $viewModel.isWeatherConditionAsTextEnabled) {} } HStack { Text(LocalizedStringKey("Weather Condition Position")) Spacer() Picker("", selection: $viewModel.weatherConditionPosition) { Text(LocalizedStringKey("Before Temperature")) .tag(WeatherConditionPosition.beforeTemperature) Text(LocalizedStringKey("After Temperature")) .tag(WeatherConditionPosition.afterTemperature) } .frame(maxWidth: 120) } .disabled(!viewModel.isWeatherConditionAsTextEnabled) } .padding() } } #Preview { ConfigureOptionsView( viewModel: .init(configManager: ConfigManager()) ) .frame(width: 380) } ================================================ FILE: DatWeatherDoe/UI/Configure/ConfigureUnitOptionsView.swift ================================================ // // ConfigureUnitOptionsView.swift // DatWeatherDoe // // Created by Inder Dhir on 8/8/23. // Copyright © 2023 Inder Dhir. All rights reserved. // import SwiftUI struct ConfigureUnitOptionsView: View { @ObservedObject var viewModel: ConfigureViewModel var body: some View { Group { HStack { Text(LocalizedStringKey("Hide unit letter")) Spacer() Toggle(isOn: $viewModel.isUnitLetterOff) {} } HStack { Text(LocalizedStringKey("Hide unit ° symbol")) Spacer() Toggle(isOn: $viewModel.isUnitSymbolOff) {} } } } } #Preview { Grid { ConfigureUnitOptionsView( viewModel: .init(configManager: ConfigManager()) ) } } ================================================ FILE: DatWeatherDoe/UI/Configure/ConfigureValueSeparatorOptionsView.swift ================================================ // // ConfigureValueSeparatorOptionsView.swift // DatWeatherDoe // // Created by Inder Dhir on 8/8/23. // Copyright © 2023 Inder Dhir. All rights reserved. // import SwiftUI struct ConfigureValueSeparatorOptionsView: View { @ObservedObject var viewModel: ConfigureViewModel let valueSeparatorPlaceholder = "\u{007C}" var body: some View { HStack { Text(LocalizedStringKey("Separate values with")) Spacer() TextField(valueSeparatorPlaceholder, text: $viewModel.valueSeparator) .font(.body) .foregroundColor(.primary) .frame(width: 114) } } } #Preview { ConfigureValueSeparatorOptionsView( viewModel: .init(configManager: ConfigManager()) ) } ================================================ FILE: DatWeatherDoe/UI/Configure/ConfigureView.swift ================================================ // // ConfigureView.swift // DatWeatherDoe // // Created by Inder Dhir on 2/18/22. // Copyright © 2022 Inder Dhir. All rights reserved. // import SwiftUI struct ConfigureView: View { @ObservedObject var viewModel: ConfigureViewModel let version: String let onSave: () -> Void let onQuit: () -> Void var body: some View { VStack { ConfigureOptionsView(viewModel: viewModel) HStack { Text(version) .font(.footnote) .fontWeight(.thin) .frame(maxWidth: .infinity, alignment: .leading) CustomButton( text: LocalizedStringKey("Done"), shortcutKey: "d", onClick: onSave ) .frame(maxWidth: .infinity, alignment: .center) Text(LocalizedStringKey("Quit")) .foregroundStyle(Color.red) .onTapGesture(perform: onQuit) .frame(maxWidth: .infinity, alignment: .trailing) } .frame(maxWidth: .infinity) .padding([.leading, .trailing]) } .padding(.bottom) .frame(width: 380) } } #Preview { ConfigureView( viewModel: .init(configManager: ConfigManager()), version: "5.0.0", onSave: {}, onQuit: {} ) } ================================================ FILE: DatWeatherDoe/UI/Configure/ConfigureViewModel.swift ================================================ // // ConfigureViewModel.swift // DatWeatherDoe // // Created by Inder Dhir on 3/20/22. // Copyright © 2022 Inder Dhir. All rights reserved. // import Combine import Foundation final class ConfigureViewModel: ObservableObject { @Published var measurementUnit: MeasurementUnit { didSet { configManager.measurementUnit = measurementUnit.rawValue } } @Published var weatherSource: WeatherSource @Published var weatherSourceText: String @Published var refreshInterval: RefreshInterval { didSet { configManager.refreshInterval = refreshInterval.rawValue } } @Published var isShowingWeatherIcon: Bool { didSet { configManager.isShowingWeatherIcon = isShowingWeatherIcon } } @Published var isShowingHumidity: Bool { didSet { configManager.isShowingHumidity = isShowingHumidity } } @Published var isShowingUVIndex: Bool { didSet { configManager.isShowingUVIndex = isShowingUVIndex } } @Published var isRoundingOffData: Bool { didSet { configManager.isRoundingOffData = isRoundingOffData } } @Published var isUnitLetterOff: Bool { didSet { configManager.isUnitLetterOff = isUnitLetterOff } } @Published var isUnitSymbolOff: Bool { didSet { configManager.isUnitSymbolOff = isUnitSymbolOff } } @Published var valueSeparator = "|" { didSet { configManager.valueSeparator = valueSeparator } } @Published var isWeatherConditionAsTextEnabled: Bool { didSet { configManager.isWeatherConditionAsTextEnabled = isWeatherConditionAsTextEnabled } } @Published var weatherConditionPosition: WeatherConditionPosition { didSet { configManager.weatherConditionPosition = weatherConditionPosition.rawValue } } private let configManager: ConfigManagerType init(configManager: ConfigManagerType) { self.configManager = configManager measurementUnit = configManager.parsedMeasurementUnit weatherSource = WeatherSource(rawValue: configManager.weatherSource) ?? .location weatherSourceText = configManager.weatherSourceText switch configManager.refreshInterval { case 300: refreshInterval = .fiveMinutes case 900: refreshInterval = .fifteenMinutes case 1800: refreshInterval = .thirtyMinutes case 3600: refreshInterval = .sixtyMinutes default: refreshInterval = .fifteenMinutes } isShowingWeatherIcon = configManager.isShowingWeatherIcon isShowingHumidity = configManager.isShowingHumidity isShowingUVIndex = configManager.isShowingUVIndex isRoundingOffData = configManager.isRoundingOffData isUnitLetterOff = configManager.isUnitLetterOff isUnitSymbolOff = configManager.isUnitSymbolOff isWeatherConditionAsTextEnabled = configManager.isWeatherConditionAsTextEnabled weatherConditionPosition = WeatherConditionPosition(rawValue: configManager.weatherConditionPosition) ?? .beforeTemperature } func saveConfig() { configManager.updateWeatherSource(weatherSource, sourceText: weatherSourceText) weatherSourceText = configManager.weatherSourceText configManager.setConfigOptions( .init( refreshInterval: refreshInterval, isShowingHumidity: isShowingHumidity, isShowingUVIndex: isShowingUVIndex, isRoundingOffData: isRoundingOffData, isUnitLetterOff: isUnitLetterOff, isUnitSymbolOff: isUnitSymbolOff, valueSeparator: valueSeparator, isWeatherConditionAsTextEnabled: isWeatherConditionAsTextEnabled ) ) } } ================================================ FILE: DatWeatherDoe/UI/Configure/ConfigureWeatherOptionsView.swift ================================================ // // ConfigureWeatherOptionsView.swift // DatWeatherDoe // // Created by Inder Dhir on 8/8/23. // Copyright © 2023 Inder Dhir. All rights reserved. // import SwiftUI struct ConfigureWeatherOptionsView: View { @ObservedObject var viewModel: ConfigureViewModel var body: some View { Group { HStack { Text(LocalizedStringKey("Weather Source")) Spacer() Picker("", selection: $viewModel.weatherSource) { Text(LocalizedStringKey("Location")).tag(WeatherSource.location) Text(LocalizedStringKey("Lat/Long")).tag(WeatherSource.latLong) } .frame(width: 120) } HStack { Text(viewModel.weatherSource.textHint) .font(.caption) .foregroundColor(.secondary) Spacer() TextField(viewModel.weatherSource.placeholder, text: $viewModel.weatherSourceText) .font(.caption) .foregroundColor(.secondary) .disabled(viewModel.weatherSource == .location) .frame(width: 114) } } } } #Preview { Grid { ConfigureWeatherOptionsView( viewModel: .init(configManager: ConfigManager()) ) } } ================================================ FILE: DatWeatherDoe/UI/Configure/Options/MeasurementUnit.swift ================================================ // // MeasurementUnit.swift // DatWeatherDoe // // Created by Inder Dhir on 2/7/23. // Copyright © 2023 Inder Dhir. All rights reserved. // import Foundation enum MeasurementUnit: String, CaseIterable, Identifiable { case metric, imperial, all var id: Self { self } var temperatureUnit: TemperatureUnit { switch self { case .metric: .celsius case .imperial: .fahrenheit case .all: .all } } } ================================================ FILE: DatWeatherDoe/UI/Configure/Options/RefreshInterval.swift ================================================ // // RefreshInterval.swift // DatWeatherDoe // // Created by Inder Dhir on 5/23/21. // Copyright © 2021 Inder Dhir. All rights reserved. // import Foundation enum RefreshInterval: TimeInterval, CaseIterable, Identifiable { case fiveMinutes = 300 case fifteenMinutes = 900 case thirtyMinutes = 1800 case sixtyMinutes = 3600 var id: Self { self } var title: String { switch self { case .fiveMinutes: String(localized: "5 min") case .fifteenMinutes: String(localized: "15 min") case .thirtyMinutes: String(localized: "30 min") case .sixtyMinutes: String(localized: "60 min") } } } ================================================ FILE: DatWeatherDoe/UI/Configure/Options/TemperatureUnit.swift ================================================ // // TemperatureUnit.swift // DatWeatherDoe // // Created by Inder Dhir on 8/8/23. // Copyright © 2023 Inder Dhir. All rights reserved. // import Foundation enum TemperatureUnit: String, CaseIterable, Identifiable { case fahrenheit, celsius, all var id: Self { self } var unitString: String { switch self { case .fahrenheit: "F" case .celsius: "C" case .all: "All" } } var degreesString: String { "\u{00B0}\(unitString)" } } ================================================ FILE: DatWeatherDoe/UI/Configure/Options/WeatherConditionPosition.swift ================================================ // // WeatherConditionPosition.swift // DatWeatherDoe // // Created by Inder Dhir on 3/27/24. // Copyright © 2024 Inder Dhir. All rights reserved. // import Foundation enum WeatherConditionPosition: String, Identifiable { case beforeTemperature, afterTemperature var id: Self { self } var title: String { switch self { case .beforeTemperature: String(localized: "Before Temperature") case .afterTemperature: String(localized: "After Temperature") } } } ================================================ FILE: DatWeatherDoe/UI/Configure/Options/WeatherSource.swift ================================================ // // WeatherSource.swift // DatWeatherDoe // // Created by Inder Dhir on 5/23/21. // Copyright © 2021 Inder Dhir. All rights reserved. // import Foundation enum WeatherSource: String, CaseIterable { case location, latLong var title: String { switch self { case .location: String(localized: "Location") case .latLong: String(localized: "Lat/Long") } } var placeholder: String { switch self { case .location: "" case .latLong: "42,42" } } var textHint: String { switch self { case .location: "" case .latLong: String(localized: "[latitude],[longitude]") } } } ================================================ FILE: DatWeatherDoe/UI/Decorator/Condition/WeatherCondition.swift ================================================ // // WeatherCondition.swift // DatWeatherDoe // // Created by Inder Dhir on 1/29/16. // Copyright © 2016 Inder Dhir. All rights reserved. // import AppKit import Foundation enum WeatherCondition { case cloudy, partlyCloudy, partlyCloudyNight case sunny, clearNight case snow case heavyRain, freezingRain, lightRain, partlyCloudyRain case thunderstorm case mist, fog static func getFallback(isDay: Bool) -> WeatherCondition { isDay ? .sunny : .clearNight } var symbolName: String { switch self { case .cloudy: "cloud" case .partlyCloudy: "cloud.sun" case .partlyCloudyNight: "cloud.moon" case .sunny: "sun.max" case .clearNight: "moon" case .snow: "cloud.snow" case .lightRain, .heavyRain, .freezingRain: "cloud.rain" case .partlyCloudyRain: "cloud.sun.rain" case .thunderstorm: "cloud.bolt.rain" case .mist, .fog: "cloud.fog" } } var accessibilityLabel: String { switch self { case .cloudy: "Cloudy" case .partlyCloudy: "Partly Cloudy" case .partlyCloudyNight: "Partly Cloudy" case .sunny: "Sunny" case .clearNight: "Clear" case .snow: "Snow" case .lightRain, .heavyRain, .freezingRain: "Rainy" case .partlyCloudyRain: "Partly cloudy with rain" case .thunderstorm: "Thunderstorm" case .mist, .fog: "Cloudy with Fog" } } } ================================================ FILE: DatWeatherDoe/UI/Decorator/Condition/WeatherConditionBuilder.swift ================================================ // // WeatherConditionBuilder.swift // DatWeatherDoe // // Created by Inder Dhir on 1/11/22. // Copyright © 2022 Inder Dhir. All rights reserved. // import Foundation protocol WeatherConditionBuilderType { func build() -> WeatherCondition } final class WeatherConditionBuilder: WeatherConditionBuilderType { private let response: WeatherAPIResponse init(response: WeatherAPIResponse) { self.response = response } func build() -> WeatherCondition { switch response.weatherConditionCode { case 1006, 1009: .cloudy case 1003: response.isDay ? .partlyCloudy : .partlyCloudyNight case 1000: response.isDay ? .sunny : .clearNight case 1030: .mist case 1135, 1147: .fog case 1066, 1114, 1117, 1210, 1213, 1216, 1219, 1222, 1225, 1237, 1249, 1252, 1255, 1258, 1261, 1264, 1279, 1282: .snow case 1192, 1195, 1243, 1246, 1276: .heavyRain case 1069, 1072, 1168, 1171, 1198, 1201, 1204, 1207: .freezingRain case 1063, 1150, 1153, 1180, 1183, 1186, 1189, 1240: response.isDay ? .partlyCloudyRain : .lightRain case 1087, 1273: .thunderstorm default: WeatherCondition.getFallback(isDay: response.isDay) } } } ================================================ FILE: DatWeatherDoe/UI/Decorator/Condition/WeatherConditionTextMapper.swift ================================================ // // WeatherConditionTextMapper.swift // DatWeatherDoe // // Created by Inder Dhir on 1/11/22. // Copyright © 2022 Inder Dhir. All rights reserved. // import Foundation protocol WeatherConditionTextMapperType { func map(_ condition: WeatherCondition) -> String } final class WeatherConditionTextMapper: WeatherConditionTextMapperType { func map(_ condition: WeatherCondition) -> String { switch condition { case .cloudy: String(localized: "Cloudy") case .partlyCloudy, .partlyCloudyNight: String(localized: "Partly cloudy") case .sunny: String(localized: "Sunny") case .clearNight: String(localized: "Clear") case .snow: String(localized: "Snow") case .heavyRain: String(localized: "Heavy rain") case .freezingRain: String(localized: "Freezing rain") case .lightRain: String(localized: "Light rain") case .partlyCloudyRain: String(localized: "Partly cloudy with rain") case .thunderstorm: String(localized: "Thunderstorm") case .mist: String(localized: "Mist") case .fog: String(localized: "Fog") } } } ================================================ FILE: DatWeatherDoe/UI/Decorator/Text/HumidityTextBuilder.swift ================================================ // // HumidityTextBuilder.swift // DatWeatherDoe // // Created by Inder Dhir on 1/15/22. // Copyright © 2022 Inder Dhir. All rights reserved. // import Foundation import OSLog protocol HumidityTextBuilderType { func build() -> String } final class HumidityTextBuilder: HumidityTextBuilderType { private let initial: String private let valueSeparator: String private let humidity: Int private let logger: Logger private let percentString = "\u{0025}" private let humidityFormatter: NumberFormatter = { let formatter = NumberFormatter() formatter.numberStyle = .none formatter.maximumFractionDigits = 0 return formatter }() init( initial: String, valueSeparator: String, humidity: Int, logger: Logger ) { self.initial = initial self.valueSeparator = valueSeparator self.humidity = humidity self.logger = logger } func build() -> String { guard let humidityString = buildHumidity() else { logger.error("Unable to construct humidity string") return initial } return "\(initial) \(valueSeparator) \(humidityString)" } private func buildHumidity() -> String? { guard let formattedString = buildFormattedString() else { return nil } return "\(formattedString)\(percentString)" } private func buildFormattedString() -> String? { humidityFormatter.string(from: NSNumber(value: humidity)) } } ================================================ FILE: DatWeatherDoe/UI/Decorator/Text/SunriseAndSunsetTextBuilder.swift ================================================ // // SunriseAndSunsetTextBuilder.swift // DatWeatherDoe // // Created by Markus Markus on 2022-07-26. // Copyright © 2022 Markus Mayer. // import Foundation protocol SunriseAndSunsetTextBuilderType { func build() -> String } final class SunriseAndSunsetTextBuilder: SunriseAndSunsetTextBuilderType { private let sunset: String private let sunrise: String private let upArrowStr = "⬆" private let downArrowStr = "⬇" init(sunset: String, sunrise: String) { self.sunset = sunset self.sunrise = sunrise } func build() -> String { "\(upArrowStr)\(sunrise) \(downArrowStr)\(sunset)" } } ================================================ FILE: DatWeatherDoe/UI/Decorator/Text/Temperature/TemperatureForecastTextBuilder.swift ================================================ // // TemperatureForecastTextBuilder.swift // DatWeatherDoe // // Created by Inder Dhir on 6/25/22. // Copyright © 2022 Inder Dhir. All rights reserved. // import Foundation protocol TemperatureForecastTextBuilderType { func build() -> String } final class TemperatureForecastTextBuilder: TemperatureForecastTextBuilderType { private let temperatureData: TemperatureData private let forecastTemperatureData: ForecastTemperatureData private let options: TemperatureTextBuilder.Options private let upArrowStr = "⬆" private let downArrowStr = "⬇" private let degreeString = "\u{00B0}" init( temperatureData: TemperatureData, forecastTemperatureData: ForecastTemperatureData, options: TemperatureTextBuilder.Options ) { self.temperatureData = temperatureData self.forecastTemperatureData = forecastTemperatureData self.options = options } func build() -> String { if options.unit == .all { buildTemperatureForAllUnits() } else { buildTemperatureForUnit(options.unit) } } private func buildTemperatureForAllUnits() -> String { let feelsLikeTempFahrenheit = buildFormattedTemperature( temperatureData.feelsLikeTempFahrenheit, unit: .fahrenheit ) let feelsLikeTempCelsius = buildFormattedTemperature( temperatureData.feelsLikeTempCelsius, unit: .celsius ) let feelsLikeTemperatureCombined = [feelsLikeTempFahrenheit, feelsLikeTempCelsius] .compactMap { $0 } .joined(separator: " / ") let maxTempFahrenheit = buildFormattedTemperature( forecastTemperatureData.maxTempF, unit: .fahrenheit ) let maxTempCelsius = buildFormattedTemperature( forecastTemperatureData.maxTempC, unit: .celsius ) let maxTempCombined = [maxTempFahrenheit, maxTempCelsius] .compactMap { $0 } .joined(separator: " / ") let maxTempStr = [upArrowStr, maxTempCombined] .compactMap { $0 } .joined() let minTempFahrenheit = buildFormattedTemperature( forecastTemperatureData.minTempF, unit: .fahrenheit ) let minTempCelsius = buildFormattedTemperature( forecastTemperatureData.minTempC, unit: .celsius ) let minTempCombined = [minTempFahrenheit, minTempCelsius] .compactMap { $0 } .joined(separator: " / ") let minTempStr = [downArrowStr, minTempCombined] .compactMap { $0 } .joined() let maxAndMinTempStr = [maxTempStr, minTempStr] .compactMap { $0 } .joined(separator: " ") return [feelsLikeTemperatureCombined, maxAndMinTempStr] .compactMap { $0 } .joined(separator: " - ") } private func buildTemperatureForUnit(_ unit: TemperatureUnit) -> String { let maxTemp = unit == .fahrenheit ? forecastTemperatureData.maxTempF : forecastTemperatureData.maxTempC let formattedMaxTemp = buildFormattedTemperature(maxTemp, unit: unit) let maxTempStr = [upArrowStr, formattedMaxTemp] .compactMap { $0 } .joined() let minTemp = unit == .fahrenheit ? forecastTemperatureData.minTempF : forecastTemperatureData.minTempC let formatedMinTemp = buildFormattedTemperature(minTemp, unit: unit) let minTempStr = [downArrowStr, formatedMinTemp] .compactMap { $0 } .joined() let maxAndMinTempStr = [maxTempStr, minTempStr] .compactMap { $0 } .joined(separator: " ") let feelsLikeTemp = unit == .fahrenheit ? temperatureData.feelsLikeTempFahrenheit : temperatureData.feelsLikeTempCelsius let formattedFeelsLikeTemp = buildFormattedTemperature(feelsLikeTemp, unit: unit) return [formattedFeelsLikeTemp, maxAndMinTempStr] .compactMap { $0 } .joined(separator: " - ") } private func buildFormattedTemperature( _ temperatureForUnit: Double, unit: TemperatureUnit ) -> String? { guard let temperatureString = TemperatureFormatter() .getFormattedTemperatureString(temperatureForUnit, isRoundingOff: options.isRoundingOff) else { return nil } return combineTemperatureWithUnitDegrees( temperature: temperatureString, unit: unit.unitString, isUnitLetterOff: options.isUnitLetterOff, isUnitSymbolOff: options.isUnitSymbolOff ) } private func combineTemperatureWithUnitDegrees( temperature: String, unit: String, isUnitLetterOff: Bool, isUnitSymbolOff: Bool ) -> String { let unitLetter = isUnitLetterOff ? "" : unit let unitSymbol = isUnitSymbolOff ? "" : degreeString return [temperature, unitLetter].joined(separator: unitSymbol) } } ================================================ FILE: DatWeatherDoe/UI/Decorator/Text/Temperature/TemperatureFormatter.swift ================================================ // // TemperatureFormatter.swift // DatWeatherDoe // // Created by Inder Dhir on 1/12/22. // Copyright © 2022 Inder Dhir. All rights reserved. // import Foundation protocol TemperatureFormatterType { func getFormattedTemperatureString( _ temperature: Double, isRoundingOff: Bool ) -> String? } final class TemperatureFormatter: TemperatureFormatterType { private let formatter: NumberFormatter = { let formatter = NumberFormatter() formatter.numberStyle = .decimal formatter.maximumFractionDigits = 1 formatter.roundingMode = .halfUp return formatter }() func getFormattedTemperatureString( _ temperature: Double, isRoundingOff: Bool ) -> String? { setupTemperatureRounding(isRoundingOff: isRoundingOff) return formatTemperatureString(temperature) } private func setupTemperatureRounding(isRoundingOff: Bool) { formatter.maximumFractionDigits = isRoundingOff ? 0 : 1 } private func formatTemperatureString(_ temperature: Double) -> String? { let formattedTemperature = formatter.string(from: NSNumber(value: temperature)) return fixRoundingIssues(formattedTemperature) } private func fixRoundingIssues(_ temperature: String?) -> String? { if temperature == "-0" { return "0" } return temperature } } ================================================ FILE: DatWeatherDoe/UI/Decorator/Text/Temperature/TemperatureTextBuilder.swift ================================================ // // TemperatureTextBuilder.swift // DatWeatherDoe // // Created by Inder Dhir on 1/15/22. // Copyright © 2022 Inder Dhir. All rights reserved. // protocol TemperatureTextBuilderType { func build() -> String? } final class TemperatureTextBuilder: TemperatureTextBuilderType { struct Options { let unit: TemperatureUnit let isRoundingOff: Bool let isUnitLetterOff: Bool let isUnitSymbolOff: Bool } private let response: WeatherAPIResponse private let options: Options private let temperatureCreator: TemperatureWithDegreesCreatorType private let degreeString = "\u{00B0}" init( response: WeatherAPIResponse, options: Options, temperatureCreator: TemperatureWithDegreesCreatorType ) { self.response = response self.options = options self.temperatureCreator = temperatureCreator } func build() -> String? { if options.unit == .all { buildTemperatureTextForAllUnits() } else { buildTemperatureText(for: options.unit) } } private func buildTemperatureTextForAllUnits() -> String? { let temperatureWithDegrees = temperatureCreator.getTemperatureWithDegrees( temperatureInMultipleUnits: .init( fahrenheit: response.temperatureData.tempFahrenheit, celsius: response.temperatureData.tempCelsius ), isRoundingOff: options.isRoundingOff, isUnitLetterOff: options.isUnitLetterOff, isUnitSymbolOff: options.isUnitSymbolOff ) return temperatureWithDegrees } private func buildTemperatureText(for unit: TemperatureUnit) -> String? { let temperatureForUnit = unit == .fahrenheit ? response.temperatureData.tempFahrenheit : response.temperatureData.tempCelsius let temperatureWithDegrees = temperatureCreator.getTemperatureWithDegrees( temperatureForUnit, unit: unit, isRoundingOff: options.isRoundingOff, isUnitLetterOff: options.isUnitLetterOff, isUnitSymbolOff: options.isUnitSymbolOff ) return temperatureWithDegrees } } ================================================ FILE: DatWeatherDoe/UI/Decorator/Text/Temperature/TemperatureWithDegreesCreator.swift ================================================ // // TemperatureWithDegreesCreator.swift // DatWeatherDoe // // Created by Inder Dhir on 8/9/23. // Copyright © 2023 Inder Dhir. All rights reserved. // import Foundation struct TemperatureInMultipleUnits { let fahrenheit: Double let celsius: Double } protocol TemperatureWithDegreesCreatorType { var degreeString: String { get } func getTemperatureWithDegrees( temperatureInMultipleUnits: TemperatureInMultipleUnits, isRoundingOff: Bool, isUnitLetterOff: Bool, isUnitSymbolOff: Bool ) -> String? func getTemperatureWithDegrees( _ temperature: Double, unit: TemperatureUnit, isRoundingOff: Bool, isUnitLetterOff: Bool, isUnitSymbolOff: Bool ) -> String? } final class TemperatureWithDegreesCreator: TemperatureWithDegreesCreatorType { let degreeString = "\u{00B0}" func getTemperatureWithDegrees( temperatureInMultipleUnits: TemperatureInMultipleUnits, isRoundingOff: Bool, isUnitLetterOff: Bool, isUnitSymbolOff: Bool ) -> String? { guard let fahrenheitString = TemperatureFormatter().getFormattedTemperatureString( temperatureInMultipleUnits.fahrenheit, isRoundingOff: isRoundingOff ) else { return nil } guard let celsiusString = TemperatureFormatter().getFormattedTemperatureString( temperatureInMultipleUnits.celsius, isRoundingOff: isRoundingOff ) else { return nil } let formattedFahrenheit = combineTemperatureWithUnitDegrees( temperature: fahrenheitString, unit: .fahrenheit, isUnitLetterOff: isUnitLetterOff, isUnitSymbolOff: isUnitSymbolOff ) let formattedCelsius = combineTemperatureWithUnitDegrees( temperature: celsiusString, unit: .celsius, isUnitLetterOff: isUnitLetterOff, isUnitSymbolOff: isUnitSymbolOff ) return [formattedFahrenheit, formattedCelsius] .joined(separator: " / ") } func getTemperatureWithDegrees( _ temperature: Double, unit: TemperatureUnit, isRoundingOff: Bool, isUnitLetterOff: Bool, isUnitSymbolOff: Bool ) -> String? { guard let temperatureString = TemperatureFormatter().getFormattedTemperatureString( temperature, isRoundingOff: isRoundingOff ) else { return nil } return combineTemperatureWithUnitDegrees( temperature: temperatureString, unit: unit, isUnitLetterOff: isUnitLetterOff, isUnitSymbolOff: isUnitSymbolOff ) } private func combineTemperatureWithUnitDegrees( temperature: String, unit: TemperatureUnit, isUnitLetterOff: Bool, isUnitSymbolOff: Bool ) -> String { let unitLetter = isUnitLetterOff ? "" : unit.unitString let unitSymbol = isUnitSymbolOff ? "" : degreeString return [temperature, unitLetter].joined(separator: unitSymbol) } } ================================================ FILE: DatWeatherDoe/UI/Decorator/Text/UVIndexTextBuilder.swift ================================================ // // UVIndexTextBuilder.swift // DatWeatherDoe // // Created by Inder Dhir on 10/18/24. // Copyright © 2024 Inder Dhir. All rights reserved. // import Foundation final class UVIndexTextBuilder { private let initial: String private let separator: String init(initial: String, separator: String) { self.initial = initial self.separator = separator } func build(from response: WeatherAPIResponse) -> String { "\(initial) \(separator) \(constructUVString(from: response))" } func constructUVString(from response: WeatherAPIResponse) -> String { let currentHour = Calendar.current.component(.hour, from: Date()) let currentUVIndex = response.getHourlyUVIndex(hour: currentHour) return "UV Index: \(currentUVIndex)" } } ================================================ FILE: DatWeatherDoe/UI/Decorator/Text/WeatherTextBuilder.swift ================================================ // // WeatherTextBuilder.swift // DatWeatherDoe // // Created by Inder Dhir on 1/15/22. // Copyright © 2022 Inder Dhir. All rights reserved. // import OSLog protocol WeatherTextBuilderType { func build() -> String } final class WeatherTextBuilder: WeatherTextBuilderType { struct Options { let isWeatherConditionAsTextEnabled: Bool let conditionPosition: WeatherConditionPosition let valueSeparator: String let temperatureOptions: TemperatureTextBuilder.Options let isShowingHumidity: Bool let isShowingUVIndex: Bool } private let response: WeatherAPIResponse private let options: Options private let logger: Logger init( response: WeatherAPIResponse, options: Options, logger: Logger ) { self.response = response self.options = options self.logger = logger } func build() -> String { let finalString = appendTemperatureAsText() |> appendUVIndex |> appendHumidityText |> buildWeatherConditionAsText return finalString } private func appendTemperatureAsText() -> String { TemperatureTextBuilder( response: response, options: options.temperatureOptions, temperatureCreator: TemperatureWithDegreesCreator() ).build() ?? "" } private func appendUVIndex(initial: String) -> String { guard options.isShowingUVIndex else { return initial } return UVIndexTextBuilder( initial: initial, separator: options.valueSeparator ).build(from: response) } private func appendHumidityText(initial: String) -> String { guard options.isShowingHumidity else { return initial } return HumidityTextBuilder( initial: initial, valueSeparator: options.valueSeparator, humidity: response.humidity, logger: logger ).build() } private func buildWeatherConditionAsText(initial: String) -> String { guard options.isWeatherConditionAsTextEnabled else { return initial } let weatherCondition = WeatherConditionBuilder(response: response).build() let weatherConditionText = WeatherConditionTextMapper().map(weatherCondition) let combinedString = options.conditionPosition == .beforeTemperature ? [weatherConditionText, initial] : [initial, weatherConditionText.lowercased()] return combinedString .compactMap { $0 } .joined(separator: ", ") } } precedencegroup ForwardPipe { associativity: left } infix operator |>: ForwardPipe private func |> (value: T, function: (T) -> U) -> U { function(value) } ================================================ FILE: DatWeatherDoe/UI/Decorator/WeatherDataBuilder.swift ================================================ // // WeatherDataBuilder.swift // DatWeatherDoe // // Created by Inder Dhir on 5/22/21. // Copyright © 2021 Inder Dhir. All rights reserved. // import Foundation import OSLog protocol WeatherDataBuilderType: AnyObject { func build() -> WeatherData } final class WeatherDataBuilder: WeatherDataBuilderType { struct Options { let unit: MeasurementUnit let showWeatherIcon: Bool let textOptions: WeatherTextBuilder.Options } private let response: WeatherAPIResponse private let options: WeatherDataBuilder.Options private let logger: Logger init( response: WeatherAPIResponse, options: WeatherDataBuilder.Options, logger: Logger ) { self.response = response self.options = options self.logger = logger } func build() -> WeatherData { .init( showWeatherIcon: options.showWeatherIcon, textualRepresentation: buildTextualRepresentation(), weatherCondition: buildWeatherCondition(), response: response ) } private func buildTextualRepresentation() -> String { WeatherTextBuilder( response: response, options: options.textOptions, logger: logger ).build() } private func buildWeatherCondition() -> WeatherCondition { WeatherConditionBuilder(response: response).build() } } ================================================ FILE: DatWeatherDoe/UI/Forecaster/WeatherForecaster.swift ================================================ // // WeatherForecaster.swift // DatWeatherDoe // // Created by Inder Dhir on 1/11/22. // Copyright © 2022 Inder Dhir. All rights reserved. // import Cocoa import Foundation protocol WeatherForecasterType { func seeForecastForCity() } final class WeatherForecaster: WeatherForecasterType { private let fullWeatherUrl = URL(string: "https://www.weatherapi.com/weather/")! func seeForecastForCity() { NSWorkspace.shared.open(fullWeatherUrl) } } ================================================ FILE: DatWeatherDoe/UI/Menu Bar/CustomButton.swift ================================================ // // CustomButton.swift // DatWeatherDoe // // Created by Inder Dhir on 6/21/24. // Copyright © 2024 Inder Dhir. All rights reserved. // import SwiftUI struct CustomButton: View { let text: LocalizedStringKey let textColor: Color let shortcutKey: KeyEquivalent let onClick: () -> Void init( text: LocalizedStringKey, textColor: Color = Color.primary, shortcutKey: KeyEquivalent, onClick: @escaping () -> Void ) { self.text = text self.textColor = textColor self.shortcutKey = shortcutKey self.onClick = onClick } var body: some View { Button(action: onClick) { Text(text) .foregroundStyle(textColor) .frame(width: 110, height: 22) }.keyboardShortcut(KeyboardShortcut(shortcutKey)) } } #Preview { CustomButton(text: "See Full Weather", shortcutKey: "f") {} } ================================================ FILE: DatWeatherDoe/UI/Menu Bar/DropdownIcon.swift ================================================ // // DropdownIcon.swift // DatWeatherDoe // // Created by Markus Mayer on 2022-11-21. // Copyright © 2022 Markus Mayer. // // Icons used by the dropdown menu enum DropdownIcon { case location case thermometer case sun case wind case uvIndexAndAirQualityText var symbolName: String { switch self { case .location: "location.north.circle" case .thermometer: "thermometer.snowflake.circle" case .sun: "sun.horizon.circle" case .wind: "wind.circle" case .uvIndexAndAirQualityText: "sun.max.circle" } } var accessibilityLabel: String { switch self { case .location: "Location" case .thermometer: "Temperature" case .sun: "Sunrise and Sunset" case .wind: "Wind data" case .uvIndexAndAirQualityText: "UV Index and Air Quality Index" } } } ================================================ FILE: DatWeatherDoe/UI/Menu Bar/MenuOptionsView.swift ================================================ // // MenuOptionsView.swift // DatWeatherDoe // // Created by Inder Dhir on 6/21/24. // Copyright © 2024 Inder Dhir. All rights reserved. // import SwiftUI struct MenuOptionData { let locationText: String let weatherText: String let sunriseSunsetText: String let tempHumidityWindText: String let uvIndexAndAirQualityText: String } struct MenuOptionsView: View { let data: MenuOptionData? let onSeeWeather: () -> Void let onRefresh: () -> Void var body: some View { VStack(alignment: .leading, spacing: 16) { VStack(alignment: .leading) { NonInteractiveMenuOptionView(icon: .location, text: data?.locationText) NonInteractiveMenuOptionView(icon: .thermometer, text: data?.weatherText) NonInteractiveMenuOptionView(icon: .sun, text: data?.sunriseSunsetText) NonInteractiveMenuOptionView(icon: .wind, text: data?.tempHumidityWindText) NonInteractiveMenuOptionView( icon: .uvIndexAndAirQualityText, text: data?.uvIndexAndAirQualityText ) } HStack { CustomButton( text: LocalizedStringKey("See Full Weather"), shortcutKey: "f", onClick: onSeeWeather ) .frame(maxWidth: .infinity, alignment: .leading) Spacer() .frame(maxWidth: .infinity) CustomButton(text: LocalizedStringKey("Refresh"), shortcutKey: "r", onClick: onRefresh) .frame(maxWidth: .infinity, alignment: .trailing) } .frame(maxWidth: .infinity) } .padding() } } ================================================ FILE: DatWeatherDoe/UI/Menu Bar/MenuView.swift ================================================ // // MenuView.swift // DatWeatherDoe // // Created by Inder Dhir on 6/23/24. // Copyright © 2024 Inder Dhir. All rights reserved. // import SwiftUI struct MenuView: View { @ObservedObject var viewModel: WeatherViewModel @ObservedObject var configureViewModel: ConfigureViewModel private var menuOptionData: MenuOptionData? private var version: String private var onSeeWeather: () -> Void private var onRefresh: () -> Void private var onSave: () -> Void init( viewModel: WeatherViewModel, configureViewModel: ConfigureViewModel, onSeeWeather: @escaping () -> Void, onRefresh: @escaping () -> Void, onSave: @escaping () -> Void ) { self.viewModel = viewModel self.configureViewModel = configureViewModel self.onSeeWeather = onSeeWeather self.onRefresh = onRefresh self.onSave = onSave version = (Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String) ?? "1.0.0" } var body: some View { VStack { MenuOptionsView( data: viewModel.menuOptionData, onSeeWeather: onSeeWeather, onRefresh: onRefresh ) Divider() ConfigureView( viewModel: configureViewModel, version: version, onSave: onSave, onQuit: { NSApp.terminate(self) } ) } } } ================================================ FILE: DatWeatherDoe/UI/Menu Bar/NonInteractiveMenuOptionView.swift ================================================ // // NonInteractiveMenuOptionView.swift // DatWeatherDoe // // Created by Inder Dhir on 6/21/24. // Copyright © 2024 Inder Dhir. All rights reserved. // import SwiftUI struct NonInteractiveMenuOptionView: View { let icon: DropdownIcon let text: String? var body: some View { HStack(spacing: 6) { Image(systemName: icon.symbolName) .renderingMode(.template) .resizable() .frame(width: 24, height: 24) .accessibilityLabel(icon.accessibilityLabel) if let text { Text(text) .foregroundStyle(Color.secondary) .frame(maxWidth: .infinity, alignment: .leading) } } } } #Preview { NonInteractiveMenuOptionView( icon: .wind, text: "Test location" ) .frame(width: 300, height: 100, alignment: .leading) .padding() } ================================================ FILE: DatWeatherDoe/UI/Menu Bar/WindSpeedFormatter.swift ================================================ // // WindSpeedFormatter.swift // DatWeatherDoe // // Created by Inder Dhir on 10/12/23. // Copyright © 2023 Inder Dhir. All rights reserved. // import Foundation struct WindSpeedInMultipleUnits { let meterPerSec: Double let milesPerHour: Double } protocol WindSpeedFormatterType { func getFormattedWindSpeedStringForAllUnits( windData: WindData, isRoundingOff: Bool ) -> String func getFormattedWindSpeedString( unit: MeasurementUnit, windData: WindData ) -> String } final class WindSpeedFormatter: WindSpeedFormatterType { private let degreeString = "\u{00B0}" private let formatter: NumberFormatter = { let formatter = NumberFormatter() formatter.numberStyle = .decimal formatter.maximumFractionDigits = 1 formatter.roundingMode = .halfUp return formatter }() func getFormattedWindSpeedStringForAllUnits( windData: WindData, isRoundingOff _: Bool ) -> String { let mphSpeed = windData.speedMph let mpsSpeed = mpsSpeedFrom(mphSpeed: mphSpeed) let mphRounded = formatter.string(from: NSNumber(value: mphSpeed)) ?? "" let windSpeedMph = [mphRounded, "mi/hr"].joined() let mpsRounded = formatter.string(from: NSNumber(value: mpsSpeed)) ?? "" let windSpeedMps = [mpsRounded, "m/s"].joined() let windSpeedStr = [windSpeedMph, windSpeedMps].joined(separator: " | ") return combinedWindString(windData: windData, windSpeed: windSpeedStr) } func getFormattedWindSpeedString( unit: MeasurementUnit, windData: WindData ) -> String { let mphSpeed = windData.speedMph let mpsSpeed = mpsSpeedFrom(mphSpeed: mphSpeed) let speed = unit == .imperial ? mphSpeed : mpsSpeed let speedRounded = formatter.string(from: NSNumber(value: speed)) ?? "" let windSpeedSuffix = unit == .imperial ? "mi/hr" : "m/s" let windSpeedStr = [speedRounded, windSpeedSuffix].joined() return combinedWindString(windData: windData, windSpeed: windSpeedStr) } private func combinedWindString( windData: WindData, windSpeed: String ) -> String { let windDegreesStr = [String(windData.degrees), degreeString].joined() let windDirectionStr = "(\(windData.direction))" let windAndDegreesStr = [windSpeed, windDegreesStr].joined(separator: " - ") let windFullStr = [windAndDegreesStr, windDirectionStr].joined(separator: " ") return windFullStr } private func mpsSpeedFrom(mphSpeed: Double) -> Double { 0.4469 * mphSpeed } } ================================================ FILE: DatWeatherDoe/UI/Status Bar/StatusBarView.swift ================================================ // // StatusBarView.swift // DatWeatherDoe // // Created by Inder Dhir on 6/23/24. // Copyright © 2024 Inder Dhir. All rights reserved. // import SwiftUI struct StatusBarView: View { let weatherResult: Result? var body: some View { if let weatherResult { switch weatherResult { case let .success(success): HStack { if success.showWeatherIcon { Image(systemName: success.weatherCondition.symbolName) .renderingMode(.template) .accessibilityLabel(success.weatherCondition.accessibilityLabel) } if let text = success.textualRepresentation { Text(text) } } case let .failure(failure): Text(failure.localizedDescription) } } } } #if DEBUG #Preview { StatusBarView( weatherResult: .success( .init( showWeatherIcon: true, textualRepresentation: "88", weatherCondition: .cloudy, response: response ) ) ) .frame(width: 100, height: 50) } #Preview { StatusBarView(weatherResult: .failure(WeatherError.latLongIncorrect)) .frame(width: 100, height: 50) } #endif ================================================ FILE: DatWeatherDoe/ViewModel/Parser/CityWeatherResultParser.swift ================================================ // // CityWeatherResultParser.swift // DatWeatherDoe // // Created by preckrasno on 14.02.2023. // Copyright © 2023 Inder Dhir. All rights reserved. // import Foundation final class CityWeatherResultParser: WeatherResultParser { override func parse() { switch weatherDataResult { case let .success(weatherData): delegate?.didUpdateWeatherData(weatherData) case let .failure(error): guard let weatherError = error as? WeatherError else { return } let errorString = weatherError == WeatherError.cityIncorrect ? errorLabels.cityErrorString : errorLabels.networkErrorString delegate?.didFailToUpdateWeatherData(errorString) } } } ================================================ FILE: DatWeatherDoe/ViewModel/Parser/ZipCodeWeatherResultParser.swift ================================================ // // ZipCodeWeatherResultParser.swift // DatWeatherDoe // // Created by Inder Dhir on 1/17/22. // Copyright © 2022 Inder Dhir. All rights reserved. // final class ZipCodeWeatherResultParser: WeatherResultParser { override func parse() { switch weatherDataResult { case let .success(weatherData): delegate?.didUpdateWeatherData(weatherData) case let .failure(error): guard let weatherError = error as? WeatherError else { return } let errorString = weatherError == WeatherError.zipCodeIncorrect ? errorLabels.zipCodeErrorString : errorLabels.networkErrorString delegate?.didFailToUpdateWeatherData(errorString) } } } ================================================ FILE: DatWeatherDoe/ViewModel/Repository/Coordinates/LocationCoordinatesWeatherRepository.swift ================================================ // // LocationCoordinatesWeatherRepository.swift // DatWeatherDoe // // Created by Inder Dhir on 1/14/22. // Copyright © 2022 Inder Dhir. All rights reserved. // import CoreLocation import OSLog final class LocationCoordinatesWeatherRepository: WeatherRepositoryType { private let appId: String private let latLong: String private let networkClient: NetworkClientType private let logger: Logger init( appId: String, latLong: String, networkClient: NetworkClientType, logger: Logger ) { self.appId = appId self.latLong = latLong self.networkClient = networkClient self.logger = logger } func getWeather() async throws -> WeatherAPIResponse { logger.debug("Getting weather via lat/long") do { let location = try getLocationCoordinatesFrom(latLong) let url = try WeatherURLBuilder(appId: appId, location: location).build() let data = try await networkClient.performRequest(url: url) return try WeatherAPIResponseParser().parse(data) } catch { logger.error("Getting weather via lat/long failed") throw error } } private func getLocationCoordinatesFrom(_ latLong: String) throws -> CLLocationCoordinate2D { try LocationValidator(latLong: latLong).validate() let latAndlong = try LocationParser().parseCoordinates(latLong) return latAndlong } } ================================================ FILE: DatWeatherDoe/ViewModel/Repository/Coordinates/LocationParser.swift ================================================ // // LocationParser.swift // DatWeatherDoe // // Created by Inder Dhir on 1/10/22. // Copyright © 2022 Inder Dhir. All rights reserved. // import CoreLocation protocol LocationParserType { func parseCoordinates(_ latLong: String) throws -> CLLocationCoordinate2D } final class LocationParser: LocationParserType { func parseCoordinates(_ latLong: String) throws -> CLLocationCoordinate2D { let latLongCombo = latLong.split(separator: ",") guard latLongCombo.count == 2 else { throw WeatherError.latLongIncorrect } return try parseLocationDegrees( possibleLatitude: String(latLongCombo[0]).trim(), possibleLongitude: String(latLongCombo[1]).trim() ) } private func parseLocationDegrees( possibleLatitude: String, possibleLongitude: String ) throws -> CLLocationCoordinate2D { let lat = CLLocationDegrees(possibleLatitude.trim()) let long = CLLocationDegrees(possibleLongitude.trim()) guard let lat, let long else { throw WeatherError.latLongIncorrect } return .init(latitude: lat, longitude: long) } } private extension String { func trim() -> String { trimmingCharacters(in: .whitespacesAndNewlines) } } ================================================ FILE: DatWeatherDoe/ViewModel/Repository/Coordinates/LocationValidator.swift ================================================ // // LocationValidator.swift // DatWeatherDoe // // Created by Inder Dhir on 1/13/22. // Copyright © 2022 Inder Dhir. All rights reserved. // final class LocationValidator: WeatherValidatorType { private let latLong: String init(latLong: String) { self.latLong = latLong } func validate() throws { let coordinates = latLong.split(separator: ",") let isValid = coordinates.count == 2 if !isValid { throw WeatherError.latLongIncorrect } } } ================================================ FILE: DatWeatherDoe/ViewModel/Repository/System/SystemLocationFetcher.swift ================================================ // // SystemLocationFetcher.swift // DatWeatherDoe // // Created by Inder Dhir on 1/9/22. // Copyright © 2022 Inder Dhir. All rights reserved. // import Combine import CoreLocation import Foundation import OSLog protocol SystemLocationFetcherType: AnyObject { func getLocation() async throws -> CLLocationCoordinate2D } final actor SystemLocationFetcher: NSObject, SystemLocationFetcherType { private let logger: Logger private var cachedLocation: CLLocationCoordinate2D? private var permissionContinuation: CheckedContinuation? private var locationUpdateContinuation: CheckedContinuation? @MainActor private lazy var locationManager: CLLocationManager = { let locationManager = CLLocationManager() locationManager.delegate = self locationManager.desiredAccuracy = kCLLocationAccuracyKilometer locationManager.distanceFilter = 1000 return locationManager }() init(logger: Logger) { self.logger = logger } func getLocation() async throws(WeatherError) -> CLLocationCoordinate2D { guard CLLocationManager.locationServicesEnabled() else { logger.error("Location services not enabled") throw WeatherError.locationError } switch CLLocationManager().authorizationStatus { case .notDetermined: let isAuthorized = await withCheckedContinuation { continuation in logger.debug("Location permission not determined") permissionContinuation = continuation Task { @MainActor in locationManager.requestWhenInUseAuthorization() } } permissionContinuation = nil logger.debug("Location permission changed, isAuthorized?: \(isAuthorized)") if isAuthorized { return try await requestLatestOrCachedLocation() } throw WeatherError.locationError case .authorized: return try await requestLatestOrCachedLocation() default: logger.error("Location permission has NOT been granted") throw WeatherError.locationError } } private func updateCachedLocation(_ location: CLLocationCoordinate2D) { cachedLocation = location } private func requestLatestOrCachedLocation() async throws(WeatherError) -> CLLocationCoordinate2D { if let cachedLocation = getCachedLocationIfPresent() { return cachedLocation } do { let latestLocation = try await Task.retrying { try await self.requestLocation() }.value return latestLocation } catch { throw WeatherError.locationError } } private func getCachedLocationIfPresent() -> CLLocationCoordinate2D? { if let cachedLocation { logger.debug("Sending cached location") return cachedLocation } return nil } private func requestLocation() async throws -> CLLocationCoordinate2D { let coordinate = try await withCheckedThrowingContinuation { continuation in locationUpdateContinuation = continuation Task { @MainActor in locationManager.startUpdatingLocation() } } locationUpdateContinuation = nil return coordinate } } // MARK: CLLocationManagerDelegate extension SystemLocationFetcher: CLLocationManagerDelegate { nonisolated func locationManagerDidChangeAuthorization(_: CLLocationManager) { let isAuthorized = CLLocationManager().authorizationStatus == .authorized Task { await permissionContinuation?.resume(returning: isAuthorized) } } nonisolated func locationManager(_: CLLocationManager, didFailWithError error: Error) { Task { await locationUpdateContinuation?.resume(throwing: error) } } nonisolated func locationManager(_ manager: CLLocationManager, didUpdateLocations _: [CLLocation]) { let coordinate = manager.location?.coordinate ?? .init(latitude: .zero, longitude: .zero) Task { await updateCachedLocation(coordinate) await locationUpdateContinuation?.resume(returning: coordinate) } } } ================================================ FILE: DatWeatherDoe/ViewModel/Repository/System/SystemLocationWeatherRepository.swift ================================================ // // SystemLocationWeatherRepository.swift // DatWeatherDoe // // Created by Inder Dhir on 1/15/22. // Copyright © 2022 Inder Dhir. All rights reserved. // import CoreLocation import OSLog final class SystemLocationWeatherRepository: WeatherRepositoryType { private let appId: String private let location: CLLocationCoordinate2D private let networkClient: NetworkClientType private let logger: Logger init( appId: String, location: CLLocationCoordinate2D, networkClient: NetworkClientType, logger: Logger ) { self.appId = appId self.location = location self.networkClient = networkClient self.logger = logger } func getWeather() async throws -> WeatherAPIResponse { logger.debug("Getting weather via location") do { let url = try WeatherURLBuilder(appId: appId, location: location).build() let data = try await networkClient.performRequest(url: url) return try WeatherAPIResponseParser().parse(data) } catch { logger.error("Getting weather via location failed") throw error } } } ================================================ FILE: DatWeatherDoe/ViewModel/Repository/System/Task+Retry.swift ================================================ // // Task+Retry.swift // DatWeatherDoe // // Created by Inder Dhir on 10/13/25. // Copyright © 2025 Inder Dhir. All rights reserved. // import Foundation extension Task where Failure == Error { @discardableResult static func retrying( priority: TaskPriority? = nil, maxRetryCount: Int = 3, retryDelay: TimeInterval = 1, operation: @Sendable @escaping () async throws -> Success ) -> Task { Task(priority: priority) { for _ in 0...sleep(nanoseconds: delay) continue } } try Task.checkCancellation() return try await operation() } } } ================================================ FILE: DatWeatherDoe/ViewModel/Repository/WeatherRepositoryFactory.swift ================================================ // // WeatherRepositoryFactory.swift // DatWeatherDoe // // Created by Inder Dhir on 8/7/23. // Copyright © 2023 Inder Dhir. All rights reserved. // import CoreLocation import Foundation import OSLog protocol WeatherRepositoryFactoryType { func create(location: CLLocationCoordinate2D) -> WeatherRepositoryType func create(latLong: String) -> WeatherRepositoryType } final class WeatherRepositoryFactory: WeatherRepositoryFactoryType { struct Options { let appId: String let networkClient: NetworkClient let logger: Logger } private let appId: String private let networkClient: NetworkClientType private let logger: Logger init(appId: String, networkClient: NetworkClientType, logger: Logger) { self.appId = appId self.networkClient = networkClient self.logger = logger } func create(location: CLLocationCoordinate2D) -> WeatherRepositoryType { SystemLocationWeatherRepository( appId: appId, location: location, networkClient: networkClient, logger: logger ) } func create(latLong: String) -> WeatherRepositoryType { LocationCoordinatesWeatherRepository( appId: appId, latLong: latLong, networkClient: networkClient, logger: logger ) } } ================================================ FILE: DatWeatherDoe/ViewModel/Repository/WeatherRepositoryType.swift ================================================ //// //// WeatherRepositoryType.swift //// DatWeatherDoe //// //// Created by Inder Dhir on 1/30/16. //// Copyright © 2016 Inder Dhir. All rights reserved. //// protocol WeatherRepositoryType: AnyObject { func getWeather() async throws -> WeatherAPIResponse } ================================================ FILE: DatWeatherDoe/ViewModel/Repository/WeatherURLBuilder.swift ================================================ // // WeatherURLBuilder.swift // DatWeatherDoe // // Created by Inder Dhir on 1/10/22. // Copyright © 2022 Inder Dhir. All rights reserved. // import CoreLocation import Foundation protocol WeatherURLBuilderType { func build() throws -> URL } final class WeatherURLBuilder: WeatherURLBuilderType { private let apiUrlString = "https://api.weatherapi.com/v1/forecast.json" private let appId: String private let location: CLLocationCoordinate2D init(appId: String, location: CLLocationCoordinate2D) { self.appId = appId self.location = location } func build() throws -> URL { let latLonString = "\(location.latitude),\(location.longitude)" let queryItems: [URLQueryItem] = [ URLQueryItem(name: "key", value: appId), URLQueryItem(name: "aqi", value: String("yes")), URLQueryItem(name: "q", value: latLonString), URLQueryItem(name: "dt", value: parsedDateToday) ] var urlComps = URLComponents(string: apiUrlString) urlComps?.queryItems = queryItems guard let finalUrl = urlComps?.url else { throw WeatherError.unableToConstructUrl } return finalUrl } private var parsedDateToday: String { let dateFormatter = DateFormatter() dateFormatter.dateFormat = "yyyy-MM-dd" return dateFormatter.string(from: Date()) } } ================================================ FILE: DatWeatherDoe/ViewModel/Repository/WeatherValidatorType.swift ================================================ // // WeatherValidatorType.swift // DatWeatherDoe // // Created by Inder Dhir on 1/10/22. // Copyright © 2022 Inder Dhir. All rights reserved. // protocol WeatherValidatorType: AnyObject { func validate() throws } ================================================ FILE: DatWeatherDoe/ViewModel/WeatherDataFormatter.swift ================================================ // // WeatherDataFormatter.swift // DatWeatherDoe // // Created by Inder Dhir on 6/24/24. // Copyright © 2024 Inder Dhir. All rights reserved. // import Foundation protocol WeatherDataFormatterType { func getLocation(for data: WeatherData) -> String func getWeatherText(for data: WeatherData) -> String func getSunriseSunset(for data: WeatherData) -> String func getWindSpeedItem(for data: WeatherData) -> String func getUVIndexAndAirQuality(for data: WeatherData) -> String } final class WeatherDataFormatter: WeatherDataFormatterType { let configManager: ConfigManagerType init(configManager: ConfigManagerType) { self.configManager = configManager } func getLocation(for data: WeatherData) -> String { [ data.response.locationName, WeatherConditionTextMapper().map(data.weatherCondition) ] .joined(separator: " - ") } func getWeatherText(for data: WeatherData) -> String { TemperatureForecastTextBuilder( temperatureData: data.response.temperatureData, forecastTemperatureData: data.response.forecastDayData.temperatureData, options: .init( unit: configManager.parsedMeasurementUnit.temperatureUnit, isRoundingOff: configManager.isRoundingOffData, isUnitLetterOff: configManager.isUnitLetterOff, isUnitSymbolOff: configManager.isUnitSymbolOff ) ).build() } func getSunriseSunset(for data: WeatherData) -> String { SunriseAndSunsetTextBuilder( sunset: data.response.forecastDayData.astro.sunset, sunrise: data.response.forecastDayData.astro.sunrise ).build() } func getWindSpeedItem(for data: WeatherData) -> String { if configManager.measurementUnit == MeasurementUnit.all.rawValue { WindSpeedFormatter() .getFormattedWindSpeedStringForAllUnits( windData: data.response.windData, isRoundingOff: configManager.isRoundingOffData ) } else { WindSpeedFormatter() .getFormattedWindSpeedString( unit: configManager.parsedMeasurementUnit, windData: data.response.windData ) } } func getUVIndexAndAirQuality(for data: WeatherData) -> String { let currentHour = Calendar.current.component(.hour, from: Date()) let currentUVIndex = data.response.getHourlyUVIndex(hour: currentHour) let uvIndex = "UV Index: \(currentUVIndex)" let airQualityIndex = "AQI: \(data.response.airQualityIndex.description)" return [uvIndex, airQualityIndex].joined(separator: " | ") } } ================================================ FILE: DatWeatherDoe/ViewModel/WeatherViewModel.swift ================================================ // // WeatherViewModel.swift // DatWeatherDoe // // Created by Inder Dhir on 1/9/22. // Copyright © 2022 Inder Dhir. All rights reserved. // import Foundation import OSLog final class WeatherViewModel: WeatherViewModelType, ObservableObject { private let locationFetcher: SystemLocationFetcherType private var weatherFactory: WeatherRepositoryFactoryType private let configManager: ConfigManagerType private var dataFormatter: WeatherDataFormatterType! private let logger: Logger private var reachability: NetworkReachability! private let forecaster = WeatherForecaster() private var weatherTimerTask: Task? @Published var menuOptionData: MenuOptionData? @Published var weatherResult: Result? init( locationFetcher: SystemLocationFetcher, weatherFactory: WeatherRepositoryFactoryType, configManager: ConfigManagerType, logger: Logger ) { self.locationFetcher = locationFetcher self.configManager = configManager self.weatherFactory = weatherFactory self.logger = logger setupReachability() } deinit { weatherTimerTask?.cancel() } func setup(with formatter: WeatherDataFormatter) { dataFormatter = formatter } func getUpdatedWeatherAfterRefresh() { weatherTimerTask?.cancel() weatherTimerTask = Task { [weak self] in guard let self else { return } while !Task.isCancelled { await self.getWeatherWithSelectedSource() try? await Task.sleep(for: .seconds(configManager.refreshInterval)) } } } func seeForecastForCurrentCity() { forecaster.seeForecastForCity() } private func setupReachability() { reachability = NetworkReachability( logger: logger, onBecomingReachable: { [weak self] in self?.getUpdatedWeatherAfterRefresh() } ) } private func getWeatherWithSelectedSource() async { let weatherSource = WeatherSource(rawValue: configManager.weatherSource) ?? .location do { let weatherData = switch weatherSource { case .location: try await getWeatherAfterUpdatingLocation() case .latLong: try await getWeatherViaLocationCoordinates() } updateWeatherData(weatherData) } catch { updateWeatherData(error) } } private func getWeatherAfterUpdatingLocation() async throws -> WeatherData { let locationFetcher = locationFetcher let location = try await locationFetcher.getLocation() return try await getWeather( repository: weatherFactory.create(location: location), unit: configManager.parsedMeasurementUnit ) } private func getWeatherViaLocationCoordinates() async throws -> WeatherData { let latLong = configManager.weatherSourceText guard !latLong.isEmpty else { throw WeatherError.latLongIncorrect } return try await getWeather( repository: weatherFactory.create(latLong: latLong), unit: configManager.parsedMeasurementUnit ) } private func buildWeatherDataOptions(for unit: MeasurementUnit) -> WeatherDataBuilder.Options { .init( unit: unit, showWeatherIcon: configManager.isShowingWeatherIcon, textOptions: buildWeatherTextOptions(for: unit) ) } private func buildWeatherTextOptions(for unit: MeasurementUnit) -> WeatherTextBuilder.Options { let conditionPosition = WeatherConditionPosition(rawValue: configManager.weatherConditionPosition) ?? .beforeTemperature return .init( isWeatherConditionAsTextEnabled: configManager.isWeatherConditionAsTextEnabled, conditionPosition: conditionPosition, valueSeparator: configManager.valueSeparator, temperatureOptions: .init( unit: unit.temperatureUnit, isRoundingOff: configManager.isRoundingOffData, isUnitLetterOff: configManager.isUnitLetterOff, isUnitSymbolOff: configManager.isUnitSymbolOff ), isShowingHumidity: configManager.isShowingHumidity, isShowingUVIndex: configManager.isShowingUVIndex ) } private func getWeather( repository: WeatherRepositoryType, unit: MeasurementUnit ) async throws -> WeatherData { do { let response = try await repository.getWeather() let weatherData = WeatherDataBuilder( response: response, options: buildWeatherDataOptions(for: unit), logger: logger ).build() return weatherData } catch { throw WeatherError.networkError } } private func updateWeatherData(_ data: WeatherData) { menuOptionData = MenuOptionData( locationText: dataFormatter.getLocation(for: data), weatherText: dataFormatter.getWeatherText(for: data), sunriseSunsetText: dataFormatter.getSunriseSunset(for: data), tempHumidityWindText: dataFormatter.getWindSpeedItem(for: data), uvIndexAndAirQualityText: dataFormatter.getUVIndexAndAirQuality(for: data) ) weatherResult = .success(data) } private func updateWeatherData(_ error: Error) { menuOptionData = nil weatherResult = .failure(error) } } ================================================ FILE: DatWeatherDoe/ViewModel/WeatherViewModelType.swift ================================================ // // WeatherViewModelType.swift // DatWeatherDoe // // Created by Inder Dhir on 1/9/22. // Copyright © 2022 Inder Dhir. All rights reserved. // import Combine protocol WeatherViewModelType: AnyObject { func setup(with formatter: WeatherDataFormatter) func getUpdatedWeatherAfterRefresh() func seeForecastForCurrentCity() } ================================================ FILE: DatWeatherDoe.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 54; objects = { /* Begin PBXBuildFile section */ 2000D4082AD86DAA0052EDA6 /* WindSpeedFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2000D4072AD86DAA0052EDA6 /* WindSpeedFormatter.swift */; }; 20012FA6267980EE00553B60 /* Reachability in Frameworks */ = {isa = PBXBuildFile; productRef = 20012FA5267980EE00553B60 /* Reachability */; }; 200520F62C29426E00006FC0 /* WeatherDataFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 200520F52C29426E00006FC0 /* WeatherDataFormatter.swift */; }; 2005C055278CB4E40067BBD1 /* WeatherValidatorType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2005C054278CB4E40067BBD1 /* WeatherValidatorType.swift */; }; 2005C057278CB5640067BBD1 /* LocationParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2005C056278CB5640067BBD1 /* LocationParser.swift */; }; 2005C059278CB5FC0067BBD1 /* WeatherURLBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2005C058278CB5FC0067BBD1 /* WeatherURLBuilder.swift */; }; 2005C05D278CE0350067BBD1 /* WeatherAPIResponseParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2005C05C278CE0350067BBD1 /* WeatherAPIResponseParser.swift */; }; 201C6C4620262E380065E795 /* WeatherAPIResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 201C6C4520262E380065E795 /* WeatherAPIResponse.swift */; }; 20206F0727BFF3D7004B418F /* ConfigureView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20206F0627BFF3D7004B418F /* ConfigureView.swift */; }; 2023EDA91C4ED09C0087FD67 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2023EDA81C4ED09C0087FD67 /* Assets.xcassets */; }; 202B1015278D46AB00ED6D42 /* WeatherConditionBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 202B1014278D46AB00ED6D42 /* WeatherConditionBuilder.swift */; }; 202B101E278D4F1900ED6D42 /* WeatherReachability.swift in Sources */ = {isa = PBXBuildFile; fileRef = 202B101D278D4F1900ED6D42 /* WeatherReachability.swift */; }; 202B1029278D5A7100ED6D42 /* WeatherForecaster.swift in Sources */ = {isa = PBXBuildFile; fileRef = 202B1028278D5A7100ED6D42 /* WeatherForecaster.swift */; }; 202B1030278D632800ED6D42 /* APIKeyParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 202B102F278D632800ED6D42 /* APIKeyParser.swift */; }; 2039B3DF2C278AA4006A6B6D /* SunriseSunsetData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2039B3DE2C278AA4006A6B6D /* SunriseSunsetData.swift */; }; 2039B3E32C289697006A6B6D /* TemperatureData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2039B3E22C289697006A6B6D /* TemperatureData.swift */; }; 2039B3E72C2896BD006A6B6D /* WindData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2039B3E62C2896BD006A6B6D /* WindData.swift */; }; 2039B3E92C2896CF006A6B6D /* ForecastData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2039B3E82C2896CF006A6B6D /* ForecastData.swift */; }; 2039B3EB2C2896D7006A6B6D /* ForecastTemperatureData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2039B3EA2C2896D7006A6B6D /* ForecastTemperatureData.swift */; }; 2039B3EF2C28996D006A6B6D /* WeatherCondition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2039B3EE2C28996D006A6B6D /* WeatherCondition.swift */; }; 2039B3F92C28D18A006A6B6D /* DatWeatherDoeApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2039B3F82C28D18A006A6B6D /* DatWeatherDoeApp.swift */; }; 2039B3FB2C28D1B0006A6B6D /* CustomButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2039B3FA2C28D1B0006A6B6D /* CustomButton.swift */; }; 2039B3FD2C28D1C0006A6B6D /* MenuOptionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2039B3FC2C28D1C0006A6B6D /* MenuOptionsView.swift */; }; 2039B3FF2C28D1D3006A6B6D /* NonInteractiveMenuOptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2039B3FE2C28D1D3006A6B6D /* NonInteractiveMenuOptionView.swift */; }; 2039B4012C291B5A006A6B6D /* MenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2039B4002C291B5A006A6B6D /* MenuView.swift */; }; 2039B4042C291C35006A6B6D /* StatusBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2039B4032C291C35006A6B6D /* StatusBarView.swift */; }; 2039B4072C2920BF006A6B6D /* MenuBarExtraAccess in Frameworks */ = {isa = PBXBuildFile; productRef = 2039B4062C2920BF006A6B6D /* MenuBarExtraAccess */; }; 2044E91E2867D3CF00AED55B /* TemperatureForecastTextBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2044E91D2867D3CF00AED55B /* TemperatureForecastTextBuilder.swift */; }; 204597682A84492400CF73CE /* TemperatureWithDegreesCreator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 204597672A84492400CF73CE /* TemperatureWithDegreesCreator.swift */; }; 20459C641C5C50DA004D0DC1 /* ConfigManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20459C631C5C50DA004D0DC1 /* ConfigManager.swift */; }; 206523C826597B120026C506 /* WeatherError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 206523C726597B120026C506 /* WeatherError.swift */; }; 206523D62659A92B0026C506 /* WeatherDataBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 206523D52659A92B0026C506 /* WeatherDataBuilder.swift */; }; 206523FB265AD5730026C506 /* WeatherSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 206523FA265AD5730026C506 /* WeatherSource.swift */; }; 206523FD265AF03E0026C506 /* RefreshInterval.swift in Sources */ = {isa = PBXBuildFile; fileRef = 206523FC265AF03E0026C506 /* RefreshInterval.swift */; }; 206E15252A7C4C5C0096D33C /* ConfigOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 206E15242A7C4C5C0096D33C /* ConfigOptions.swift */; }; 206E152F2A7C544D0096D33C /* ConfigureOptionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 206E152E2A7C544D0096D33C /* ConfigureOptionsView.swift */; }; 206FF1BB2BB4BB9400111EAE /* WeatherConditionPosition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 206FF1BA2BB4BB9400111EAE /* WeatherConditionPosition.swift */; }; 2074949927A09278002AA589 /* WeatherURLBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2074949827A09278002AA589 /* WeatherURLBuilderTests.swift */; }; 2077BC52278DF98800E0453C /* WeatherConditionTextMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2077BC51278DF98800E0453C /* WeatherConditionTextMapper.swift */; }; 209174102A9BDA4A00BB63E0 /* ConfigureViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2085263427E80B9E0017D7F4 /* ConfigureViewModel.swift */; }; 209482C629934BFF00AF39D4 /* MeasurementUnit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 209482C529934BFF00AF39D4 /* MeasurementUnit.swift */; }; 209F8A38279136D300EB5C45 /* LocationValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 209F8A37279136D300EB5C45 /* LocationValidator.swift */; }; 209F8A3D27914A5900EB5C45 /* NetworkClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 209F8A3C27914A5900EB5C45 /* NetworkClient.swift */; }; 209F8A4127915DBC00EB5C45 /* LocationCoordinatesWeatherRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 209F8A4027915DBC00EB5C45 /* LocationCoordinatesWeatherRepository.swift */; }; 20B3845F27A1CFE800F85482 /* LocationValidatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20B3845E27A1CFE800F85482 /* LocationValidatorTests.swift */; }; 20B46814279394FB00FC6050 /* WeatherTextBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20B46813279394FB00FC6050 /* WeatherTextBuilder.swift */; }; 20B468192793989B00FC6050 /* HumidityTextBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20B468182793989B00FC6050 /* HumidityTextBuilder.swift */; }; 20B4681B2793A7E300FC6050 /* TemperatureTextBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20B4681A2793A7E300FC6050 /* TemperatureTextBuilder.swift */; }; 20B468222793B85900FC6050 /* SystemLocationWeatherRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20B468212793B85900FC6050 /* SystemLocationWeatherRepository.swift */; }; 20B857362CC3304C0098DB1D /* UVIndexTextBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20B857352CC3304C0098DB1D /* UVIndexTextBuilder.swift */; }; 20B9CDCD27B8325900C42508 /* WeatherSourceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20B9CDCC27B8325900C42508 /* WeatherSourceTests.swift */; }; 20B9CDCF27B8335A00C42508 /* TemperatureUnitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20B9CDCE27B8335A00C42508 /* TemperatureUnitTests.swift */; }; 20B9CDD127B833EE00C42508 /* RefreshIntervalTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20B9CDD027B833EE00C42508 /* RefreshIntervalTests.swift */; }; 20BBCDAA278B8A18007DEEB0 /* WeatherViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20BBCDA9278B8A18007DEEB0 /* WeatherViewModel.swift */; }; 20BBCDAD278B8B28007DEEB0 /* WeatherViewModelType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20BBCDAC278B8B28007DEEB0 /* WeatherViewModelType.swift */; }; 20BBCDAF278B92A7007DEEB0 /* SystemLocationFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20BBCDAE278B92A7007DEEB0 /* SystemLocationFetcher.swift */; }; 20C6722A2E9DEDCB00A577C1 /* Task+Retry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20C672292E9DEDCB00A577C1 /* Task+Retry.swift */; }; 20CA6E11278F49AC00FFC53A /* TemperatureFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20CA6E10278F49AC00FFC53A /* TemperatureFormatter.swift */; }; 20D857102A8317F5005727BB /* ConfigureUnitOptionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20D8570F2A8317F5005727BB /* ConfigureUnitOptionsView.swift */; }; 20D857122A831802005727BB /* ConfigureWeatherOptionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20D857112A831802005727BB /* ConfigureWeatherOptionsView.swift */; }; 20D857142A831A16005727BB /* ConfigureValueSeparatorOptionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20D857132A831A16005727BB /* ConfigureValueSeparatorOptionsView.swift */; }; 20D8571B2A831D62005727BB /* WeatherRepositoryType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20D8571A2A831D62005727BB /* WeatherRepositoryType.swift */; }; 20D8571D2A831F40005727BB /* WeatherRepositoryFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20D8571C2A831F40005727BB /* WeatherRepositoryFactory.swift */; }; 20D8571F2A832AC6005727BB /* TemperatureUnit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20D8571E2A832AC6005727BB /* TemperatureUnit.swift */; }; 20E8A1A62C2B3C5A007E8733 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 20E8A1A52C2B3C5A007E8733 /* Localizable.xcstrings */; }; 20E8A1AB2C2B3FE6007E8733 /* TestData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20E8A1AA2C2B3FE6007E8733 /* TestData.swift */; }; 20F0E5F82C33500900434C3A /* AirQuality.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20F0E5F72C33500900434C3A /* AirQuality.swift */; }; 20F17D3A26597A02003A164E /* WeatherData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20F17D3926597A02003A164E /* WeatherData.swift */; }; E3105BA62818805A00FB4C55 /* SunriseAndSunsetTextBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3105BA52818805A00FB4C55 /* SunriseAndSunsetTextBuilder.swift */; }; E3BE2C91292C505A00C4F468 /* DropdownIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3BE2C90292C505A00C4F468 /* DropdownIcon.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ 61D04A341C7B7BCF00CBE6AE /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 2023ED9B1C4ED09C0087FD67 /* Project object */; proxyType = 1; remoteGlobalIDString = 2023EDA21C4ED09C0087FD67; remoteInfo = SwiftWeather; }; /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ 2000D4072AD86DAA0052EDA6 /* WindSpeedFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindSpeedFormatter.swift; sourceTree = ""; }; 200520F52C29426E00006FC0 /* WeatherDataFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherDataFormatter.swift; sourceTree = ""; }; 2005C054278CB4E40067BBD1 /* WeatherValidatorType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherValidatorType.swift; sourceTree = ""; }; 2005C056278CB5640067BBD1 /* LocationParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationParser.swift; sourceTree = ""; }; 2005C058278CB5FC0067BBD1 /* WeatherURLBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherURLBuilder.swift; sourceTree = ""; }; 2005C05C278CE0350067BBD1 /* WeatherAPIResponseParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherAPIResponseParser.swift; sourceTree = ""; }; 201C6C4520262E380065E795 /* WeatherAPIResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherAPIResponse.swift; sourceTree = ""; }; 20206F0627BFF3D7004B418F /* ConfigureView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigureView.swift; sourceTree = ""; }; 2023EDA31C4ED09C0087FD67 /* DatWeatherDoe.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = DatWeatherDoe.app; sourceTree = BUILT_PRODUCTS_DIR; }; 2023EDA81C4ED09C0087FD67 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 2023EDAD1C4ED09C0087FD67 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 202B1014278D46AB00ED6D42 /* WeatherConditionBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherConditionBuilder.swift; sourceTree = ""; }; 202B101D278D4F1900ED6D42 /* WeatherReachability.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherReachability.swift; sourceTree = ""; }; 202B1028278D5A7100ED6D42 /* WeatherForecaster.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherForecaster.swift; sourceTree = ""; }; 202B102F278D632800ED6D42 /* APIKeyParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIKeyParser.swift; sourceTree = ""; }; 2039B3DE2C278AA4006A6B6D /* SunriseSunsetData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SunriseSunsetData.swift; sourceTree = ""; }; 2039B3E22C289697006A6B6D /* TemperatureData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemperatureData.swift; sourceTree = ""; }; 2039B3E62C2896BD006A6B6D /* WindData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindData.swift; sourceTree = ""; }; 2039B3E82C2896CF006A6B6D /* ForecastData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForecastData.swift; sourceTree = ""; }; 2039B3EA2C2896D7006A6B6D /* ForecastTemperatureData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForecastTemperatureData.swift; sourceTree = ""; }; 2039B3EE2C28996D006A6B6D /* WeatherCondition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherCondition.swift; sourceTree = ""; }; 2039B3F82C28D18A006A6B6D /* DatWeatherDoeApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatWeatherDoeApp.swift; sourceTree = ""; }; 2039B3FA2C28D1B0006A6B6D /* CustomButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomButton.swift; sourceTree = ""; }; 2039B3FC2C28D1C0006A6B6D /* MenuOptionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuOptionsView.swift; sourceTree = ""; }; 2039B3FE2C28D1D3006A6B6D /* NonInteractiveMenuOptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NonInteractiveMenuOptionView.swift; sourceTree = ""; }; 2039B4002C291B5A006A6B6D /* MenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuView.swift; sourceTree = ""; }; 2039B4032C291C35006A6B6D /* StatusBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusBarView.swift; sourceTree = ""; }; 2044E91D2867D3CF00AED55B /* TemperatureForecastTextBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemperatureForecastTextBuilder.swift; sourceTree = ""; }; 204597672A84492400CF73CE /* TemperatureWithDegreesCreator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemperatureWithDegreesCreator.swift; sourceTree = ""; }; 20459C631C5C50DA004D0DC1 /* ConfigManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConfigManager.swift; sourceTree = ""; }; 206523C726597B120026C506 /* WeatherError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherError.swift; sourceTree = ""; }; 206523D52659A92B0026C506 /* WeatherDataBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherDataBuilder.swift; sourceTree = ""; }; 206523FA265AD5730026C506 /* WeatherSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherSource.swift; sourceTree = ""; }; 206523FC265AF03E0026C506 /* RefreshInterval.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshInterval.swift; sourceTree = ""; }; 206E15242A7C4C5C0096D33C /* ConfigOptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigOptions.swift; sourceTree = ""; }; 206E152E2A7C544D0096D33C /* ConfigureOptionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigureOptionsView.swift; sourceTree = ""; }; 206FF1BA2BB4BB9400111EAE /* WeatherConditionPosition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherConditionPosition.swift; sourceTree = ""; }; 2074949827A09278002AA589 /* WeatherURLBuilderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherURLBuilderTests.swift; sourceTree = ""; }; 2077BC51278DF98800E0453C /* WeatherConditionTextMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherConditionTextMapper.swift; sourceTree = ""; }; 207E989B26838D0D00DC2162 /* DatWeatherDoe.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = DatWeatherDoe.xctestplan; sourceTree = ""; }; 2085263427E80B9E0017D7F4 /* ConfigureViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigureViewModel.swift; sourceTree = ""; }; 209482C529934BFF00AF39D4 /* MeasurementUnit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeasurementUnit.swift; sourceTree = ""; }; 209F8A37279136D300EB5C45 /* LocationValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationValidator.swift; sourceTree = ""; }; 209F8A3C27914A5900EB5C45 /* NetworkClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkClient.swift; sourceTree = ""; }; 209F8A4027915DBC00EB5C45 /* LocationCoordinatesWeatherRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationCoordinatesWeatherRepository.swift; sourceTree = ""; }; 20B3845E27A1CFE800F85482 /* LocationValidatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationValidatorTests.swift; sourceTree = ""; }; 20B46813279394FB00FC6050 /* WeatherTextBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherTextBuilder.swift; sourceTree = ""; }; 20B468182793989B00FC6050 /* HumidityTextBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HumidityTextBuilder.swift; sourceTree = ""; }; 20B4681A2793A7E300FC6050 /* TemperatureTextBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemperatureTextBuilder.swift; sourceTree = ""; }; 20B468212793B85900FC6050 /* SystemLocationWeatherRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemLocationWeatherRepository.swift; sourceTree = ""; }; 20B857352CC3304C0098DB1D /* UVIndexTextBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UVIndexTextBuilder.swift; sourceTree = ""; }; 20B9CDCC27B8325900C42508 /* WeatherSourceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherSourceTests.swift; sourceTree = ""; }; 20B9CDCE27B8335A00C42508 /* TemperatureUnitTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemperatureUnitTests.swift; sourceTree = ""; }; 20B9CDD027B833EE00C42508 /* RefreshIntervalTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshIntervalTests.swift; sourceTree = ""; }; 20BBCDA9278B8A18007DEEB0 /* WeatherViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherViewModel.swift; sourceTree = ""; }; 20BBCDAC278B8B28007DEEB0 /* WeatherViewModelType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherViewModelType.swift; sourceTree = ""; }; 20BBCDAE278B92A7007DEEB0 /* SystemLocationFetcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemLocationFetcher.swift; sourceTree = ""; }; 20C672292E9DEDCB00A577C1 /* Task+Retry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Task+Retry.swift"; sourceTree = ""; }; 20CA6E10278F49AC00FFC53A /* TemperatureFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemperatureFormatter.swift; sourceTree = ""; }; 20CB480B27B83EF90043C60F /* Config.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = ""; }; 20D8570F2A8317F5005727BB /* ConfigureUnitOptionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigureUnitOptionsView.swift; sourceTree = ""; }; 20D857112A831802005727BB /* ConfigureWeatherOptionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigureWeatherOptionsView.swift; sourceTree = ""; }; 20D857132A831A16005727BB /* ConfigureValueSeparatorOptionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigureValueSeparatorOptionsView.swift; sourceTree = ""; }; 20D8571A2A831D62005727BB /* WeatherRepositoryType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherRepositoryType.swift; sourceTree = ""; }; 20D8571C2A831F40005727BB /* WeatherRepositoryFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherRepositoryFactory.swift; sourceTree = ""; }; 20D8571E2A832AC6005727BB /* TemperatureUnit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemperatureUnit.swift; sourceTree = ""; }; 20E8A1A52C2B3C5A007E8733 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = ""; }; 20E8A1AA2C2B3FE6007E8733 /* TestData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestData.swift; sourceTree = ""; }; 20F0E5F72C33500900434C3A /* AirQuality.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AirQuality.swift; sourceTree = ""; }; 20F17D3926597A02003A164E /* WeatherData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WeatherData.swift; sourceTree = ""; }; 20FF84E92456326400FC9DAA /* DatWeatherDoe.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DatWeatherDoe.entitlements; sourceTree = ""; }; 61D04A2F1C7B7BCF00CBE6AE /* DatWeatherDoeTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = DatWeatherDoeTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; E3105BA52818805A00FB4C55 /* SunriseAndSunsetTextBuilder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SunriseAndSunsetTextBuilder.swift; sourceTree = ""; }; E3BE2C90292C505A00C4F468 /* DropdownIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DropdownIcon.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ 2023EDA01C4ED09C0087FD67 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 2039B4072C2920BF006A6B6D /* MenuBarExtraAccess in Frameworks */, 20012FA6267980EE00553B60 /* Reachability in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; 61D04A2C1C7B7BCF00CBE6AE /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 2005C050278CB4040067BBD1 /* Repository */ = { isa = PBXGroup; children = ( 20B468202793B81700FC6050 /* Coordinates */, 20B4681F2793B81200FC6050 /* System */, 2005C058278CB5FC0067BBD1 /* WeatherURLBuilder.swift */, 20D8571A2A831D62005727BB /* WeatherRepositoryType.swift */, 2005C054278CB4E40067BBD1 /* WeatherValidatorType.swift */, 20D8571C2A831F40005727BB /* WeatherRepositoryFactory.swift */, ); path = Repository; sourceTree = ""; }; 2023ED9A1C4ED09C0087FD67 = { isa = PBXGroup; children = ( 20CB480B27B83EF90043C60F /* Config.xcconfig */, 2023EDA51C4ED09C0087FD67 /* DatWeatherDoe */, 61D04A301C7B7BCF00CBE6AE /* DatWeatherDoeTests */, 2023EDA41C4ED09C0087FD67 /* Products */, BF848D6F74093EEA00A37228 /* Frameworks */, ); sourceTree = ""; }; 2023EDA41C4ED09C0087FD67 /* Products */ = { isa = PBXGroup; children = ( 2023EDA31C4ED09C0087FD67 /* DatWeatherDoe.app */, 61D04A2F1C7B7BCF00CBE6AE /* DatWeatherDoeTests.xctest */, ); name = Products; sourceTree = ""; }; 2023EDA51C4ED09C0087FD67 /* DatWeatherDoe */ = { isa = PBXGroup; children = ( 206523CE2659838A0026C506 /* API */, 206523CF265983A90026C506 /* Config */, 202B101F278D4F2100ED6D42 /* Reachability */, 206523CC2659835D0026C506 /* UI */, 20BBCDAB278B8B1C007DEEB0 /* ViewModel */, 206523CD265983750026C506 /* Resources */, 2039B3F82C28D18A006A6B6D /* DatWeatherDoeApp.swift */, ); path = DatWeatherDoe; sourceTree = ""; }; 202B1016278D481100ED6D42 /* Condition */ = { isa = PBXGroup; children = ( 2039B3EE2C28996D006A6B6D /* WeatherCondition.swift */, 202B1014278D46AB00ED6D42 /* WeatherConditionBuilder.swift */, 2077BC51278DF98800E0453C /* WeatherConditionTextMapper.swift */, ); path = Condition; sourceTree = ""; }; 202B101F278D4F2100ED6D42 /* Reachability */ = { isa = PBXGroup; children = ( 202B101D278D4F1900ED6D42 /* WeatherReachability.swift */, ); path = Reachability; sourceTree = ""; }; 202B1027278D58B900ED6D42 /* Menu Bar */ = { isa = PBXGroup; children = ( 2000D4072AD86DAA0052EDA6 /* WindSpeedFormatter.swift */, E3BE2C90292C505A00C4F468 /* DropdownIcon.swift */, 2039B3FA2C28D1B0006A6B6D /* CustomButton.swift */, 2039B3FC2C28D1C0006A6B6D /* MenuOptionsView.swift */, 2039B3FE2C28D1D3006A6B6D /* NonInteractiveMenuOptionView.swift */, 2039B4002C291B5A006A6B6D /* MenuView.swift */, ); path = "Menu Bar"; sourceTree = ""; }; 2039B4022C291C19006A6B6D /* Status Bar */ = { isa = PBXGroup; children = ( 2039B4032C291C35006A6B6D /* StatusBarView.swift */, ); path = "Status Bar"; sourceTree = ""; }; 206523CC2659835D0026C506 /* UI */ = { isa = PBXGroup; children = ( 20CA6E0D278F3FDF00FFC53A /* Configure */, 206523D72659CC4C0026C506 /* Decorator */, 209F8A332790EC5000EB5C45 /* Forecaster */, 202B1027278D58B900ED6D42 /* Menu Bar */, 2039B4022C291C19006A6B6D /* Status Bar */, ); path = UI; sourceTree = ""; }; 206523CD265983750026C506 /* Resources */ = { isa = PBXGroup; children = ( 20E8A1A92C2B3E4A007E8733 /* DevelopmentAssets */, 20652400265B0F870026C506 /* Localization */, 20FF84E92456326400FC9DAA /* DatWeatherDoe.entitlements */, 2023EDA81C4ED09C0087FD67 /* Assets.xcassets */, 2023EDAD1C4ED09C0087FD67 /* Info.plist */, ); path = Resources; sourceTree = ""; }; 206523CE2659838A0026C506 /* API */ = { isa = PBXGroup; children = ( 209F8A3927913D0400EB5C45 /* Response */, 209F8A3C27914A5900EB5C45 /* NetworkClient.swift */, 20F17D3926597A02003A164E /* WeatherData.swift */, 206523C726597B120026C506 /* WeatherError.swift */, ); path = API; sourceTree = ""; }; 206523CF265983A90026C506 /* Config */ = { isa = PBXGroup; children = ( 206E15242A7C4C5C0096D33C /* ConfigOptions.swift */, 20459C631C5C50DA004D0DC1 /* ConfigManager.swift */, 202B102F278D632800ED6D42 /* APIKeyParser.swift */, ); path = Config; sourceTree = ""; }; 206523D72659CC4C0026C506 /* Decorator */ = { isa = PBXGroup; children = ( 202B1016278D481100ED6D42 /* Condition */, 20B468152793958800FC6050 /* Text */, 206523D52659A92B0026C506 /* WeatherDataBuilder.swift */, ); path = Decorator; sourceTree = ""; }; 20652400265B0F870026C506 /* Localization */ = { isa = PBXGroup; children = ( 20E8A1A52C2B3C5A007E8733 /* Localizable.xcstrings */, ); path = Localization; sourceTree = ""; }; 2074949627A09263002AA589 /* API */ = { isa = PBXGroup; children = ( 2074949727A0926E002AA589 /* Repository */, ); path = API; sourceTree = ""; }; 2074949727A0926E002AA589 /* Repository */ = { isa = PBXGroup; children = ( 2074949A27A09337002AA589 /* Location */, 2074949827A09278002AA589 /* WeatherURLBuilderTests.swift */, ); path = Repository; sourceTree = ""; }; 2074949A27A09337002AA589 /* Location */ = { isa = PBXGroup; children = ( 20B3845D27A1CFC200F85482 /* Coordinates */, ); path = Location; sourceTree = ""; }; 209F8A332790EC5000EB5C45 /* Forecaster */ = { isa = PBXGroup; children = ( 202B1028278D5A7100ED6D42 /* WeatherForecaster.swift */, ); path = Forecaster; sourceTree = ""; }; 209F8A3927913D0400EB5C45 /* Response */ = { isa = PBXGroup; children = ( 201C6C4520262E380065E795 /* WeatherAPIResponse.swift */, 2005C05C278CE0350067BBD1 /* WeatherAPIResponseParser.swift */, 2039B3DE2C278AA4006A6B6D /* SunriseSunsetData.swift */, 2039B3E22C289697006A6B6D /* TemperatureData.swift */, 2039B3E62C2896BD006A6B6D /* WindData.swift */, 2039B3E82C2896CF006A6B6D /* ForecastData.swift */, 2039B3EA2C2896D7006A6B6D /* ForecastTemperatureData.swift */, 20F0E5F72C33500900434C3A /* AirQuality.swift */, ); path = Response; sourceTree = ""; }; 20AF0D962795257100AA7D18 /* Options */ = { isa = PBXGroup; children = ( 20D8571E2A832AC6005727BB /* TemperatureUnit.swift */, 209482C529934BFF00AF39D4 /* MeasurementUnit.swift */, 206523FA265AD5730026C506 /* WeatherSource.swift */, 206523FC265AF03E0026C506 /* RefreshInterval.swift */, 206FF1BA2BB4BB9400111EAE /* WeatherConditionPosition.swift */, ); path = Options; sourceTree = ""; }; 20B3845D27A1CFC200F85482 /* Coordinates */ = { isa = PBXGroup; children = ( 20B3845E27A1CFE800F85482 /* LocationValidatorTests.swift */, ); path = Coordinates; sourceTree = ""; }; 20B468152793958800FC6050 /* Text */ = { isa = PBXGroup; children = ( 20B4681C2793AA0E00FC6050 /* Temperature */, 20B468182793989B00FC6050 /* HumidityTextBuilder.swift */, E3105BA52818805A00FB4C55 /* SunriseAndSunsetTextBuilder.swift */, 20B46813279394FB00FC6050 /* WeatherTextBuilder.swift */, 20B857352CC3304C0098DB1D /* UVIndexTextBuilder.swift */, ); path = Text; sourceTree = ""; }; 20B4681C2793AA0E00FC6050 /* Temperature */ = { isa = PBXGroup; children = ( 20B4681A2793A7E300FC6050 /* TemperatureTextBuilder.swift */, 20CA6E10278F49AC00FFC53A /* TemperatureFormatter.swift */, 2044E91D2867D3CF00AED55B /* TemperatureForecastTextBuilder.swift */, 204597672A84492400CF73CE /* TemperatureWithDegreesCreator.swift */, ); path = Temperature; sourceTree = ""; }; 20B4681F2793B81200FC6050 /* System */ = { isa = PBXGroup; children = ( 20B468212793B85900FC6050 /* SystemLocationWeatherRepository.swift */, 20BBCDAE278B92A7007DEEB0 /* SystemLocationFetcher.swift */, 20C672292E9DEDCB00A577C1 /* Task+Retry.swift */, ); path = System; sourceTree = ""; }; 20B468202793B81700FC6050 /* Coordinates */ = { isa = PBXGroup; children = ( 209F8A4027915DBC00EB5C45 /* LocationCoordinatesWeatherRepository.swift */, 209F8A37279136D300EB5C45 /* LocationValidator.swift */, 2005C056278CB5640067BBD1 /* LocationParser.swift */, ); path = Coordinates; sourceTree = ""; }; 20B9CDC927B8323900C42508 /* UI */ = { isa = PBXGroup; children = ( 20B9CDCA27B8324300C42508 /* Configure */, ); path = UI; sourceTree = ""; }; 20B9CDCA27B8324300C42508 /* Configure */ = { isa = PBXGroup; children = ( 20B9CDCB27B8324900C42508 /* Options */, ); path = Configure; sourceTree = ""; }; 20B9CDCB27B8324900C42508 /* Options */ = { isa = PBXGroup; children = ( 20B9CDCC27B8325900C42508 /* WeatherSourceTests.swift */, 20B9CDCE27B8335A00C42508 /* TemperatureUnitTests.swift */, 20B9CDD027B833EE00C42508 /* RefreshIntervalTests.swift */, ); path = Options; sourceTree = ""; }; 20BBCDAB278B8B1C007DEEB0 /* ViewModel */ = { isa = PBXGroup; children = ( 2005C050278CB4040067BBD1 /* Repository */, 20BBCDAC278B8B28007DEEB0 /* WeatherViewModelType.swift */, 20BBCDA9278B8A18007DEEB0 /* WeatherViewModel.swift */, 200520F52C29426E00006FC0 /* WeatherDataFormatter.swift */, ); path = ViewModel; sourceTree = ""; }; 20CA6E0D278F3FDF00FFC53A /* Configure */ = { isa = PBXGroup; children = ( 20AF0D962795257100AA7D18 /* Options */, 2085263427E80B9E0017D7F4 /* ConfigureViewModel.swift */, 20206F0627BFF3D7004B418F /* ConfigureView.swift */, 206E152E2A7C544D0096D33C /* ConfigureOptionsView.swift */, 20D857112A831802005727BB /* ConfigureWeatherOptionsView.swift */, 20D8570F2A8317F5005727BB /* ConfigureUnitOptionsView.swift */, 20D857132A831A16005727BB /* ConfigureValueSeparatorOptionsView.swift */, ); path = Configure; sourceTree = ""; }; 20E8A1A92C2B3E4A007E8733 /* DevelopmentAssets */ = { isa = PBXGroup; children = ( 20E8A1AA2C2B3FE6007E8733 /* TestData.swift */, ); path = DevelopmentAssets; sourceTree = ""; }; 61D04A301C7B7BCF00CBE6AE /* DatWeatherDoeTests */ = { isa = PBXGroup; children = ( 207E989B26838D0D00DC2162 /* DatWeatherDoe.xctestplan */, 2074949627A09263002AA589 /* API */, 20B9CDC927B8323900C42508 /* UI */, ); path = DatWeatherDoeTests; sourceTree = ""; }; BF848D6F74093EEA00A37228 /* Frameworks */ = { isa = PBXGroup; children = ( ); name = Frameworks; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ 2023EDA21C4ED09C0087FD67 /* DatWeatherDoe */ = { isa = PBXNativeTarget; buildConfigurationList = 2023EDB01C4ED09C0087FD67 /* Build configuration list for PBXNativeTarget "DatWeatherDoe" */; buildPhases = ( 2023ED9F1C4ED09C0087FD67 /* Sources */, 2023EDA01C4ED09C0087FD67 /* Frameworks */, 2023EDA11C4ED09C0087FD67 /* Resources */, ); buildRules = ( ); dependencies = ( 20CB68BE2A2D9033001C73B9 /* PBXTargetDependency */, ); name = DatWeatherDoe; packageProductDependencies = ( 20012FA5267980EE00553B60 /* Reachability */, 2039B4062C2920BF006A6B6D /* MenuBarExtraAccess */, ); productName = SwiftWeather; productReference = 2023EDA31C4ED09C0087FD67 /* DatWeatherDoe.app */; productType = "com.apple.product-type.application"; }; 61D04A2E1C7B7BCF00CBE6AE /* DatWeatherDoeTests */ = { isa = PBXNativeTarget; buildConfigurationList = 61D04A381C7B7BCF00CBE6AE /* Build configuration list for PBXNativeTarget "DatWeatherDoeTests" */; buildPhases = ( 61D04A2B1C7B7BCF00CBE6AE /* Sources */, 61D04A2C1C7B7BCF00CBE6AE /* Frameworks */, 61D04A2D1C7B7BCF00CBE6AE /* Resources */, ); buildRules = ( ); dependencies = ( 61D04A351C7B7BCF00CBE6AE /* PBXTargetDependency */, ); name = DatWeatherDoeTests; packageProductDependencies = ( ); productName = DefaultsTests; productReference = 61D04A2F1C7B7BCF00CBE6AE /* DatWeatherDoeTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 2023ED9B1C4ED09C0087FD67 /* Project object */ = { isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = YES; LastSwiftUpdateCheck = 1330; LastUpgradeCheck = 2600; ORGANIZATIONNAME = "Inder Dhir"; TargetAttributes = { 2023EDA21C4ED09C0087FD67 = { CreatedOnToolsVersion = 7.2; LastSwiftMigration = 1000; SystemCapabilities = { com.apple.Sandbox = { enabled = 0; }; }; }; 61D04A2E1C7B7BCF00CBE6AE = { CreatedOnToolsVersion = 7.2.1; LastSwiftMigration = 0810; TestTargetID = 2023EDA21C4ED09C0087FD67; }; }; }; buildConfigurationList = 2023ED9E1C4ED09C0087FD67 /* Build configuration list for PBXProject "DatWeatherDoe" */; compatibilityVersion = "Xcode 12.0"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, fr, de, it, ja, "zh-Hans", ); mainGroup = 2023ED9A1C4ED09C0087FD67; packageReferences = ( 20012FA4267980EE00553B60 /* XCRemoteSwiftPackageReference "Reachability" */, 20CB68BC2A2D9029001C73B9 /* XCRemoteSwiftPackageReference "SwiftLint" */, 2000D40D2AD88D6B0052EDA6 /* XCRemoteSwiftPackageReference "SwiftFormat" */, 2039B4052C2920BF006A6B6D /* XCRemoteSwiftPackageReference "MenuBarExtraAccess" */, ); productRefGroup = 2023EDA41C4ED09C0087FD67 /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( 2023EDA21C4ED09C0087FD67 /* DatWeatherDoe */, 61D04A2E1C7B7BCF00CBE6AE /* DatWeatherDoeTests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ 2023EDA11C4ED09C0087FD67 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 2023EDA91C4ED09C0087FD67 /* Assets.xcassets in Resources */, 20E8A1A62C2B3C5A007E8733 /* Localizable.xcstrings in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; 61D04A2D1C7B7BCF00CBE6AE /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ 2023ED9F1C4ED09C0087FD67 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 209174102A9BDA4A00BB63E0 /* ConfigureViewModel.swift in Sources */, 2005C055278CB4E40067BBD1 /* WeatherValidatorType.swift in Sources */, 20B468222793B85900FC6050 /* SystemLocationWeatherRepository.swift in Sources */, 20E8A1AB2C2B3FE6007E8733 /* TestData.swift in Sources */, 2039B3FB2C28D1B0006A6B6D /* CustomButton.swift in Sources */, 20BBCDAF278B92A7007DEEB0 /* SystemLocationFetcher.swift in Sources */, 209482C629934BFF00AF39D4 /* MeasurementUnit.swift in Sources */, 2005C057278CB5640067BBD1 /* LocationParser.swift in Sources */, 2005C05D278CE0350067BBD1 /* WeatherAPIResponseParser.swift in Sources */, 20F0E5F82C33500900434C3A /* AirQuality.swift in Sources */, 2000D4082AD86DAA0052EDA6 /* WindSpeedFormatter.swift in Sources */, 2039B3FF2C28D1D3006A6B6D /* NonInteractiveMenuOptionView.swift in Sources */, 20459C641C5C50DA004D0DC1 /* ConfigManager.swift in Sources */, 2039B3F92C28D18A006A6B6D /* DatWeatherDoeApp.swift in Sources */, 2039B3E32C289697006A6B6D /* TemperatureData.swift in Sources */, 2039B3FD2C28D1C0006A6B6D /* MenuOptionsView.swift in Sources */, 20B4681B2793A7E300FC6050 /* TemperatureTextBuilder.swift in Sources */, 20BBCDAA278B8A18007DEEB0 /* WeatherViewModel.swift in Sources */, 2039B3EF2C28996D006A6B6D /* WeatherCondition.swift in Sources */, 204597682A84492400CF73CE /* TemperatureWithDegreesCreator.swift in Sources */, E3BE2C91292C505A00C4F468 /* DropdownIcon.swift in Sources */, 202B1030278D632800ED6D42 /* APIKeyParser.swift in Sources */, 209F8A4127915DBC00EB5C45 /* LocationCoordinatesWeatherRepository.swift in Sources */, E3105BA62818805A00FB4C55 /* SunriseAndSunsetTextBuilder.swift in Sources */, 202B101E278D4F1900ED6D42 /* WeatherReachability.swift in Sources */, 2039B3DF2C278AA4006A6B6D /* SunriseSunsetData.swift in Sources */, 2039B4012C291B5A006A6B6D /* MenuView.swift in Sources */, 20B46814279394FB00FC6050 /* WeatherTextBuilder.swift in Sources */, 20CA6E11278F49AC00FFC53A /* TemperatureFormatter.swift in Sources */, 2077BC52278DF98800E0453C /* WeatherConditionTextMapper.swift in Sources */, 20BBCDAD278B8B28007DEEB0 /* WeatherViewModelType.swift in Sources */, 20D8571F2A832AC6005727BB /* TemperatureUnit.swift in Sources */, 20D857102A8317F5005727BB /* ConfigureUnitOptionsView.swift in Sources */, 20C6722A2E9DEDCB00A577C1 /* Task+Retry.swift in Sources */, 202B1029278D5A7100ED6D42 /* WeatherForecaster.swift in Sources */, 206523C826597B120026C506 /* WeatherError.swift in Sources */, 209F8A38279136D300EB5C45 /* LocationValidator.swift in Sources */, 20D8571B2A831D62005727BB /* WeatherRepositoryType.swift in Sources */, 2044E91E2867D3CF00AED55B /* TemperatureForecastTextBuilder.swift in Sources */, 20D857142A831A16005727BB /* ConfigureValueSeparatorOptionsView.swift in Sources */, 201C6C4620262E380065E795 /* WeatherAPIResponse.swift in Sources */, 206E15252A7C4C5C0096D33C /* ConfigOptions.swift in Sources */, 2039B3E72C2896BD006A6B6D /* WindData.swift in Sources */, 20B857362CC3304C0098DB1D /* UVIndexTextBuilder.swift in Sources */, 2005C059278CB5FC0067BBD1 /* WeatherURLBuilder.swift in Sources */, 20D857122A831802005727BB /* ConfigureWeatherOptionsView.swift in Sources */, 20D8571D2A831F40005727BB /* WeatherRepositoryFactory.swift in Sources */, 20B468192793989B00FC6050 /* HumidityTextBuilder.swift in Sources */, 206523D62659A92B0026C506 /* WeatherDataBuilder.swift in Sources */, 206FF1BB2BB4BB9400111EAE /* WeatherConditionPosition.swift in Sources */, 20206F0727BFF3D7004B418F /* ConfigureView.swift in Sources */, 200520F62C29426E00006FC0 /* WeatherDataFormatter.swift in Sources */, 2039B3EB2C2896D7006A6B6D /* ForecastTemperatureData.swift in Sources */, 20F17D3A26597A02003A164E /* WeatherData.swift in Sources */, 202B1015278D46AB00ED6D42 /* WeatherConditionBuilder.swift in Sources */, 206E152F2A7C544D0096D33C /* ConfigureOptionsView.swift in Sources */, 2039B3E92C2896CF006A6B6D /* ForecastData.swift in Sources */, 209F8A3D27914A5900EB5C45 /* NetworkClient.swift in Sources */, 2039B4042C291C35006A6B6D /* StatusBarView.swift in Sources */, 206523FD265AF03E0026C506 /* RefreshInterval.swift in Sources */, 206523FB265AD5730026C506 /* WeatherSource.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 61D04A2B1C7B7BCF00CBE6AE /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 2074949927A09278002AA589 /* WeatherURLBuilderTests.swift in Sources */, 20B9CDCF27B8335A00C42508 /* TemperatureUnitTests.swift in Sources */, 20B3845F27A1CFE800F85482 /* LocationValidatorTests.swift in Sources */, 20B9CDD127B833EE00C42508 /* RefreshIntervalTests.swift in Sources */, 20B9CDCD27B8325900C42508 /* WeatherSourceTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ 20CB68BE2A2D9033001C73B9 /* PBXTargetDependency */ = { isa = PBXTargetDependency; productRef = 20CB68BD2A2D9033001C73B9 /* SwiftLintPlugin */; }; 61D04A351C7B7BCF00CBE6AE /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 2023EDA21C4ED09C0087FD67 /* DatWeatherDoe */; targetProxy = 61D04A341C7B7BCF00CBE6AE /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ 2023EDAE1C4ED09C0087FD67 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 20CB480B27B83EF90043C60F /* Config.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "-"; COPY_PHASE_STRIP = NO; DEAD_CODE_STRIPPING = YES; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MACOSX_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; STRING_CATALOG_GENERATE_SYMBOLS = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 6.0; }; name = Debug; }; 2023EDAF1C4ED09C0087FD67 /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = 20CB480B27B83EF90043C60F /* Config.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "-"; COPY_PHASE_STRIP = NO; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MACOSX_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = macosx; STRING_CATALOG_GENERATE_SYMBOLS = YES; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OPTIMIZATION_LEVEL = "-O"; SWIFT_VERSION = 6.0; }; name = Release; }; 2023EDB11C4ED09C0087FD67 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = DatWeatherDoe/Resources/DatWeatherDoe.entitlements; CODE_SIGN_IDENTITY = "Mac Developer"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Mac Developer"; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 57; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"DatWeatherDoe/Resources/DevelopmentAssets\" \"DatWeatherDoe/Resources/DevelopmentAssets/TestData.swift\""; DEVELOPMENT_TEAM = Q8X4D3A8MT; ENABLE_APP_SANDBOX = YES; ENABLE_HARDENED_RUNTIME = YES; ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES; ENABLE_RESOURCE_ACCESS_LOCATION = YES; INFOPLIST_FILE = "$(SRCROOT)/DatWeatherDoe/Resources/Info.plist"; INFOPLIST_KEY_CFBundleDisplayName = DatWeatherDoe; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.weather"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); MARKETING_VERSION = 5.6.2; PRODUCT_BUNDLE_IDENTIFIER = com.inderdhir.DatWeatherDoe.debug; PRODUCT_NAME = DatWeatherDoe; PROVISIONING_PROFILE = ""; SWIFT_APPROACHABLE_CONCURRENCY = YES; SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; SWIFT_STRICT_CONCURRENCY = complete; SWIFT_SWIFT3_OBJC_INFERENCE = Default; }; name = Debug; }; 2023EDB21C4ED09C0087FD67 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = DatWeatherDoe/Resources/DatWeatherDoe.entitlements; CODE_SIGN_IDENTITY = "Mac Developer"; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Mac Developer"; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 57; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"DatWeatherDoe/Resources/DevelopmentAssets\" \"DatWeatherDoe/Resources/DevelopmentAssets/TestData.swift\""; DEVELOPMENT_TEAM = Q8X4D3A8MT; ENABLE_APP_SANDBOX = YES; ENABLE_HARDENED_RUNTIME = YES; ENABLE_OUTGOING_NETWORK_CONNECTIONS = YES; ENABLE_RESOURCE_ACCESS_LOCATION = YES; INFOPLIST_FILE = "$(SRCROOT)/DatWeatherDoe/Resources/Info.plist"; INFOPLIST_KEY_CFBundleDisplayName = DatWeatherDoe; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.weather"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); MARKETING_VERSION = 5.6.2; PRODUCT_BUNDLE_IDENTIFIER = com.inderdhir.DatWeatherDoe; PRODUCT_NAME = DatWeatherDoe; PROVISIONING_PROFILE = ""; SWIFT_APPROACHABLE_CONCURRENCY = YES; SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; SWIFT_STRICT_CONCURRENCY = complete; SWIFT_SWIFT3_OBJC_INFERENCE = Default; }; name = Release; }; 61D04A361C7B7BCF00CBE6AE /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = Q8X4D3A8MT; GENERATE_INFOPLIST_FILE = YES; MACOSX_DEPLOYMENT_TARGET = 14.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.inderdhir.DefaultsTests; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/DatWeatherDoe.app/Contents/MacOS/DatWeatherDoe"; }; name = Debug; }; 61D04A371C7B7BCF00CBE6AE /* Release */ = { isa = XCBuildConfiguration; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_IDENTITY = "Apple Distribution"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = ""; GENERATE_INFOPLIST_FILE = YES; MACOSX_DEPLOYMENT_TARGET = 14.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.inderdhir.DefaultsTests; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/DatWeatherDoe.app/Contents/MacOS/DatWeatherDoe"; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ 2023ED9E1C4ED09C0087FD67 /* Build configuration list for PBXProject "DatWeatherDoe" */ = { isa = XCConfigurationList; buildConfigurations = ( 2023EDAE1C4ED09C0087FD67 /* Debug */, 2023EDAF1C4ED09C0087FD67 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 2023EDB01C4ED09C0087FD67 /* Build configuration list for PBXNativeTarget "DatWeatherDoe" */ = { isa = XCConfigurationList; buildConfigurations = ( 2023EDB11C4ED09C0087FD67 /* Debug */, 2023EDB21C4ED09C0087FD67 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 61D04A381C7B7BCF00CBE6AE /* Build configuration list for PBXNativeTarget "DatWeatherDoeTests" */ = { isa = XCConfigurationList; buildConfigurations = ( 61D04A361C7B7BCF00CBE6AE /* Debug */, 61D04A371C7B7BCF00CBE6AE /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ 2000D40D2AD88D6B0052EDA6 /* XCRemoteSwiftPackageReference "SwiftFormat" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/nicklockwood/SwiftFormat"; requirement = { kind = upToNextMajorVersion; minimumVersion = 0.60.0; }; }; 20012FA4267980EE00553B60 /* XCRemoteSwiftPackageReference "Reachability" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/ashleymills/Reachability.swift"; requirement = { kind = upToNextMajorVersion; minimumVersion = 5.2.4; }; }; 2039B4052C2920BF006A6B6D /* XCRemoteSwiftPackageReference "MenuBarExtraAccess" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/orchetect/MenuBarExtraAccess"; requirement = { kind = upToNextMajorVersion; minimumVersion = 1.3.0; }; }; 20CB68BC2A2D9029001C73B9 /* XCRemoteSwiftPackageReference "SwiftLint" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/realm/SwiftLint"; requirement = { kind = upToNextMajorVersion; minimumVersion = 0.63.2; }; }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ 20012FA5267980EE00553B60 /* Reachability */ = { isa = XCSwiftPackageProductDependency; package = 20012FA4267980EE00553B60 /* XCRemoteSwiftPackageReference "Reachability" */; productName = Reachability; }; 2039B4062C2920BF006A6B6D /* MenuBarExtraAccess */ = { isa = XCSwiftPackageProductDependency; package = 2039B4052C2920BF006A6B6D /* XCRemoteSwiftPackageReference "MenuBarExtraAccess" */; productName = MenuBarExtraAccess; }; 20CB68BD2A2D9033001C73B9 /* SwiftLintPlugin */ = { isa = XCSwiftPackageProductDependency; package = 20CB68BC2A2D9029001C73B9 /* XCRemoteSwiftPackageReference "SwiftLint" */; productName = "plugin:SwiftLintPlugin"; }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 2023ED9B1C4ED09C0087FD67 /* Project object */; } ================================================ FILE: DatWeatherDoe.xcodeproj/project.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: DatWeatherDoe.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: DatWeatherDoe.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved ================================================ { "originHash" : "cf72def9a17d22a974c73e8b0f0a4d80bccc1e736e0ce0e89a85e671a37bb5b6", "pins" : [ { "identity" : "collectionconcurrencykit", "kind" : "remoteSourceControl", "location" : "https://github.com/JohnSundell/CollectionConcurrencyKit.git", "state" : { "revision" : "b4f23e24b5a1bff301efc5e70871083ca029ff95", "version" : "0.2.0" } }, { "identity" : "cryptoswift", "kind" : "remoteSourceControl", "location" : "https://github.com/krzyzanowskim/CryptoSwift.git", "state" : { "revision" : "e45a26384239e028ec87fbcc788f513b67e10d8f", "version" : "1.9.0" } }, { "identity" : "menubarextraaccess", "kind" : "remoteSourceControl", "location" : "https://github.com/orchetect/MenuBarExtraAccess", "state" : { "revision" : "33bb0e4b1e407feac791e047dcaaf9c69b25fd26", "version" : "1.3.0" } }, { "identity" : "reachability.swift", "kind" : "remoteSourceControl", "location" : "https://github.com/ashleymills/Reachability.swift", "state" : { "revision" : "21d1dc412cfecbe6e34f1f4c4eb88d3f912654a6", "version" : "5.2.4" } }, { "identity" : "sourcekitten", "kind" : "remoteSourceControl", "location" : "https://github.com/jpsim/SourceKitten.git", "state" : { "revision" : "731ffe6a35344a19bab00cdca1c952d5b4fee4d8", "version" : "0.37.2" } }, { "identity" : "swift-argument-parser", "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-argument-parser.git", "state" : { "revision" : "309a47b2b1d9b5e991f36961c983ecec72275be3", "version" : "1.6.1" } }, { "identity" : "swift-filename-matcher", "kind" : "remoteSourceControl", "location" : "https://github.com/ileitch/swift-filename-matcher", "state" : { "revision" : "eef5ac0b6b3cdc64b3039b037bed2def8a1edaeb", "version" : "2.0.1" } }, { "identity" : "swift-syntax", "kind" : "remoteSourceControl", "location" : "https://github.com/swiftlang/swift-syntax.git", "state" : { "revision" : "65b02a90ad2cc213e09309faeb7f6909e0a8577a", "version" : "604.0.0-prerelease-2026-01-20" } }, { "identity" : "swiftformat", "kind" : "remoteSourceControl", "location" : "https://github.com/nicklockwood/SwiftFormat", "state" : { "revision" : "f098405c8e97cb2bde1779fdd3ac72880ff32fa3", "version" : "0.60.0" } }, { "identity" : "swiftlint", "kind" : "remoteSourceControl", "location" : "https://github.com/realm/SwiftLint", "state" : { "revision" : "88952528a590ed366c6f76f6bfb980b5ebdcefc1", "version" : "0.63.2" } }, { "identity" : "swiftytexttable", "kind" : "remoteSourceControl", "location" : "https://github.com/scottrhoyt/SwiftyTextTable.git", "state" : { "revision" : "c6df6cf533d120716bff38f8ff9885e1ce2a4ac3", "version" : "0.9.0" } }, { "identity" : "swxmlhash", "kind" : "remoteSourceControl", "location" : "https://github.com/drmohundro/SWXMLHash.git", "state" : { "revision" : "a853604c9e9a83ad9954c7e3d2a565273982471f", "version" : "7.0.2" } }, { "identity" : "yams", "kind" : "remoteSourceControl", "location" : "https://github.com/jpsim/Yams.git", "state" : { "revision" : "d41ba4e7164c0838c6d48351f7575f7f762151fe", "version" : "6.1.0" } } ], "version" : 3 } ================================================ FILE: DatWeatherDoe.xcodeproj/xcshareddata/xcschemes/DatWeatherDoe.xcscheme ================================================ ================================================ FILE: DatWeatherDoeTests/API/Repository/Location/Coordinates/LocationValidatorTests.swift ================================================ // // LocationValidatorTests.swift // DatWeatherDoeTests // // Created by Inder Dhir on 1/26/22. // Copyright © 2022 Inder Dhir. All rights reserved. // @testable import DatWeatherDoe import Testing struct LocationValidatorTests { @Test func testLocation_empty() async throws { #expect(throws: (any Error).self) { try LocationValidator(latLong: "").validate() } } @Test func testLocation_correct() async throws { #expect(throws: Never.self) { try LocationValidator(latLong: "12,24").validate() } } } ================================================ FILE: DatWeatherDoeTests/API/Repository/WeatherURLBuilderTests.swift ================================================ // // WeatherURLBuilderTests.swift // DatWeatherDoeTests // // Created by Inder Dhir on 1/25/22. // Copyright © 2022 Inder Dhir. All rights reserved. // @testable import DatWeatherDoe import Foundation import Testing struct WeatherURLBuilderTests { @Test func testBuild() async throws { let urlString = try WeatherURLBuilder( appId: "123456", location: .init(latitude: 42, longitude: 42) ).build().absoluteString #expect(urlString == "https://api.weatherapi.com/v1/forecast.json?key=123456&aqi=yes&q=42.0,42.0&dt=\(parsedDateToday)") } private var parsedDateToday: String { let dateFormatter = DateFormatter() dateFormatter.dateFormat = "yyyy-MM-dd" return dateFormatter.string(from: Date()) } } ================================================ FILE: DatWeatherDoeTests/DatWeatherDoe.xctestplan ================================================ { "configurations" : [ { "id" : "8E1BA5A7-5E49-4CF1-8D13-2F0DA4BF0476", "name" : "Main Config", "options" : { } } ], "defaultOptions" : { "codeCoverage" : false, "targetForVariableExpansion" : { "containerPath" : "container:DatWeatherDoe.xcodeproj", "identifier" : "2023EDA21C4ED09C0087FD67", "name" : "DatWeatherDoe" }, "testExecutionOrdering" : "random" }, "testTargets" : [ { "parallelizable" : true, "target" : { "containerPath" : "container:DatWeatherDoe.xcodeproj", "identifier" : "61D04A2E1C7B7BCF00CBE6AE", "name" : "DatWeatherDoeTests" } } ], "version" : 1 } ================================================ FILE: DatWeatherDoeTests/UI/Configure/Options/RefreshIntervalTests.swift ================================================ // // RefreshIntervalTests.swift // DatWeatherDoeTests // // Created by Inder Dhir on 2/12/22. // Copyright © 2022 Inder Dhir. All rights reserved. // @testable import DatWeatherDoe import Testing struct RefreshIntervalTests { @Test func refreshIntervalTimes() { #expect(RefreshInterval.fiveMinutes.rawValue == 300) #expect(RefreshInterval.fifteenMinutes.rawValue == 900) #expect(RefreshInterval.thirtyMinutes.rawValue == 1800) #expect(RefreshInterval.sixtyMinutes.rawValue == 3600) } @Test func refreshintervalStrings() { #expect(RefreshInterval.fiveMinutes.title == "5 min") #expect(RefreshInterval.fifteenMinutes.title == "15 min") #expect(RefreshInterval.thirtyMinutes.title == "30 min") #expect(RefreshInterval.sixtyMinutes.title == "60 min") } } ================================================ FILE: DatWeatherDoeTests/UI/Configure/Options/TemperatureUnitTests.swift ================================================ // // TemperatureUnitTests.swift // DatWeatherDoeTests // // Created by Inder Dhir on 2/12/22. // Copyright © 2022 Inder Dhir. All rights reserved. // @testable import DatWeatherDoe import Testing struct TemperatureUnitTests { @Test func fahrenheit() { let fahrenheitUnit = TemperatureUnit.fahrenheit #expect(fahrenheitUnit.unitString == "F") #expect(fahrenheitUnit.degreesString == "\u{00B0}F") } @Test func celsius() { let fahrenheitUnit = TemperatureUnit.celsius #expect(fahrenheitUnit.unitString == "C") #expect(fahrenheitUnit.degreesString == "\u{00B0}C") } @Test func all() { let fahrenheitUnit = TemperatureUnit.all #expect(fahrenheitUnit.unitString == "All") #expect(fahrenheitUnit.degreesString == "\u{00B0}All") } } ================================================ FILE: DatWeatherDoeTests/UI/Configure/Options/WeatherSourceTests.swift ================================================ // // WeatherSourceTests.swift // DatWeatherDoeTests // // Created by Inder Dhir on 2/12/22. // Copyright © 2022 Inder Dhir. All rights reserved. // @testable import DatWeatherDoe import Testing struct WeatherSourceTests { @Test func locationSource() { let locationSource = WeatherSource.location #expect(locationSource.title == "Location") #expect(locationSource.placeholder == "") #expect(locationSource.textHint == "") } @Test func latLongSource() { let latLongSource = WeatherSource.latLong #expect(latLongSource.title == "Lat/Long") #expect(latLongSource.placeholder == "42,42") #expect(latLongSource.textHint == "[latitude],[longitude]") } } ================================================ FILE: LICENSE ================================================ Apache License, Version 2.0 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: You must give any other recipients of the Work or Derivative Works a copy of this License; and You must cause any modified files to carry prominent notices stating that You changed the files; and You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright 2016 Inder Dhir Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: README.md ================================================ # [](image.png) DatWeatherDoe > **Note** OpenWeatherMap API 2.5 support is ending in June 2024. The app uses WeatherAPI going forward with location support. - Fetch weather using: - Location services - Latitude / Longitude - Configurable polling interval - Dark mode support - Supports MacOS 13.0+ ## Screenshots ![Screenshot 1](screenshot_1.png)\ ![Screenshot 2](screenshot_2.png) ## Installation ### Homebrew Cask `brew install --cask datweatherdoe` ### Manual ## Using Location Services If using location, please make sure that the app has permission to access location services on macOS. `System Preferences > Security & Privacy > Privacy > Location Services` ![Location services screenshot 1](location_services_1.png) ![Location services screenshot 2](location_services_2.png) ## Developer Setup - Get your personal API key for WeatherAPI [here](https://www.weatherapi.com) - Add the following in "Config.xcconfig": ```env WEATHER_API_KEY=YOUR_KEY ``` ## Donate Buy me a coffee to support the development of this project. [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/Y8Y211O253) ## Contributing Please see CONTRIBUTING.md