How to unwrap a var from a ForEach in SwiftUI?

110 views Asked by At

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!

1

There are 1 answers

7
vadian On BEST ANSWER

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)
            }
        }
    }
}