Home > Blockchain >  I can print data from API but i can't set values and UI with this data in SwiftUI
I can print data from API but i can't set values and UI with this data in SwiftUI

Time:02-24

I sent data to my ListView, and can print it in console. The problem is that i can't update any values with this data, so can't also update UI with it. My app should take a city name which String - put it to fetchWeather() func, download data and do all stuff, and after this start getWeather() func in in ListView and update values (city), which i use in UI.

Code:

WeatherMenager:

 import Foundation
    
    struct WeatherManager {
        
    
        let weatherURL = "https://api.openweathermap.org/data/2.5/weather?appid=APP_ID&units=metric"
        
        func fetchWeather(cityName: String) {
            let urlString = "\(weatherURL)&q=\(cityName)"
            performRequest(with: urlString)
            return
        }
        
        
        func performRequest(with urlString: String) {
            if let url = URL(string: urlString) {
                let session = URLSession(configuration: .default)
                let task = session.dataTask(with: url) { (data, response, error) in
                    if error != nil {
                        
                        return
                    }
                    if let safeData = data {
                        if let weather = self.parseJSON(safeData) {
                            
                            let listVC = ListView()
                            
                            DispatchQueue.main.async {
                            
                                listVC.getWeather(weather: weather)
                            }
                        }
                    }
                }
                task.resume()
            }
        }
        
        func parseJSON(_ weatherData: Data) -> WeatherModel? {
            let decoder = JSONDecoder()
            do {
                let decodedData = try decoder.decode(WeatherData.self, from: weatherData)
                let id = decodedData.weather[0].id
                let temp = decodedData.main.temp
                let name = decodedData.name
                
                let weather = WeatherModel(conditionId: id, cityName: name, temperature: temp)
                return weather
                
            } catch {
                return nil
            }
        }
    }

WeatherModel:

import Foundation

struct WeatherModel {
    var conditionId: Int
    var cityName: String
    var temperature: Double
    
    var temperatureString: String {
        return String(format: "%.1f", temperature)
    }
    
    var conditionName: String {
        switch conditionId {
        case 200...232:
            return "cloud.bolt"
        case 300...321:
            return "cloud.drizzle"
        case 500...531:
            return "cloud.rain"
        case 600...622:
            return "cloud.snow"
        case 701...781:
            return "cloud.fog"
        case 800:
            return "sun.max"
        case 801...804:
            return "cloud.bolt"
        default:
            return "cloud"
        }
    }
    
}

WeatherData:

import Foundation

struct WeatherData: Codable {
    let name: String
    let main: Main
    let weather: [Weather]
}

struct Main: Codable {
    let temp: Double
}

struct Weather: Codable {
    let description: String
    let id: Int
}

ListView: (place where i want to display data)

import SwiftUI

struct ListView: View{
    

    @State var weatherMenager = WeatherManager()
    @State var city : String = ""
    
    @State var textFieldText : String = ""
    
    
    func getWeather(weather : WeatherModel){ // 
        
        print(weather.cityName) //works - print data in console
        print(weather.temperatureString)// works - print data in console
        
        self.city = weather.cityName
        
        print(city) // doesn't print data just like it's empty string
        
    }
.... more code which dont't really metter - just swiftui code

I have no idea what's wrong, thanks.

CodePudding user response:

You have a couple of things that work against some of the principals of SwiftUI.

Most importantly, in your current code, you create a new ListView when you've gotten the weather back when you do this:

let listVC = ListView()
DispatchQueue.main.async {
  listVC.getWeather(weather: weather)
}

This is not the same instance of ListView that you started with, so it will never show anything on the UI. Also, it relates to why the print(city) code doesn't work -- city is a @State variable which requires the view to be loaded to the hierarchy to work as expected. Since your ListView isn't actually loaded, property wrappers like that may behave in unexpected ways.

One way to address this is to convert your WeatherManager to an ObservableObject and then observe a @Published property that gets updated when the weather is returned. That may look like this:

class WeatherManager : ObservableObject { //<-- Conform to ObservableObject
    
    @Published var weatherModel : WeatherModel? //<-- @Published property to store the returned data
    
    let weatherURL = "https://api.openweathermap.org/data/2.5/weather?appid=APP_ID&units=metric" //<-- Removed your App ID
    
    func fetchWeather(cityName: String) {
        let urlString = "\(weatherURL)&q=\(cityName)"
        print("Fetch weather: ",urlString)
        performRequest(with: urlString)
    }
    
    func performRequest(with urlString: String) {
        if let url = URL(string: urlString) {
            let session = URLSession(configuration: .default)
            let task = session.dataTask(with: url) { (data, response, error) in
                if error != nil {
                    print("Error",error!)
                    return
                }
                if let safeData = data, let weather = self.parseJSON(safeData) {
                    DispatchQueue.main.async {
                        self.weatherModel = weather
                    }
                }
            }
            task.resume()
        }
    }
    
    func parseJSON(_ weatherData: Data) -> WeatherModel? {
        let decoder = JSONDecoder()
        do {
            let decodedData = try decoder.decode(WeatherData.self, from: weatherData)
            let id = decodedData.weather[0].id
            let temp = decodedData.main.temp
            let name = decodedData.name
            
            let weather = WeatherModel(conditionId: id, cityName: name, temperature: temp)
            return weather
            
        } catch {
            print("Error decoding: ", error)
            return nil
        }
    }
}


struct ListView: View{
    @StateObject var weatherManager = WeatherManager() //<-- Here
    @State var city : String = "Portland"
    @State var textFieldText : String = ""
    
    var body: some View {
        VStack {
            if let weather = weatherManager.weatherModel {
                Text("Temp: \(weather.temperatureString)")
                Text("City: \(weather.cityName)")
            }
        }.onAppear {
            weatherManager.fetchWeather(cityName: city)
        }
    }
}

Note that this is the smallest change to your code to make it workable. You may want to look into using Combine, for your URL task, which would be a most SwiftUI-centric way of loading the data. See https://developer.apple.com/documentation/foundation/urlsession/processing_url_session_data_task_results_with_combine

Also, you don't need the ObservableObject -- everything could be moved into a Task on your View. Personally, I like the separation of network requests into a separate object, but some feel that even this code belongs on the View.

Finally, although it doesn't relate directly to your issue, be careful with your city parameter in the URL -- you're not URL encoding it right now, so city names with spaces, for example, won't work as expected.

  • Related