I’m facing an issue in my SwiftUI project where the UI is not updating automatically after fetching data. I have a WeatherViewModel fetching weather data for multiple locations, and the UI doesn’t reflect the changes until I manually trigger an update by pressing a button. I’ve tried using @Published properties and ObservableObject, but the problem persists.
I attempted to use @Published properties and ObservableObject in my code.Despite these efforts, the UI still doesn’t update automatically.
the weather data model:
import Foundation
import Foundation
// MARK: - Welcome
struct WeatherDataModel: Codable {
let coord: Coord
let weather: [Weather]
let base: String
let main: Main
let visibility: Int
let wind: Wind
let clouds: Clouds
let dt: Int
let sys: Sys
let timezone, id: Int
let name: String
let cod: Int
}
// MARK: - Clouds
struct Clouds: Codable {
let all: Int
}
// MARK: - Coord
struct Coord: Codable {
let lon, lat: Double
}
// MARK: - Main
struct Main: Codable {
let temp, feelsLike, tempMin, tempMax: Double
let pressure, humidity: Int
enum CodingKeys: String, CodingKey {
case temp
case feelsLike = "feels_like"
case tempMin = "temp_min"
case tempMax = "temp_max"
case pressure, humidity
}
}
// MARK: - Sys
struct Sys: Codable {
let type, id: Int
let country: String
let sunrise, sunset: Int
}
// MARK: - Weather
struct Weather: Codable {
let id: Int
let main, description, icon: String
}
// MARK: - Wind
struct Wind: Codable {
let speed: Double
let deg: Int
}
class Location: ObservableObject, Identifiable {
let id = UUID()
@Published var name: String
@Published var weatherDataModel: WeatherDataModel?
init(name: String) {
self.name = name
}
}
class WeatherViewModel: ObservableObject {
@Published var locations: [Location] = []
func fetchWeather(for location: Location) {
// Implement API request to OpenWeatherMap using URLSession
// You'll need to replace "YOUR_API_KEY" with your actual OpenWeatherMap API key
let apiKey = "8281a792c5f995747b19a57c8e52ea8d"
let urlString = "https://api.openweathermap.org/data/2.5/weather?q=(location.name)&appid=(apiKey)"
// let urlString = "https://api.openweathermap.org/data/2.5/weather?q=mexico&appid=(apiKey)"
guard let url = URL(string: urlString) else { return }
URLSession.shared.dataTask(with: url) { data, _, error in
guard let data = data, error == nil else {
return }
do {
let decodedData = try JSONDecoder().decode(WeatherDataModel.self, from: data)
DispatchQueue.main.async {
location.weatherDataModel = decodedData
print("Weather data fetched successfully: (decodedData)")
}
} catch {
print("Error decoding weather data: (error)")
}
}.resume()
}
}
Also for some strange reason the view does update automatically with the .onAppear but then when I click the button which clearly has no code in it then the view updates and the data from the API is displayed.
the view receiving the data:
//
// TestView.swift
// Amun
//
// Created by Richard Nkanga on 09/02/2024.
//
import SwiftUI
struct HomeView: View {
@StateObject private var viewModel = WeatherViewModel()
@State var names = ["UNITED STATES", "Montreal", "Nigeria", "poland", "London"]
@State private var newLocation = "Canada"
@State private var isEditing = false
var body: some View {
NavigationStack {
ZStack {
Color(.background)
.ignoresSafeArea(.all)
.onAppear {
}
VStack {
List(viewModel.locations, id: .id) { locs in
HStack {
Text(locs.name)
Text("(Double(locs.weatherDataModel?.main.temp ?? 0))")
Spacer()
if let weatherData = locs.weatherDataModel {
Text("Temp: (Int(weatherData.main.temp))°C")
Text("Temp: (weatherData.name)")
} else {
Text("Loading...")
}
}
}
.onAppear {
for name in names {
let location = Location(name: name)
viewModel.locations.append(location)
viewModel.fetchWeather(for: location)
}
}
Button(action: {
isEditing.toggle()
}, label: {
Text(isEditing ? "Done" : "Edit")
.foregroundColor(Color.red)
})
}
}
.background(Color.red)
.navigationTitle("Weather")
}
}
}
#Preview {
HomeView()
}
The ui never updates and just keeps showing loading…
2
Answers
Playing around a bit with your code the solution I came up with was to, first of all, switch to async/await, because the code would be shorter and more readable. I also found a Coding Key error: in the Sys struct the type and id need to be optional.
Here’s how I modified your REST call to be async:
Then your Sys struct:
As said in the comments, I also changed Location to be a struct:
And, at the end change the onAppear with task modifier like so:
Here is important to use the append at the end of every operations, if you place it before your data won’t be updated.
Let me know what do you think of this solution!
Nesting classes conforming to
ObservableObject
cannot work. In practice theLocation
class is not needed at all becauseWeatherDataModel
fortunately provides anid
andname
property.This is a different
async
approachImport Foundation with
@preconcurrency
to suppress aURLSession
warningAdopt
Identifiable
inWeatherDataModel
Declare
type
andid
inSys
as optionalDelete the CodingKeys in
Main
because the JSONDecoder manages the snake case keysCreate an enum for different states
In
WeatherViewModel
publish thestate
and pass the names infetchWeather
which is marked asasync
and handles also the errorsIn the view get the data asynchronously in a
.task
andswitch
on the publishedstate