#swiftui #swiftui-list #swiftui-view #swiftui-text #swiftui-button
#свифтуи #swiftui-список #swiftui-просмотр #swiftui-текст #swiftui-кнопка
Вопрос:
Я пытаюсь настроить текстовое поле и кнопку в моем приложении openweathermap так, чтобы они отображались в собственном представлении, отличном от основного представления содержимого. В TextFieldView действие кнопки настроено для вызова ответа API. Затем данные о погоде из ответа заполняются в подробном представлении на основе листа, которое запускается кнопкой в TextFieldView. Я настроил метод ForEach на листе, чтобы вернуть последний город, добавленный в массив моделей погоды (который технически был бы последним городом, введенным в текстовое поле), а затем заполнить подробное представление на основе листа данными о погоде для этого города. Ранее, когда у меня был HStack, содержащий текстовое поле, кнопку и элемент управления листом, настроенный в представлении содержимого, на листе правильно отображалась погода для города, который только что был введен в текстовое поле. После перемещения этих элементов в отдельный вид TextFieldView метод ForEach, похоже, перестал работать. Вместо этого информация о погоде, возвращаемая после ввода названия города в текстовое поле, отображается при неправильном подсчете. Например, если бы я ввел «Лондон» в текстовое поле, подробное представление на листе было бы полностью пустым. Если затем я введу «Рим» в качестве следующей записи, в подробном представлении на листе отобразится информация о погоде для предыдущей записи «Лондон». При вводе «Париж» в текстовое поле отображается информация о погоде для «Рима» и так далее…
Подводя итог, можно сказать, что метод ForEach на листе перестал работать должным образом после того, как я переместил все текстовое поле и функцию кнопок в отдельное представление. Есть идеи, почему происходит описанная мной проблема?
Вот мой код:
Представление содержимого
struct ContentView: View { // Whenever something in the viewmodel changes, the content view will know to update the UI related elements @StateObject var viewModel = WeatherViewModel() var body: some View { NavigationView { VStack(alignment: .leading) { List { ForEach(viewModel.cityNameList.reversed()) { city in NavigationLink(destination: DetailView(detail: city), label: { Text(city.name).font(.system(size: 18)) Spacer() Text("(city.main.temp, specifier: "%.0f")amp;deg;") .font(.system(size: 18)) }) } .onDelete { indexSet in let reversed = Array(viewModel.cityNameList.reversed()) let items = Set(indexSet.map { reversed[$0].id }) viewModel.cityNameList.removeAll { items.contains($0.id) } } } .refreshable { viewModel.updatedAll() } TextFieldView(viewModel: viewModel) }.navigationBarTitle("Weather", displayMode: .inline) } } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } }
TextFieldView
struct TextFieldView: View { @State private var cityName = "" @State private var showingDetail = false @FocusState var isInputActive: Bool var viewModel: WeatherViewModel var body: some View { HStack { TextField("Enter City Name", text: $cityName) .focused($isInputActive) Spacer() .toolbar { ToolbarItemGroup(placement: .keyboard) { Button("Done") { isInputActive = false } } } if isInputActive == false { Button(action: { viewModel.fetchWeather(for: cityName) cityName = "" self.showingDetail.toggle() }) { Image(systemName: "plus") .font(.largeTitle) .frame(width: 75, height: 75) .foregroundColor(Color.white) .background(Color(.systemBlue)) .clipShape(Circle()) } .sheet(isPresented: $showingDetail) { ForEach(0..lt;viewModel.cityNameList.count, id: .self) { city in if (city == viewModel.cityNameList.count-1) { DetailView(detail: viewModel.cityNameList[city]) } } } } } .frame(minWidth: 100, idealWidth: 150, maxWidth: 500, minHeight: 30, idealHeight: 40, maxHeight: 50, alignment: .leading) .padding(.leading, 16) .padding(.trailing, 16) } } struct TextFieldView_Previews: PreviewProvider { static var previews: some View { TextFieldView(viewModel: WeatherViewModel()) } }
DetailView
struct DetailView: View { @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")amp;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()) } }
ViewModel
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.escaped())amp;units=imperialamp;appid=lt;YourAPIKeygt;") 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.addToList(model) } } catch { print(error) } } task.resume() } func updatedAll() { // keep a copy of all the cities names 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) } } }
Модель
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: Double = 0.0 var humidity = 0 } struct WeatherInfo: Codable { var description: String = "" }
Комментарии:
1. Используйте это:
@ObservedObject var viewModel: WeatherViewModel
вTextFieldView
.2. ах, да… это сработало. Почему это сработало?
Ответ №1:
Вам нужно использовать ObservedObject
в себе TextFieldView
, чтобы использовать свой оригинал (единственный источник истины) @StateObject var viewModel
, который вы создаете, ContentView
и наблюдать за любыми изменениями в нем.
Так что используйте это:
struct TextFieldView: View { @ObservedObject var viewModel: WeatherViewModel ... }