Small macOS SwiftUI app to display F5 Weather data for my location
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

124 lines
4.2 KiB

import Foundation
// MARK: What each forecast row holds
struct F5DayCast: Identifiable {
var id = UUID()
var day: String = "" // e.g. "Fri 10/30"
var low: Double = 0.0 // e.g. 31
var high: Double = 0.0 // e.g. 53
var condition: String = "" // e.g. "Clear"
}
// MARK: The core model for the application
class AppModel: ObservableObject {
// example individual JSON line:
// {"V1":"Wed 10/28","V2":38,"V3":47,"conditions":"Rain","c_alpha":0.75}
@Published var forecast: [F5DayCast] = [] // each day's forecast
@Published var minTemp: Double = Double.infinity // we're holding the overall lowest and highest temps so we
@Published var maxTemp: Double = -Double.infinity // can draw the lines scaled properly
@Published var showAlert: Bool = false // in case there are errors
@Published var alertMessage: String = ""
var lastGrab : Date?
init() {
getForecast() // get the forecast right away
}
func height() -> CGFloat {
let nRows = forecast.count > 0 ? forecast.count : MIN_ROWS
return(CGFloat(Double((nRows * ROW_HEIGHT_WITH_INSETS)) + (2*Double(INSETS))))
}
func getForecast(force: Bool = false) {
if (!force) {
if let g = lastGrab {
if (g.timeIntervalSinceNow < 60*60) { return }
} else {
lastGrab = Date()
minTemp = Double.infinity
maxTemp = -Double.infinity
}
}
if let urlBaseString = Bundle.main.object(forInfoDictionaryKey: "ForecastURL") as? String {
let urlString : String = "\(urlBaseString)?q=\(Date().timeIntervalSince1970)"
logger.info("Retrieving \(urlString)")
if let url = URL(string: urlString) {
let task = URLSession.shared.dataTask(with: url) { data, response, error in
if let error = error { // alert on error
self.showAlert = true
self.alertMessage = error.localizedDescription
logger.info("Error fetching data: \(error.localizedDescription)")
return
}
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else { // alert on non-200 response
self.showAlert = true
self.alertMessage = "Received a non-200 response from the server."
logger.info("Non-200 response.")
return
}
if let mimeType = httpResponse.mimeType, mimeType == "application/json",
let data = data,
let res = String(data: data, encoding: .utf8) {
DispatchQueue.main.async {
let lines = res.split(whereSeparator: \.isNewline) // convert the String response to lines
self.forecast = lines.map { line in // process JSON in each line
let v = try? JSONDecoder().decode(JSON.self, from: line.data(using: .utf8)!)
if let m = v!.V2.doubleValue { // see if the day's min is the lowest we've seen
self.minTemp = (m < self.minTemp) ? m : self.minTemp
}
if let m = v!.V3.doubleValue { // see if the day's max is the lowest we've seen
self.maxTemp = (m > self.maxTemp) ? m : self.maxTemp
}
return( // if the records are junk but are still "records", populate the day with defaults
F5DayCast(
day: v!.V1.stringValue ?? "Day ##/##",
low: v!.V2.doubleValue ?? 32.0,
high: v!.V3.doubleValue ?? 90.0,
condition: v!.conditions.stringValue ?? "Clear"
)
)
} // lines processor
} // DQ
} // mimeType
} // task setup
task.resume()
} else {
logger.info("URL Error")
DispatchQueue.main.async {
self.showAlert = true
self.alertMessage = "URL error"
}
} // URL
} // urlString
} // getForecast()
} // class