Как настроить текстовое поле и кнопку в пользовательском представлении SwiftUI

#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  ... }