有了 Observation,SwiftUI 还需要 Combine 吗
- Published on
- Authors
- Name
- zzzwco
- @zzzwco
Table of Contents
Xcode 15b6
macOS 14
Combine 初体验
SwiftUI 和 Combine 是在 WWDC 19 上同时发布的,它们非常利于构建声明式和响应式应用。对于简单的应用,如果不需要处理复杂的数据流,我们并不需要直接使用 Combine。SwiftUI 目前已经内置了一些能够使用 Combine 特性的 API,比如 Timer、URLSession、NotificationCenter 等,我们可以通过 onReceive 来订阅事件。
比如这里有个简单的秒钟计时器:
struct ContentView: View {
private let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
@State private var count = 0
var body: some View {
Text(count.formatted())
.onReceive(timer) { _ in
count += 1
}
}
}
这样写虽然能够实现功能,但是 UI 和业务逻辑放在一个文件中,不利于单元测试和代码复用。虽然 Apple 没有为 SwiftUI 应用钦定一个架构,笔者还是倾向于选择 MVVM 来进行实际开发。
将上面的代码稍作改动:
struct ContentView: View {
private let vm = ContentViewModel()
var body: some View {
Text(vm.count.formatted())
}
}
import Combine
final class ContentViewModel: ObservableObject {
@Published var count = 0
private let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
private var bag = Set<AnyCancellable>()
init() {
timer.sink { [weak self] _ in
guard let self else { return }
count += 1
}
.store(in: &bag)
}
}
代码拆分后,View 只需要负责视图的布局和数据展示,ViewModel 处理业务逻辑和事件,各司其职,一目了然。
Observation 发布之后
WWDC 23 上发布了一个新的框架:Observation。它极大地简化了 SwiftUI 的状态管理,同时对性能也有一定的提升。
@Observable
final class Book: Identifiable {
var title = "Sample Book Title"
var author = Author()
var isAvailable = true
}
struct BookView: View {
@State private var book: Book
var body: some View {
Text(book.title)
}
}
@Observable
其实是一个宏,宏(Macros)也是 WWDC 23 加入 Swift 标准库的新特性,它可以减少样板代码,提高编码效率。
在 Observation 发布之前,如果要熟练地对 SwiftUI 进行状态管理,需要学习各种各样的属性包装器并了解它们的区别和使用场景。而现在不需要了,如上代码所示,我们只需要使用宏进行声明,然后在视图中调用相应的属性即可,如果某个属性不想被观察,只需要使用宏 @ObservationIgnored 进行标记即可。
这个简单的示例并不足以充分展示 Observation 对 SwiftUI 状态管理的提升作用,如需深入了解 Observation,可以阅读以下文档或观看 Session:
- Managing model data in your app
- Migrating from the Observable Object protocol to the Observable macro
- Discover Observation in SwiftUI
回到本文要探讨的问题:有了 Observation,SwiftUI 还需要 Combine 吗?答案是肯定的。因为虽然我们不再需要 Combine 提供的 ObservableObject 以及各种 SwiftUI 提供的属性包装器了,但 Observation 不具备 Combine 处理复杂的数据流的能力。
我们来看一个非常实用的例子:输入框的防抖。代码如下:
struct SampleView: View {
@State private var vm = SampleViewModel()
var body: some View {
Form {
Section {
TextField("Input something", text: $vm.searchText)
} header: {
Text("Debounce")
} footer: {
Text("Searching **\(vm.searchKeywords)** ...")
.opacity(vm.isSearching ? 1 : 0)
}
.textCase(nil)
}
}
}
import SwiftUI
import Observation
import Combine
/// The ViewModel responsible for managing the state and logic for ``SampleView``.
/// It performs a debounced search based on user input.
@Observable
final class SampleViewModel {
/// The text entered by the user in the search text field.
var searchText: String = "" {
didSet {
// Send the updated text to the Combine pipeline
searchTextPub.send(searchText)
}
}
/// The processed keywords used for searching.
var searchKeywords = ""
/// A flag indicating whether a search is currently in progress.
var isSearching = false
/// A Combine PassthroughSubject to publish updates to `searchText`.
private let searchTextPub = PassthroughSubject<String, Never>()
/// A collection of AnyCancellable to store Combine subscriptions.
private var bag = Set<AnyCancellable>()
init() {
searchTextPub
.dropFirst() // Ignore the initial value
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } // Trim whitespaces and new lines
.filter { !$0.isEmpty } // Filter out empty strings
.debounce(for: .seconds(0.5), scheduler: DispatchQueue.main) // Debounce for 0.5 seconds
.sink { [weak self] value in // Subscribe to updates
guard let self else { return }
if searchKeywords != value {
searchKeywords = value
mockRequest()
}
}
.store(in: &bag)
}
/// Simulates a search request with a random delay.
func mockRequest() {
isSearching = true
Task { @MainActor in
try? await Task.sleep(for: .seconds(.random(in: 1...2)))
isSearching = false
}
}
}
可以看到,视图层的代码非常简单,只需要从 ViewModel 取数据即可,但 ViewModel 不少工作来实现输入的防抖和对无效数据的过滤。
由于使用的 Observation,所以在 ViewModel 中无法像之前使用 Combine 那样直接从使用 @Published 声明的属性中方便地使用 publisher 实例,这里需要多写一些代码来向订阅者发送事件。
从这个实例可以看出,对于复杂的视图和数据流,Combine 仍然是不可或缺的。
总结
- Combine 和 Observation 并不是对立关系,二者可以共存,使用场景不同。
- Observation 天生就是为视图服务的,主要用于状态管理。
- Combine 有许多的操作符,非常适合处理业务层复杂的数据流。