为什么SwiftUI应用TextField每输入一个字符就触发API拉取与ViewModel初始化?
问题根源
你的问题本质是SwiftUI视图刷新机制导致的:enteredIngredient 是@State属性,每次TextField输入字符修改该值都会触发整个IngredientView的body重新计算。你当前使用的NavigationLink(isActive:destination:label:)接口中,destination闭包的内容会在每次body刷新时直接求值,而非等到跳转触发时才执行,因此每次输入都会直接初始化ViewModel,而你的ViewModel在init方法中直接调用了FetchData()发起网络请求,就出现了输入一个字符就触发一次下载的问题。
另外你的ViewModel当前是普通类,没有遵守ObservableObject协议,网络请求回来的数据变更无法通知SwiftUI刷新DetailsView的UI,后续也会出现页面不更新的问题。
修复步骤
1. 改造ViewModel
将ViewModel改为可观察对象,把需要触发UI刷新的属性用@Published修饰,同时把网络请求调用从init方法移除,避免初始化时直接发起请求:
// ViewModel.swift import Foundation import UIKit import Combine class ViewModel: ObservableObject { let dataType: String let urlString: String @Published var drinks: Drinks = Drinks(drinks: [Cocktail]()) @Published var ingredients: Ingredients = Ingredients(ingredients: [Ingredient]()) @Published var alcohols: Alcohols = Alcohols(drinks: [Alcohol]()) @Published var dataIsFound: Bool = true init(urlString: String, dataType: String) { self.urlString = urlString self.dataType = dataType // 移除此处的FetchData()调用 } // 其余方法(FetchData、buildIngredients)保持不变 }
2. 改造IngredientView跳转逻辑
推荐使用iOS 16+的navigationDestination接口,只有跳转触发时才会初始化目标视图和ViewModel,避免提前执行:
// IngredientView.swift import SwiftUI struct IngredientView: View { @State var enteredIngredient: String = "" // 新增路由绑定属性 @State private var targetIngredient: String? var body: some View { ZStack { Color(.systemTeal) .edgesIgnoringSafeArea(.all) VStack(spacing: 5.0) { Text("请输入你想要搜索的原料") .padding(50.0) .foregroundColor(.white) .font(.system(size: 20.0)) .navigationTitle("按原料搜索") TextField("原料", text: $enteredIngredient) .textFieldStyle(.roundedBorder) .frame(width: 250.0, height: 60.0, alignment: .center) .foregroundColor(.black) Button { if !enteredIngredient.isEmpty { targetIngredient = enteredIngredient } } label: { Text("继续") .foregroundColor(.white) .font(.system(size: 25.0)) .padding() .frame(width: 250.0) } .background(.blue) .clipShape(RoundedRectangle(cornerRadius: 30.0)) } // 新增跳转路由配置 .navigationDestination(item: $targetIngredient) { ingredient in DetailsView(viewModel: ViewModel( urlString: "https://www.thecocktaildb.com/api/json/v1/1/search.php?i=\(ingredient)", dataType: "Ingredients" )) } } } }
如果需要兼容iOS 15及更早版本,可以用LazyView包装原有NavigationLink的destination,延迟初始化执行:
// 先定义LazyView工具类 struct LazyView<Content: View>: View { let content: () -> Content init(@ViewBuilder _ content: @escaping () -> Content) { self.content = content } var body: some View { content() } } // 原有NavigationLink修改为 NavigationLink(isActive: $ingredientIsEntered) { LazyView { DetailsView(viewModel: ViewModel(urlString: "https://www.thecocktaildb.com/api/json/v1/1/search.php?i=\(enteredIngredient)", dataType: "Ingredients")) } } label: { EmptyView() }
3. 改造DetailsView
用@StateObject持有ViewModel,在视图首次显示时发起网络请求:
// DetailsView.swift import SwiftUI import SDWebImageSwiftUI import UIKit struct DetailsView: View { // 用@StateObject持有可观察的ViewModel @StateObject var viewModel: ViewModel var body: some View { // 原有body的列表展示逻辑保持不变 .onAppear { // 仅在视图首次显示时发起请求 viewModel.FetchData() } } }
内容的提问来源于stack exchange,提问作者sjlearmonth
相关产品推荐
相关产品推荐

