Search code examples
swiftmvvmswiftuifetchviewmodel

Swift, fetching data in viewmodel and passing it to view


I'm trying to update data in my viewModel here is my viewModel;

import SwiftUI
import CoreLocation
final class LocationViewViewModel: ObservableObject {
    static let previewWeather: Response = load("Weather.json")

    let weatherManager = WeatherManager()
    let locationManager = LocationManager.shared
    
    @Published var weather: Response
    
    init(weather: Response) {       // Remove async
        DispatchQueue.main.async {                       // Here, you enter in an async environment
            let data = await fetchData()     // Read the data and pass it to a constant
            DispatchQueue.main.async {      // Get on the main thread
                self.weather = data         // Here, change the state of you app
            }
        }
    }
    
    func fetchData() async -> Response {
        guard let weather = try? await weatherManager.getWeather(latitude: weatherManager.latitude!, longitude: weatherManager.latitude!) else { fatalError("Network Error.") }
        return weather
    }
        var city: String {
            return locationManager.getCityName()
        }
        
        var date: String {
            return dateFormatter.string(from: Date(timeIntervalSince1970: TimeInterval(weather.current.dt)))
        }
        
        var weatherIcon: String {
            if weather.current.weather.count > 0 {
                return weather.current.weather[0].icon
            }
            return "sun.max"
        }
        
        var temperature: String {
            return getTempFor(temp: weather.current.temp)
        }
        
        var condition: String {
            if weather.current.weather.count > 0 {
                return weather.current.weather[0].main
            }
            return ""
        }
        
        var windSpeed: String {
            return String(format: "%0.1f", weather.current.wind_speed)
        }
        
        var humidity: String {
            return String(format: "%d%%", weather.current.humidity)
        }
        
        var rainChances: String {
            return String(format: "%0.0f%%", weather.current.dew_point)
        }
        
  

    
    var dateFormatter: DateFormatter = {
       let formatter = DateFormatter()
        formatter.dateStyle = .medium
       return formatter
   }()
   
    var dayFormatter: DateFormatter = {
       let formatter = DateFormatter()
       formatter.dateFormat = "EEE"
       return formatter
   }()
   
    var timeFormatter: DateFormatter = {
       let formatter = DateFormatter()
       formatter.dateFormat = "hh a"
       return formatter
   }()
    
    func getTimeFor(time: Int) -> String {
        return timeFormatter.string(from: Date(timeIntervalSince1970: TimeInterval(time)))
    }
    
    func getTempFor(temp: Double) -> String {
        return String(format: "%0.1f", temp)
    }
    
    func getDayFor(day: Int) -> String {
        return dayFormatter.string(from: Date(timeIntervalSince1970: TimeInterval(day)))
    }
}

Also i fetched that data for my previous view in my weather manager so im using the same function in my viewModel. My weatherManager;

final class WeatherManager {
    var longitude = LocationManager.shared.location?.coordinate.longitude
    var latitude = LocationManager.shared.location?.coordinate.latitude
    var units: String = "metric"
    
    func getWeather(latitude: CLLocationDegrees, longitude: CLLocationDegrees) async throws -> Response {
        guard let url = URL(string: "https://api.openweathermap.org/data/2.5/onecall?lat=\(latitude)&lon=\(longitude)&units=\(units)&exclude=hourly,minutely&appid=\(API.API_KEY)") else { fatalError("Invalid Url.")}
        
        let urlRequest = URLRequest(url: url)
        
        let (data, response) = try await URLSession.shared.data(for: urlRequest)
                
        guard (response as? HTTPURLResponse)?.statusCode == 200 else { fatalError("Error while fetching data") }
                
        let decodedData = try JSONDecoder().decode(Response.self, from: data)
        
        return decodedData
    }
}

But I stuck with compile errors about initializing my weather Also tried to make my weather model optional but in the end i get the fatal error which says Fatal error: Unexpectedly found nil while unwrapping an Optional value What is the correct way of doing this if you are using fetched data in many views & viewModels


Solution

  • Your init() is trying to run asynchronously and it's updating a @Published property. Even if you manage to avoid compile errors, you cannot update a property that will change the state of your views (@Published) unless you are on the main thread.

    What I propose:

        @Published var weather = Response()     // Initialise this property in some way, the dummy values will be used by the app until you complete fetching the data
        
        init(weather: Response) {       // Remove async
            Task {                       // Here, you enter in an async environment
                let data = await fetchData()     // Read the data and pass it to a constant
                DispatchQueue.main.async {      // Get on the main thread
                    self.weather = data         // Here, change the state of you app
                }
            }
        }
    

    I hope this works, but it would be better if after "But I stuck with compile errors..." you showed what kind of errors you find. I tried to use my best guess with the solution above.