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.