swiftswiftuiunwrap

How to unwrap a var from a ForEach in SwiftUI?


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!


Solution

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