我在 SwiftUI 项目中遇到一个问题,即 UI 在获取数据后不会自动更新。我有一个 WeatherViewModel 获取多个位置的天气数据,并且 UI 不会反映更改,直到我通过按下按钮手动触发更新。我尝试过使用 @Published 属性和 ObservableObject,但问题仍然存在。
我尝试在代码中使用 @Published 属性和 ObservableObject。尽管做出了这些努力,UI 仍然不会自动更新。
天气数据模型:
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()
}
}
另外,由于一些奇怪的原因,视图确实会使用 .onAppear 自动更新,但是当我单击其中显然没有代码的按钮时,视图会更新并显示来自 API 的数据。
接收数据的视图:
//
// 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()
}
用户界面永远不会更新,只是一直显示正在加载...
稍微修改一下你的代码,我想出的解决方案是,首先,切换到 async/await,因为代码会更短且更具可读性。我还发现了一个编码键错误:在 Sys 结构中,类型和 id 需要是可选的。
以下是我将 REST 调用修改为异步的方法:
func fetchWeather(for location: Location) async throws -> WeatherDataModel? {
// 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 nil }
/// Shortest async way
do {
let (data, _) = try await URLSession.shared.data(from: url)
let decodedResponse = try JSONDecoder().decode(WeatherDataModel.self, from: data)
return decodedResponse
} catch(let error) {
print("Error decoding weather data: \(error)")
return nil
}
/// Longest async version
/*return await withCheckedContinuation { contination in
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
contination.resume(returning: decodedData)
print("Weather data fetched successfully: \(decodedData)")
}
} catch {
print("Error decoding weather data: \(error)")
}
}.resume()
}*/
}
然后你的系统结构:
struct Sys: Codable {
let type, id: Int?
let country: String
let sunrise, sunset: Int
}
正如评论中所说,我还将 Location 更改为结构:
struct Location: Identifiable {
let id = UUID()
var name: String
var weatherDataModel: WeatherDataModel?
init(name: String) {
self.name = name
}
}
最后,使用任务修饰符更改 onAppear,如下所示:
.task {
for name in names {
var location = Location(name: name)
let weatherDataModel = try? await viewModel.fetchWeather(for: location)
location.weatherDataModel = weatherDataModel
/// IMPORTANT!!! Append the location at the end.
viewModel.locations.append(location)
}
}
如果您将附加放在数据不会更新之前,那么在每个操作末尾使用附加非常重要。 让我知道您对此解决方案的看法!