I'm currently testing my Swift
skills while having fun with WeatherKit
but there's something I can't do by myself.
I'd like to "unwrap" the temperature.value
I get from hour
like I did for currenWeather
? Is it possible, if yes, what should I do?
import Foundation
import WeatherKit
class WeatherManager: ObservableObject {
@Published private var currentWeather: CurrentWeather?
@Published private var hourWeather: Forecast<HourWeather>?
var isLoading: Bool
init() {
isLoading = true
Task {
await getWeather(48.864716, 2.349014) /* Paris, France */
}
}
@MainActor
func getWeather(_ lat: Double, _ long: Double) async {
let nowDate = Date.now
let oneDayFromDate = Date.now.addingTimeInterval(60 * 60 * 24)
do {
currentWeather = try await WeatherService.shared.weather(for: .init(latitude: lat, longitude: long), including: .current)
hourWeather = try await WeatherService.shared.weather(for: .init(latitude: lat, longitude: long), including: .hourly(startDate: nowDate, endDate: oneDayFromDate))
} catch {
print("WeatherKit failed to get: \(error)")
}
isLoading = false
}
var unwrappedTemperature: Double {
// Here: it's unwrapped
currentWeather?.temperature.value ?? 0
}
var unwrappedHourly: [HourWeather] {
hourWeather?.forecast ?? []
}
}
import SwiftUI
struct ContentView: View {
@EnvironmentObject private var weatherManager: WeatherManager
var body: some View {
VStack {
if !weatherManager.isLoading {
Text("\(weatherManager.unwrappedTemperature, specifier: "%.0f")°C")
ScrollView(.horizontal, showsIndicators: false) {
HStack {
ForEach(weatherManager.unwrappedHourly, id: \.date) { hour in
// Here: is it possible to unwrap "temperature.value"?
Text("\(hour.temperature.value, specifier: "%.0f")°C")
}
}
}
} else {
ProgressView()
}
}
}
}
I hope my question is clear enough. Thanks, in advance!
WeatherKit
uses non-optional values as much as possible.
A better approach is to create an enum for the loading state and set it accordingly. There are states for idle
, loading
, loaded
passing the non-optional data and failed
passing the error.
And it's not necessary to call the service twice.
@MainActor
final class WeatherManager: ObservableObject {
enum LoadingState {
case idle, loading, loaded(CurrentWeather, Forecast<HourWeather>), failed(Error)
}
@Published var state: LoadingState = .idle
init() {
state = .loading
Task {
await getWeather(lat: 48.864716, long: 2.349014) /* Paris, France */
}
}
func getWeather(lat: Double, long: Double) async {
let nowDate = Date.now
let oneDayFromDate = Calendar.current.date(byAdding: .day, value: 1, to: nowDate)!
let location = CLLocation(latitude: lat, longitude: long)
do {
let (cWeather, hWeather) = try await WeatherService.shared.weather(for: location, including: .current, .hourly(startDate: nowDate, endDate: oneDayFromDate))
state = .loaded(cWeather, hWeather)
} catch {
state = .failed(error)
}
}
}
In the view switch
on the state and show appropriate views
struct ContentView: View {
@EnvironmentObject private var weatherManager: WeatherManager
var body: some View {
VStack {
switch weatherManager.state {
case .idle:
EmptyView()
case .loading:
ProgressView()
case .loaded(let currentWeather, let hourWeather):
Text(currentWeather.temperature.formatted(.measurement(numberFormatStyle: .number.precision(.fractionLength(0)))))
ScrollView(.horizontal, showsIndicators: false) {
HStack {
ForEach(hourWeather.forecast, id: \.date) { hour in
Text(hour.temperature.formatted(.measurement(numberFormatStyle: .number.precision(.fractionLength(0)))))
}
}
}
case .failed(let error):
Text(error.localizedDescription)
}
}
}
}