swiftswiftuiweatherkit

WeatherKit extension switch isn't working for specific var


Working on a simple weather app. I'd like to set a different video for the background for each weather condition (clear, rain, snow, etc...). Managed to loop-play a video as the background of the View but, for some reason, it does not change based on conditions. Videos are stored in the Bundle and are called by their string name ("deer", "lake", "snow".

I've made an extension for WeatherConditions:

extension WeatherCondition {
    var videoBG : String {
        switch self {
            
        case .blizzard:
            return "snow"
        case .blowingDust:
            return "lake"
        case .blowingSnow:
            return "lake"
        case .breezy:
            return "lake"
        case .clear:
            return "deer"
        case .cloudy:
            return "lake"
        case .drizzle:
            return "lake"
        case .flurries:
            return "lake"
        case .foggy:
            return "lake"
        case .freezingDrizzle:
            return "lake"
        case .freezingRain:
            return "lake"
        case .frigid:
            return "lake"
        case .hail:
            return "lake"
        case .haze:
            return "lake"
        case .heavyRain:
            return "lake"
        case .heavySnow:
            return "lake"
        case .hot:
            return "lake"
        case .hurricane:
            return "lake"
        case .isolatedThunderstorms:
            return "lake"
        case .mostlyClear:
            return "deer"
        case .mostlyCloudy:
            return "lake"
        case .partlyCloudy:
            return "lake"
        case .rain:
            return "lake"
        case .scatteredThunderstorms:
            return "lake"
        case .sleet:
            return "lake"
        case .smoky:
            return "lake"
        case .snow:
            return "lake"
        case .strongStorms:
            return "lake"
        case .sunFlurries:
            return "lake"
        case .sunShowers:
            return "lake"
        case .thunderstorms:
            return "deer"
        case .tropicalStorm:
            return "lake"
        case .windy:
            return "lake"
        case .wintryMix:
            return "lake"
        @unknown default:
            return "deer"
        }
    }
}

and then:

@MainActor class WeatherKitManager: ObservableObject {
    
    @Published var weather: Weather?
    
    func getWeather() async {
        do {
            weather = try await Task.detached(priority: .userInitiated) {
                return try await WeatherService.shared.weather(for: .init(latitude: 42.34779, longitude: 13.39943))  // Coordinates for SAN PIO just as example coordinates
            }.value
        } catch {
            fatalError("\(error)")
        }
    }

    var videoAppBG: String {
        weather?.currentWeather.condition.videoBG ?? "snow"
    }
}

and I set it as my video background with:

var body: some View {
    
    NavigationView {
        ZStack {
            
            BgdFullScreenVideoView(videoName: weatherKitManager.videoAppBG)
                .zIndex(-10)
            VStack (alignment: .leading, spacing: 10) {
                Spacer()
                VStack(alignment: .leading) {
                    Text("\(weatherKitManager.temp)")
                        .foregroundColor(.white)
                        .font(.system(size: 75))
                        .fontWeight(.black)
                        .shadow(radius: 8)
                    
                    Text("L'Aquila")
                        .foregroundColor(.white)
                        .font(.system(size: 30))
                        .fontWeight(.medium)
                        .shadow(radius: 8)
                }
                .padding(.bottom, 80)
            }
            .padding(.leading,25)
            .frame(maxWidth: .infinity, alignment: .leading)
            
        }
        .toolbar {
            ToolbarItem(placement: .navigationBarLeading) {
                ZStack {
                    Image("logo")
                        .resizable()
                        .aspectRatio(contentMode: .fit)
                        .frame(width: 30, height: 30)
                        .shadow(radius: 3)
                        .zIndex(10)
                }
                .padding(.top,40)
            }
        }
    }
    .onAppear() {
        Task {
            await weatherKitManager.getWeather()
        }
    }
    .frame(maxWidth: .infinity, maxHeight: .infinity)
    .ignoresSafeArea()
}

but I always get "snow" played, no matter the real weather condition. Used the same way to get temperature, symbolName (etc...) and it works.

In other words, the switch always returns "nill".

Just doesn't for "videoBG".

What am I doing wrong?


Solution

  • A computed property cannot work because it does not refresh the view.

    A possible way is to declare another @Published property for the condition which all related UI elements can retrieve their data from.

    But this requires a change in getWeather which is too complicated anyway, a detach task is not needed.

    And the second task in .onAppear is not needed either, replace the modifier with

    .task {
        await weatherKitManager.getWeather()
    }
    

    To get rid of the optional I recommend an enum for the state

    enum LoadingState {
        case idle, loading, success(Weather), failure(Error)
    }
    

    Then replace the view model with

    @MainActor class WeatherKitManager: ObservableObject {
        
        @Published var state: LoadingState = .idle
        @Published var condition: WeatherCondition = .clear
        
        func getWeather() async {
            state = .loading
            do {
                let weather = try await WeatherService.shared.weather(for: .init(latitude: 42.34779, longitude: 13.39943))  // Coordinates for SAN PIO just as example coordinates
                state = .success(weather)
                condition = weather.currentWeather.condition
            } catch {
                state = .failure(error)
            }
        }
    }
    

    In the view show different views depending on the state for example

    NavigationView {
        switch weatherKitManager.state {
            case .idle: EmptyView()
            case .loading: ProgressView()
            case .success(let weather):
                ZStack {
                    BgdFullScreenVideoView(videoName: weatherKitManager.condition.videoBG)
                        .zIndex(-10)
                    VStack (alignment: .leading, spacing: 10) {
                        Spacer()
                        VStack(alignment: .leading) {
                            Text(weather.currentWeather.temperature.formatted())
                                .foregroundColor(.white)
                                .font(.system(size: 75))
                                .fontWeight(.black)
                                .shadow(radius: 8)
                            
                            Text("L'Aquila")
                                .foregroundColor(.white)
                                .font(.system(size: 30))
                                .fontWeight(.medium)
                                .shadow(radius: 8)
                        }
                        .padding(.bottom, 80)
                    }
                    .padding(.leading,25)
                    .frame(maxWidth: .infinity, alignment: .leading)
                }
            case .failure(let error):
                Text(error.localizedDescription).
                    .font(.largeTitle)
        }
    }