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
================================================
================================================
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
\

## 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`


## 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.
[](https://ko-fi.com/Y8Y211O253)
## Contributing
Please see CONTRIBUTING.md