You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.30 17:03:37