如何在自定义 SwiftUI 视图中设置文本字段和按钮
How to set up Textfield and Button in Custom SwiftUI View
我正在尝试将我的 openweathermap 应用程序中的文本字段和按钮配置为在其自己的视图中,而不是在主要内容视图中。在 TextFieldView 中,按钮的操作设置为调用 API 响应。然后,来自响应的天气数据被填充到基于 sheet 的 DetailView 上,它由 TextFieldView 中的按钮触发。我将 sheet 中的 ForEach 方法配置为 return 添加到 WeatherModel 数组的最后一个城市(从技术上讲,这将是最近输入文本字段的城市),然后填充 sheet - 基于 DetailView,带有该城市的天气数据。以前,当我在 ContentView 中设置包含文本字段、按钮和 sheet 控件的 HStack 时,Sheet 会正确显示刚刚输入文本字段的城市的天气。将这些项目移动到单独的 TextFieldView 后,ForEach 方法似乎已停止工作。相反,在文本字段中输入城市名称后 returned 的天气信息显示错误。例如,如果我在文本字段中输入“London”,sheet 中的 DetailView 将完全空白。如果我随后输入“Rome”作为下一个条目,sheet 中的 DetailView 会显示上一个“London”条目的天气信息。在文本字段中输入“巴黎”会显示“罗马”的天气信息,依此类推...
总而言之,在我将整个文本字段和按钮功能移动到单独的视图后,sheet 中的 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")°")
.font(.system(size: 18))
})
}
.onDelete { indexSet in
let reversed = Array(viewModel.cityNameList.reversed())
let items = Set(indexSet.map { reversed[[=10=]].id })
viewModel.cityNameList.removeAll { items.contains([=10=].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..<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())
}
}
详细视图
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")°")
.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.escaped())&units=imperial&appid=<YourAPIKey>") 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{[=13=].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: {[=13=].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() -> String {
return weather.count > 0 ? weather[0].description : ""
}
}
struct CurrentWeather: Codable {
var temp: Double = 0.0
var humidity = 0
}
struct WeatherInfo: Codable {
var description: String = ""
}
您需要在 TextFieldView
中使用 ObservedObject
才能使用您的
您在 ContentView
中创建的原始(单一真实来源)@StateObject var viewModel
并观察对其的任何更改。
所以使用这个:
struct TextFieldView: View {
@ObservedObject var viewModel: WeatherViewModel
...
}
我正在尝试将我的 openweathermap 应用程序中的文本字段和按钮配置为在其自己的视图中,而不是在主要内容视图中。在 TextFieldView 中,按钮的操作设置为调用 API 响应。然后,来自响应的天气数据被填充到基于 sheet 的 DetailView 上,它由 TextFieldView 中的按钮触发。我将 sheet 中的 ForEach 方法配置为 return 添加到 WeatherModel 数组的最后一个城市(从技术上讲,这将是最近输入文本字段的城市),然后填充 sheet - 基于 DetailView,带有该城市的天气数据。以前,当我在 ContentView 中设置包含文本字段、按钮和 sheet 控件的 HStack 时,Sheet 会正确显示刚刚输入文本字段的城市的天气。将这些项目移动到单独的 TextFieldView 后,ForEach 方法似乎已停止工作。相反,在文本字段中输入城市名称后 returned 的天气信息显示错误。例如,如果我在文本字段中输入“London”,sheet 中的 DetailView 将完全空白。如果我随后输入“Rome”作为下一个条目,sheet 中的 DetailView 会显示上一个“London”条目的天气信息。在文本字段中输入“巴黎”会显示“罗马”的天气信息,依此类推...
总而言之,在我将整个文本字段和按钮功能移动到单独的视图后,sheet 中的 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")°")
.font(.system(size: 18))
})
}
.onDelete { indexSet in
let reversed = Array(viewModel.cityNameList.reversed())
let items = Set(indexSet.map { reversed[[=10=]].id })
viewModel.cityNameList.removeAll { items.contains([=10=].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..<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())
}
}
详细视图
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")°")
.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.escaped())&units=imperial&appid=<YourAPIKey>") 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{[=13=].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: {[=13=].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() -> String {
return weather.count > 0 ? weather[0].description : ""
}
}
struct CurrentWeather: Codable {
var temp: Double = 0.0
var humidity = 0
}
struct WeatherInfo: Codable {
var description: String = ""
}
您需要在 TextFieldView
中使用 ObservedObject
才能使用您的
您在 ContentView
中创建的原始(单一真实来源)@StateObject var viewModel
并观察对其的任何更改。
所以使用这个:
struct TextFieldView: View {
@ObservedObject var viewModel: WeatherViewModel
...
}