Home > Mobile >  Swift, fetching data in viewmodel and passing it to view
Swift, fetching data in viewmodel and passing it to view

Time:02-21

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

CodePudding user response:

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.

CodePudding user response:

We don't use view model objects in SwiftUI. Your object is doing unnecessary things that SwiftUI does for us automatically like formatting strings (so labels auto update automatically when region settings change) and managing asynchronous tasks (tasks are started when view appears and when ever data changes and also cancelled if data changes before previous request ends or the view disappears). Try re-architecting it to use SwiftUI data Views correctly, e.g.

struct WeatherView: View {
    let location: Location
    @State var weather: Weather?

    var body: some View {
        Form {
            Text(weather.date, format: .dateTime) // new simpler formatting
            Text(weather.date, formatter: dateFormatter) // label is auto updated when locale changes
            Text(weather?.date == nil ? "No date" : "\(weather.date!, format: .dateTime)") // optional handling
        }
        .task(id: location) { newLocation // tasks auto cancelled and restarted when location changes
             weather = await WeatherManager.shared.getWeather(location: newLocation)
        }
    }
  • Related