我正在尝试将 .refreshable 添加到我的 SwiftUI openweathermap 应用程序中,以便下拉并刷新从 API 返回到应用程序的值。我将应用程序设置为允许用户在文本字段中输入城市名称,点击搜索按钮,然后在工作表中查看该城市的天气详细信息。关闭表单后,用户可以在列表中看到他/她之前搜索过的所有城市作为导航链接,每个列表链接中都可以看到城市名称和温度。我尝试将 .refreshable {}
添加到 ContentView 中的列表中。我尝试设置 .refreshable 在 ViewModel 中调用 fetchWeather()
,而 ViewModel 又被设置为将用户输入的 cityName 作为参数传递到 API URL(也在 ViewModel 中)。但是,我现在认为这无法刷新天气数据,因为调用 fetchWeather()
的操作是在工具栏按钮中定义的,而不是在列表中定义的。知道如何设置 .refreshable 来刷新列表中每个搜索城市的天气数据吗?请参阅下面的我的代码。谢谢!
内容 View
struct ContentView: View {
// Whenever something in the viewmodel changes, the content view will know to update the UI related elements
@StateObject var viewModel = WeatherViewModel()
@State private var cityName = ""
@State private var showingDetail = false
var body: some View {
NavigationView {
VStack {
List {
ForEach(viewModel.cityNameList) { city in
NavigationLink(destination: DetailView(detail: city), label: {
Text(city.name).font(.system(size: 32))
Spacer()
Text("\(city.main.temp, specifier: "%.0f")°").font(.system(size: 32))
})
}.onDelete { index in
self.viewModel.cityNameList.remove(atOffsets: index)
}
}.refreshable {
viewModel.fetchWeather(for: cityName)
}
}.navigationTitle("Weather")
.toolbar {
ToolbarItem(placement: (.bottomBar)) {
HStack {
TextField("Enter City Name", text: $cityName)
.frame(minWidth: 100, idealWidth: 150, maxWidth: 240, minHeight: 30, idealHeight: 40, maxHeight: 50, alignment: .leading)
Spacer()
Button(action: {
viewModel.fetchWeather(for: cityName)
cityName = ""
self.showingDetail.toggle()
}) {
HStack {
Image(systemName: "plus")
.font(.title)
}
.padding(15)
.foregroundColor(.white)
.background(Color.green)
.cornerRadius(40)
}.sheet(isPresented: $showingDetail) {
ForEach(0..<viewModel.cityNameList.count, id: \.self) { city in
if (city == viewModel.cityNameList.count-1) {
DetailView(detail: viewModel.cityNameList[city])
}
}
}
}
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
详情查看
struct DetailView: View {
@StateObject var viewModel = WeatherViewModel()
@State private var cityName = ""
@State var selection: Int? = nil
var detail: WeatherModel
var body: some View {
VStack(spacing: 20) {
Text(detail.name)
.font(.system(size: 32))
Text("\(detail.main.temp, specifier: "%.0f")°")
.font(.system(size: 44))
Text(detail.firstWeatherInfo())
.font(.system(size: 24))
}
}
}
struct DetailView_Previews: PreviewProvider {
static var previews: some View {
DetailView(detail: WeatherModel.init())
}
}
View 模型
class WeatherViewModel: ObservableObject {
@Published var cityNameList = [WeatherModel]()
func fetchWeather(for cityName: String) {
guard let url = URL(string: "https://api.openweathermap.org/data/2.5/weather?q=\(cityName)&units=imperial&appid=<MyAPIKey>") else { return }
let task = URLSession.shared.dataTask(with: url) { data, _, error in
guard let data = data, error == nil else { return }
do {
let model = try JSONDecoder().decode(WeatherModel.self, from: data)
DispatchQueue.main.async {
self.cityNameList.append(model)
}
}
catch {
print(error) // <-- you HAVE TO deal with errors here
}
}
task.resume()
}
}
型号
struct WeatherModel: Identifiable, Codable {
let id = UUID()
var name: String = ""
var main: CurrentWeather = CurrentWeather()
var weather: [WeatherInfo] = []
func firstWeatherInfo() -> String {
return weather.count > 0 ? weather[0].description : ""
}
}
struct CurrentWeather: Codable {
var temp: Double = 0.0
}
struct WeatherInfo: Codable {
var description: String = ""
}
最佳答案
我会做的是这个(或类似的更并发和防错的方法):
在 WeatherViewModel
添加此功能以更新所有城市的天气信息:
func updateAll() {
// keep a copy of all the cities names
let listOfNames = cityNameList.map{$0.name}
// remove all current info
cityNameList.removeAll()
// fetch the up-to-date weather info
for city in listOfNames {
fetchWeather(for: city)
}
}
以及ContentView
:
.refreshable {
viewModel.updateAll()
}
注意:您不应该有 @StateObject var viewModel = WeatherViewModel()
在DetailView
。
您应该传入模型(如果需要),并且有 @ObservedObject var viewModel: WeatherViewModel
.
编辑1:
由于获取/附加新的天气信息是异步的,因此
可能会导致 cityNameList
中的顺序不同.
对于少数城市,您可以尝试在每个fetchWeather
之后对城市进行排序。 ,如:
func fetchWeather(for cityName: String)
...
DispatchQueue.main.async {
self.cityNameList.append(model)
self.cityNameList.sort(by: {$0.name < $1.name}) // <-- here
}
...
如果当您有大量城市需要获取时这变得很麻烦, 您将需要一个更强大且独立的排序机制。
EDIT2:这是一个更强大的排序方案。
删除self.cityNameList.sort(by: {$0.name < $1.name})
来自fetchWeather
.
在 ContentView
对城市进行排序,例如:
ForEach(viewModel.cityNameList.sorted(by: { $0.name < $1.name })) { city in ... }
并使用:
.onDelete { index in
delete(with: index)
}
与:
private func delete(with indexSet: IndexSet) {
// must sort the list as in the body
let sortedList = viewModel.cityNameList.sorted(by: { $0.name < $1.name })
if let firstNdx = indexSet.first {
// get the city from the sorted list
let theCity = sortedList[firstNdx]
// get the index of the city from the viewModel, and remove it
if let ndx = viewModel.cityNameList.firstIndex(of: theCity) {
viewModel.cityNameList.remove(at: ndx)
}
}
}
EDIT3:保持原始添加中的顺序。
从 EDIT1
中删除所有模组和EDIT2
.
在 WeatherViewModel
添加这些功能:
func updateAllWeather() {
let listOfNames = cityNameList.map{$0.name}
// fetch the up-to-date weather info
for city in listOfNames {
fetchWeather(for: city)
}
}
func addToList( _ city: WeatherModel) {
// if already have this city, just update
if let ndx = cityNameList.firstIndex(where: {$0.name == city.name}) {
cityNameList[ndx].main = city.main
cityNameList[ndx].weather = city.weather
} else {
// add a new city
cityNameList.append(city)
}
}
在 fetchWeather
,使用:
DispatchQueue.main.async {
self.addToList(model)
}
在 ContentView
,
.onDelete { index in
viewModel.cityNameList.remove(atOffsets: index)
}
.refreshable {
viewModel.updateAll()
}
请注意,异步函数 fetchWeather
的逻辑有错误。
您应该使用完成处理程序在完成后继续。
特别是在您的 add
中使用时按钮。
最后编辑:
这是我在使用 swift 5.5 async/await 的实验中使用的代码:
struct ContentView: View {
@StateObject var viewModel = WeatherViewModel()
@State private var cityName = ""
@State private var showingDetail = false
var body: some View {
NavigationView {
VStack {
List {
ForEach(viewModel.cityNameList) { city in
NavigationLink(destination: DetailView(detail: city), label: {
Text(city.name).font(.system(size: 32))
Spacer()
Text("\(city.main.temp, specifier: "%.0f")°").font(.system(size: 32))
})
}.onDelete { index in
viewModel.cityNameList.remove(atOffsets: index)
}
}.refreshable {
viewModel.updateAllWeather() // <--- here
}
}
.environmentObject(viewModel) // <--- here
.navigationTitle("Weather")
.toolbar {
ToolbarItem(placement: (.bottomBar)) {
HStack {
TextField("Enter City Name", text: $cityName)
.frame(minWidth: 100, idealWidth: 150, maxWidth: 240, minHeight: 30, idealHeight: 40, maxHeight: 50, alignment: .leading)
Spacer()
Button(action: {
Task { // <--- here
await viewModel.fetchWeather(for: cityName)
cityName = ""
showingDetail.toggle()
}
}) {
HStack {
Image(systemName: "plus").font(.title)
}
.padding(15)
.foregroundColor(.white)
.background(Color.green)
.cornerRadius(40)
}
.sheet(isPresented: $showingDetail) {
ForEach(0..<viewModel.cityNameList.count, id: \.self) { city in
if (city == viewModel.cityNameList.count-1) {
DetailView(detail: viewModel.cityNameList[city])
.environmentObject(viewModel) // <--- here
}
}
}
}
}
}
}
}
}
struct DetailView: View {
@EnvironmentObject var viewModel: WeatherViewModel // <--- here
@State private var cityName = ""
@State var selection: Int? = nil
var detail: WeatherModel
var body: some View {
VStack(spacing: 20) {
Text(detail.name)
.font(.system(size: 32))
Text("\(detail.main.temp, specifier: "%.0f")°")
.font(.system(size: 44))
Text(detail.firstWeatherInfo())
.font(.system(size: 24))
}
}
}
class WeatherViewModel: ObservableObject {
@Published var cityNameList = [WeatherModel]()
// add or update function
func addToList( _ city: WeatherModel) {
// if already have this city, just update it
if let ndx = cityNameList.firstIndex(where: {$0.name == city.name}) {
cityNameList[ndx].main = city.main
cityNameList[ndx].weather = city.weather
} else {
// add a new city to the list
cityNameList.append(city)
}
}
// note the async
func fetchWeather(for cityName: String) async {
guard let url = URL(string: "https://api.openweathermap.org/data/2.5/weather?q=\(cityName)&units=imperial&appid=YOURKEY") else { return }
do {
let (data, response) = try await URLSession.shared.data(for: URLRequest(url: url))
guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
// throw URLError(.badServerResponse) // todo
print(URLError(.badServerResponse))
return
}
let result = try JSONDecoder().decode(WeatherModel.self, from: data)
DispatchQueue.main.async {
self.addToList(result)
}
}
catch {
return // todo
}
}
// fetch all the latest weather info concurrently
func updateAllWeather() {
let listOfNames = cityNameList.map{$0.name}
Task {
await withTaskGroup(of: Void.self) { group in
for city in listOfNames {
group.addTask { await self.fetchWeather(for: city) }
}
}
}
}
}
关于swift - 如何在SwiftUI中使用.refreshable调用API并刷新列表,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/69968555/