做iOS开发的人应该都遇到过这种场景:用户在搜索框里打字,你得实时去后台查数据。以前的做法是加个延时器,等用户停顿一下再发请求。现在有了SwiftUI和Combine,这事儿变得自然又顺滑。
从一个搜索框说起
比如你在做一个商品搜索功能。用户每输入一个字符,界面上的列表就要变。如果每次都立刻请求网络,服务器扛不住;如果用GCD加延迟,逻辑绕,还容易出错。
这时候Combine就派上用场了。它能监听文本变化流,做去抖(debounce),再交给网络层。SwiftUI则负责把结果自动刷到界面上,不用手动reloadData。
class SearchViewModel: ObservableObject {
@Published var query = ""
@Published var results = [Product]();
private var cancellables = Set<AnyCancellable>();
init() {
$query
.removeDuplicates()
.debounce(for: .seconds(0.5), scheduler: RunLoop.main)
.flatMap { query -> AnyPublisher<[Product], Never> in
if query.isEmpty {
return Just([]).eraseToAnyPublisher();
}
return API.searchProducts(query)
.catch { _ in Just([]) }
.eraseToAnyPublisher();
}
.receive(on: RunLoop.main)
.assign(to: \SearchViewModel.results, on: self)
.store(in: &cancellables);
}
}
上面这段代码里,$query是Combine里的Publisher,只要query一变,后续链式操作就会触发。debounce控制频率,flatMap负责异步拉数据,最后assign把结果写回属性。整个过程声明式,一气呵成。
SwiftUI怎么接上这个数据流
视图层更简单。用@ObservedObject接住ViewModel,Text绑定query,List展示results。数据一更新,界面自动刷新。
struct SearchView: View {
@StateObject private var viewModel = SearchViewModel();
var body: some View {
VStack {
TextField("搜商品", text: $viewModel.query)
.padding()
List(viewModel.results) { product in
Text(product.name)
}
}
}
}
你看,没有代理,没有回调,也没有通知。输入框和列表之间,靠数据流连接。这种编程方式更像是在描述“当A变了,B该怎样”,而不是一步步指挥机器做什么。
实际开发中的小坑
刚用Combine时容易忘掉store(in:),结果订阅被释放,数据收不到。还有就是主线程问题,网络回来的数据如果不切回主线程,SwiftUI更新会失效。记得加receive(on: RunLoop.main)。
另外,不是所有地方都要用Combine。简单的状态切换,用@State照样够用。Combine适合处理复杂的异步链路,比如登录流程、表单验证、多源数据合并这些场景。
把SwiftUI和Combine搭一块,像是给App装了根神经。数据从一端进去,另一端自动反应,中间的传导逻辑清晰可测。比起过去一堆delegate和completion block来回跳,现在读代码更像是看一条河怎么流动。