SwiftUI .searchable引发视图重绘问题求助
SwiftUI .searchable 重绘卡顿问题解决
问题背景
使用SwiftUI的.searchable修饰符时,为了获取isSearching环境变量来隐藏视图,按照官方要求将.searchable移到父视图层级,并用ObservableObject存储搜索文本。但每次在搜索栏输入字符时,整个视图都会重绘,原本被隐藏的元素会短暂出现,导致界面卡顿。尝试过从根视图注入环境对象、切换使用ObservableObject和StateObject,问题依旧存在。
复现代码
子视图(Temp)
/* 原本把.searchable和searchText放在这个视图里时一切正常 */ struct Temp: View { @ObservedObject var searchModel: SearchModel @Environment(\.isSearching) private var isSearching @FetchRequest( sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: false)], animation: .default) private var savedItem: FetchedResults<Item> var body: some View { List { if isSearching { } else { withAnimation { Button(action: {self.shouldPresentSheet.toggle()}) { BeansListButton() .contentShape(Rectangle()) } }.listRowBackground(Color("colorBeanListBG")) } ForEach(savedItem, id: \.self) { item in Section { NavigationLink { Detail(item: item) } label: { ZStack(alignment: .topTrailing){ ListItem(brand: item.brand!, product: item.product!, weight: item.weight, rating: Int(item.rating)) } } } .listRowBackground(Color("colorBeanListBG")) } .onDelete(perform: deleteItems) .listRowSeparator(.hidden) } .onChange(of: searchModel.searchText) { search in let productKeyPredicate = NSPredicate(format: "product CONTAINS[cd] %@", search) let brandKeyPredicate = NSPredicate(format: "brand CONTAINS[cd] %@", search) let orPredicate = NSCompoundPredicate(type: .or, subpredicates: [productKeyPredicate, brandKeyPredicate]) if search.isEmpty { savedItem.nsPredicate = NSPredicate(value: true) } else { savedItem.nsPredicate = orPredicate } } } }
父视图
/* 为了访问isSearching环境变量,需要嵌套结构并使用ObservableObject */ @ObservedObject private var searchModel = SearchModel() NavigationStack { Temp(searchModel: searchModel) .searchable(text: $searchModel.searchText, placement: .navigationBarDrawer(displayMode: .always)) }
ViewModel
class SearchModel: ObservableObject { @Published var searchText: String = "" }
解决方案
方案1:封装独立子视图限制重绘范围
把需要隐藏的按钮封装成独立子视图,直接在子视图中响应isSearching环境变量,避免父视图整体重绘影响:
struct HiddenButtonView: View { @Environment(\.isSearching) private var isSearching @Binding var shouldPresentSheet: Bool var body: some View { if !isSearching { withAnimation { Button(action: { shouldPresentSheet.toggle() }) { BeansListButton() .contentShape(Rectangle()) } } .listRowBackground(Color("colorBeanListBG")) } } }
替换Temp视图List中的原有按钮代码:
List { HiddenButtonView(shouldPresentSheet: $shouldPresentSheet) // ... 其他列表内容 }
这样只有按钮子视图会响应搜索状态变化,父视图不会因搜索文本更新触发全局重绘。
方案2:将搜索逻辑移至ViewModel优化状态流转
把搜索条件生成逻辑从视图层移到SearchModel,通过@Published属性驱动FetchRequest更新,减少视图层的状态依赖:
class SearchModel: ObservableObject { @Published var searchText: String = "" private(set) var predicate: NSPredicate = NSPredicate(value: true) init() { $searchText .map { text in guard !text.isEmpty else { return NSPredicate(value: true) } let productPredicate = NSPredicate(format: "product CONTAINS[cd] %@", text) let brandPredicate = NSPredicate(format: "brand CONTAINS[cd] %@", text) return NSCompoundPredicate(type: .or, subpredicates: [productPredicate, brandPredicate]) } .assign(to: &$predicate) } }
修改Temp视图的FetchRequest初始化方式,监听ViewModel的predicate变化:
struct Temp: View { @ObservedObject var searchModel: SearchModel @Environment(\.isSearching) private var isSearching private var savedItem: FetchRequest<Item> init(searchModel: SearchModel) { self.searchModel = searchModel _savedItem = FetchRequest( sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: false)], predicate: searchModel.predicate, animation: .default ) } var body: some View { List { // ... 列表内容 } .onChange(of: searchModel.predicate) { newPredicate in savedItem.nsPredicate = newPredicate } } }
方案3:使用StateObject绑定ViewModel生命周期
在父视图中将@ObservedObject替换为@StateObject,确保ViewModel仅初始化一次,避免因父视图重建导致的不必要状态重置:
@StateObject private var searchModel = SearchModel()
关键原理
- SwiftUI视图重绘由状态变化触发,拆分独立子视图可限制重绘范围,避免全局刷新。
- 将业务逻辑(如搜索条件生成)移至ViewModel,减少视图层的状态依赖,降低重绘频率。
StateObject确保ViewModel与父视图生命周期绑定,避免不必要的实例重建和状态重置。
内容的提问来源于stack exchange,提问作者byrnec25
相关产品推荐
相关产品推荐

