【问题标题】:How to use .refreshable in SwiftUI to call API and refresh list如何在 SwiftUI 中使用 .refreshable 调用 API 并刷新列表
【发布时间】:2021-12-26 07:33:58
【问题描述】:

我正在尝试将 .refreshable 添加到我的 SwiftUI openweathermap 应用程序中,以便下拉和刷新从 API 返回到应用程序的值。我将我的应用程序设置为允许用户在文本字段中输入城市名称,点击搜索按钮,然后在工作表中查看该城市的天气详细信息。关闭表格后,用户可以在列表中以导航链接的形式查看他/她之前搜索过的所有城市,每个列表链接中都可以看到城市名称和温度。我试图将.refreshable {} 添加到我的 ContentView 的列表中。我尝试在我的 ViewModel 中设置 .refreshable 以调用 fetchWeather(),而后者又设置为将用户输入的 cityName 作为参数传递到 API URL(也在 ViewModel 中)。但是,我现在认为这不会刷新天气数据,因为调用fetchWeather() 的操作是在工具栏按钮中定义的,而不是在列表中。知道如何设置 .refreshable 来刷新列表中每个搜索城市的天气数据吗?请参阅下面的代码。谢谢!

内容视图

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")&deg;")
                .font(.system(size: 44))
            Text(detail.firstWeatherInfo())
                .font(.system(size: 24))
        }
    }
}

struct DetailView_Previews: PreviewProvider {
    static var previews: some View {
        DetailView(detail: WeatherModel.init())
    }
}

视图模型

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 = ""
}

【问题讨论】:

    标签: swift swiftui swiftui-list swiftui-navigationlink swiftui-navigationview


    【解决方案1】:

    我会做的是这个(或类似的更并发和防错的方法):

    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()
    }
      
                
    

    注意:DetailView 中不应包含 @StateObject var viewModel = WeatherViewModel()。 您应该将模型传入(如果需要),并拥有@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:这是一个更强大的排序方案。

    fetchWeather 中删除self.cityNameList.sort(by: {$0.name &lt; $1.name})

    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:保持原始添加中的顺序。

    EDIT1EDIT2 中移除所有模组。

    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")&deg;").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")&deg;")
                    .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) }
                    }
                }
            }
        }
        
    }
    

    【讨论】:

    • 我感觉你会回应。哈哈谢谢。我现在正在尝试这个解决方案。
    • 这个解决方案似乎在获取最新的天气信息方面有效。天气数据似乎更新得很好。但是,当我下拉刷新时,似乎城市列表项已重新排列。 cityNameList.removeAll() 是否也可能删除有关 WeatherModal 数组中 listItems 顺序的信息?
    • 是否还有一种方法可以下拉列表以刷新而不下拉导航标题?我通过 Xcode 将这个应用程序加载到我的手机上。当我下拉刷新时,navigationTitle 会被下拉,在某些情况下会出现奇怪的故障卡在列表下方。
    • 更新了我处理排序的答案。至于下拉,你可能需要和苹果谈谈,祝你好运。
    • Edit #3 似乎是最好的解决方案。我实现了所有 3 个,第 3 个似乎最适合防止在列表中重新排序,以及处理重复值。此外,手机上的故障似乎已经解决。我相信这也是由于价值被阻止重新排序。凉爽的。我将查找如何向 fetchWeather 添加完成处理程序,除非您对此也有一些建议。非常感谢!
    【解决方案2】:
    import UIKit
    class VC: UIViewController  {
    
    var arrlabelpass = [String]()
    var arrimagepass = [UIImage]()
    var arrTable  = ["1","1","1","1","1","1"]
    var arrTablelbl  = ["12","14","13","11","16","17"]
    let itemcell = "CCell"
    let itemcell1 = "TCell"
    
    var refresh : UIRefreshControl {
        let ref = UIRefreshControl()
        ref.addTarget(self, action: #selector(handler(_:)), for: .valueChanged)
        return ref
    }
    
    @IBOutlet weak var tableView: UITableView!
    @IBOutlet weak var collectionView: UICollectionView!
    override func viewDidLoad() {
        super.viewDidLoad()
       
        tableView.delegate = self
        tableView.dataSource = self
        
        collectionView.delegate = self
        collectionView.dataSource = self
        
        let nib = UINib (nibName: itemcell, bundle: nil)
        collectionView.register(nib, forCellWithReuseIdentifier: itemcell)
        
        let nib1 = UINib(nibName: itemcell1, bundle: nil)
        tableView.register(nib1, forCellReuseIdentifier: itemcell1)
        collectionView.addSubview(refresh)
        collectionView.isHidden = true
    }
    
    @objc func handler(_ control:UIRefreshControl) {
        
    //        collectionView.backgroundColor = self.randomElement()
        control.endRefreshing()
    }
    
    }
    
    extension VC : UITableViewDelegate , UITableViewDataSource , UICollectionViewDelegate , UICollectionViewDataSource  {
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return arrTable.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let tCell = tableView.dequeueReusableCell(withIdentifier: itemcell1, for: indexPath)as! TCell
        tCell.tIMG.image = UIImage(named: arrTable[indexPath.row])
        tCell.LBL.text = arrTablelbl[indexPath.row]
        return tCell
    }
    
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        let lblindex = arrTablelbl[indexPath.row]
        let imageindex = UIImage(named: arrTable[indexPath.row])
        arrlabelpass.append(lblindex)
        arrimagepass.append(imageindex!)
        collectionView.reloadData()
        collectionView.isHidden = false
        arrTablelbl[indexPath.row].removeAll()
        arrTable[indexPath.row].removeAll()
        tableView.reloadData()
    
    }
    
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return arrlabelpass.count
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let ccell = collectionView.dequeueReusableCell(withReuseIdentifier: itemcell, for: indexPath)as! CCell
        ccell.cIMG.image = arrimagepass[indexPath.row]
        ccell.cLBL.text = arrlabelpass[indexPath.row]
        return ccell
        
    }
    
    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        tableView.reloadData()
        arrimagepass.remove(at: indexPath.row)
        arrlabelpass.remove(at: indexPath.row)
        collectionView.reloadData()
    }
    
    }
    

    【讨论】:

      猜你喜欢
      • 2021-12-19
      • 1970-01-01
      • 2019-08-01
      • 2018-08-26
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-07-08
      相关资源
      最近更新 更多