搜索|ViewModel 层搜索逻辑的设计
了解如何使用 Swift 的 Observation 框架和 didSet 属性观察器,在 SwiftUI 应用中构建高效响应式的搜索功能架构。

使用 Swift 提供的 Observation + didSet 来实现 ViewModel 层的搜索功能。
这篇文章专注于搜索逻辑的设计与实现,对于搜索框 UI 界面的实现,可参考下面这篇文章:
搜索|使用 .searchable 添加搜索框
了解如何使用 SwiftUI 提供的 searchable 修饰器,为你的 App 添加方便好用的搜索功能。

搜索逻辑数据流

在 ViewModel 中存储搜索字符串
首先,在 ViewModel 中定义搜索文本属性:
@Observable
@MainActor
final class BookmarksViewModel: BaseViewModel {
/// 电影搜索文本
var movieSearchText: String = ""
/// 书籍搜索文本
var bookSearchText: String = ""
}
设计要点:
- 使用 @Observable 宏让属性变化可被 SwiftUI 观察
- 分离不同类型的搜索文本,保持独立状态
- 使用 @MainActor 确保 UI 更新在主线程
使用 didSet 触发数据检索
为搜索属性添加 didSet 观察器:
@Observable
@MainActor
final class BookmarksViewModel: BaseViewModel {
/// 电影搜索文本
var movieSearchText: String = "" {
didSet {
Task { await fetch() }
}
}
/// 书籍搜索文本
var bookSearchText: String = "" {
didSet {
Task { await fetch() }
}
}
}
设计要点:
- 自动响应文本变化,无需手动监听
- 创建异步任务,不阻塞 UI 线程
实现智能防抖机制
实现防抖功能,避免频繁查询:
// BaseViewModel 协议中的 fetch 方法
func fetch() async {
// 取消之前的防抖任务
debounceTask?.cancel()
// 创建新的防抖任务
debounceTask = Task { @MainActor [weak self] in
guard let self = self else { return }
do {
// 等待防抖延迟
try await Task.sleep(for: .milliseconds(100))
// 检查任务是否被取消
guard !Task.isCancelled else { return }
// 执行实际的数据获取
await self.performFetch()
} catch {
// 处理错误
}
}
}
防抖效果:
- 用户快速输入 "abc" 时,只会在停止输入 100ms 后执行一次查询
- 有效减少数据库压力和网络请求
使用 Task.detached 在后台执行查询
在 performFetch 中执行实际的数据查询。
如果使用 SwiftData,可使用 DataActor 将任务安全的分派到后台线程。
UI 层的双向数据绑定
在 SearchBarView 中实现与 ViewModel 的绑定:
struct SearchBarView: View {
var bookmarkType: BookmarkType
var body: some View {
@Bindable var viewModel = BookmarksViewModel.shared
TextField(
"搜索\(bookmarkType == .movie ? "电影" : "书籍")...",
text: bookmarkType == .movie
? $viewModel.movieSearchText
: $viewModel.bookSearchText
)
.submitLabel(.search)
}
}
绑定特点:
- 使用 @Bindable 创建双向绑定
- 根据内容类型动态选择绑定的属性
- TextField 的每次输入都会更新 ViewModel