#ios #swift #swiftui #swiftui-list #swiftui-navigationlink
Вопрос:
Я пытаюсь настроить приложение погоды SwiftUI. когда пользователь ищет название города в текстовом поле, а затем нажимает кнопку поиска, в списке должен появиться элемент списка навигационных ссылок. Затем пользователь должен иметь возможность щелкнуть ссылку навигации и перейти к подробному просмотру. Моя цель состоит в том, чтобы поисковые навигационные ссылки заполняли список. Однако мои поисковые города не заполняются в списке, и я не уверен, почему. В contentView я настраиваю список с функцией ForEach, которая передается в cityNameList, который является экземпляром модели WeatherViewModel. Я ожидаю, что текст(город.название) должен отображаться в качестве элемента списка навигационных ссылок. Как я должен настроить представление содержимого или модель представления для заполнения списка элементами списка навигационных ссылок? Смотрите Мой код ниже:
Представление содержимого
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)amp;units=imperialamp;appid=lt;MyAPIKeygt;") 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() } }
Model
import Foundation struct WeatherModel: Identifiable, Codable { var id = UUID() var name: String = "" var main: CurrentWeather = CurrentWeather() var weather: [WeatherInfo] = [] func firstWeatherInfo() -gt; String { return weather.count gt; 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()) } }
Комментарии:
1. грустно видеть, что вы не последовали моему совету из вашего предыдущего вопроса, как вам будет угодно. Вы не видите список
NavigationLink
, потому что у вас ничего нет в списке. Обратите внимание, что вы@Published var cityNameList = [WeatherModel]()
не должны были@Published var cityNameList = [WeatherViewModel]()
этого делать . В вашемfetchWeather()
добавьте результаты (в виде модели погоды) в свойcityNameList
.2. Спасибо @workingdog! После добавления
@Published var cityNameList = [WeatherModel]()
, похоже, что модель погоды должна будет соответствовать определяемой. Как я могу это сделать?3. используйте это:
struct WeatherModel: Identifiable, Codable { let id = UUID() ....}
. Не волнуйтесь, это не повлияет на декодирование json (Xcode предупредит вас об этом).4. @workingdog Я обновил свой код выше, чтобы отразить ваш ответ на мой предыдущий вопрос (см. Код выше). Я также попытался передать ViewModel.cityWeather в список ForEach в списке, но я получаю следующую ошибку:
Generic struct 'ForEach' requires that 'WeatherModel' conform to 'RandomAccessCollection'
5. @workingdog Я реализовал исправления, которые вы предложили в приведенном ниже примере, но элементы списка городов по-прежнему не заполняются в списке.
Ответ №1:
попробуйте что-нибудь вроде этого примера кода, который хорошо работает для меня:
struct WeatherModel: Identifiable, Codable { let id = UUID() var name: String = "" var main: CurrentWeather = CurrentWeather() var weather: [WeatherInfo] = [] func firstWeatherInfo() -gt; String { return weather.count gt; 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)amp;units=imperialamp;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) // lt;-- you HAVE TO deal with errors here } } task.resume() } }