如何在 SwiftUI 列表中设置 NavigationLink

How to setup NavigationLink inside SwiftUI list

我正在尝试设置 SwiftUI 天气应用程序。当用户在文本字段中搜索城市名称然后点击搜索按钮时,NavigationLink 列表项应该出现在列表中。然后,用户应该能够单击导航 link 并重新定向到详细视图。我的目标是让搜索到的导航 link 填充列表。但是,我的搜索城市没有出现在列表中,我不确定为什么。在 ContentView 中,我使用 ForEach 函数设置了一个列表,该函数传入 cityNameList,它是 WeatherViewModel 的一个实例。我的期望是 Text(city.title) 应该显示为 NavigationLink 列表项。我应该如何配置 ContentView 或 ViewModel 以使用 NavigationLink 列表项填充列表?请参阅下面我的代码:

内容视图

import SwiftUI

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

    var body: some View {
        NavigationView {

            VStack {
                TextField("Enter City Name", text: $cityName).textFieldStyle(.roundedBorder)
                
                Button(action: {
                    viewModel.fetchWeather(for: cityName)
                    cityName = ""
                }, label: {
                    Text("Search")
                        .padding(10)
                        .background(Color.green)
                        .foregroundColor(Color.white)
                        .cornerRadius(10)
                })
                
                List {
                    ForEach(viewModel.cityWeather, id: \.id) { city in
                        NavigationLink(destination: DetailView(detail: viewModel)) {
                            HStack {
                                Text(city.cityWeather.name)
                                    .font(.system(size: 32))
                            }
                        }
                    }
                }
                
                Spacer()
            }
            .navigationTitle("Weather MVVM")
        }.padding()
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

视图模型

import Foundation

class WeatherViewModel: ObservableObject {
    
    //everytime these properties are updated, any view holding onto an instance of this viewModel will go ahead and updated the respective UI
        
    @Published var cityWeather: WeatherModel = 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
            // get data
            guard let data = data, error == nil else {
                return
            }
            
            //convert data to model
            do {
                let model = try JSONDecoder().decode(WeatherModel.self, from: data)
                
                DispatchQueue.main.async {
                    self.cityWeather = model
                }
            }
            catch {
                print(error)
            }
        }
        task.resume()
    }
}

型号

import Foundation

struct WeatherModel: Identifiable, Codable {
    var 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: Float = 0.0
}

struct WeatherInfo: Codable {
    var description: String = ""
}

详细视图

import SwiftUI

struct DetailView: View {
    
    var detail: WeatherViewModel
    
    var body: some View {
        
        VStack(spacing: 20) {
            Text(detail.cityWeather.name)
                .font(.system(size: 32))
            Text("\(detail.cityWeather.main.temp)")
                .font(.system(size: 44))
            Text(detail.cityWeather.firstWeatherInfo())
                .font(.system(size: 24))
        }
        

    }
}

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

试试像这个示例代码,对我来说效果很好:

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: Float = 0.0
}

struct WeatherInfo: Codable {
    var description: String = ""
}

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 = ""
    
    var body: some View {
        NavigationView {
            VStack {
                TextField("Enter City Name", text: $cityName).textFieldStyle(.roundedBorder)
                Button(action: {
                    viewModel.fetchWeather(for: cityName)
                    cityName = ""
                }, label: {
                    Text("Search")
                        .padding(10)
                        .background(Color.green)
                        .foregroundColor(Color.white)
                        .cornerRadius(10)
                })
                List {
                    ForEach(viewModel.cityNameList) { city in
                        NavigationLink(destination: DetailView(detail: city)) {
                            HStack {
                                Text(city.name).font(.system(size: 32))
                            }
                        }
                    }
                }
                Spacer()
            }.navigationTitle("Weather MVVM")
        }.navigationViewStyle(.stack)
    }
}

struct DetailView: View {
    var detail: WeatherModel
    
    var body: some View {
        VStack(spacing: 20) {
            Text(detail.name).font(.system(size: 32))
            Text("\(detail.main.temp)").font(.system(size: 44))
            Text(detail.firstWeatherInfo()).font(.system(size: 24))
        }
    }
}

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=YOURKEY") 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()
    }
}