Lightweight Observable 是一个可供订阅的 observable 序列的简单实现。该框架旨在保持极简的同时又足够方便。整个代码只有约 100 行(不包括注释)。借助 Lightweight Observable,您可以轻松地在 MVVM 应用程序中设置 UI 绑定,处理异步网络调用等等。
该代码深受 roberthein/observable 的影响。但是,我需要一些在语法上更接近 RxSwift 的东西,这就是我提出此代码的原因,并且出于可重用性的考虑,后来将其移至 CocoaPod。
如果您想从 1.x.x 版本更新,请查看 Lightweight Observable 2.0 迁移指南
要运行示例项目,请克隆 repo,然后从 Example 目录打开 workspace。
如果您的最低所需版本大于等于 iOS 13.0,我建议使用 Combine 而不是添加 Lightweight Observable
作为依赖项。
如果您依赖于在订阅闭包中具有当前值和先前值,请查看此扩展:Combine+Pairwise.swift。
2.2
起,Observable
实例符合 Swift Combine
中的 Publisher
协议 🎉这使得从 LightweightObservable
迁移到 Combine
变得更加容易,因为您可以使用 Combine
中的功能,而无需将底层 Observable
更改为 Publisher
。
在 PublishSubject
实例上使用 Combine
函数的示例代码
var subscriptions = Set<AnyCancellable>()
let publishSubject = PublishSubject<Int>()
publishSubject
.map { $0 * 2 }
.sink { print($0) }
.store(in: &subscriptions)
publishSubject.update(1) // Prints "2"
publishSubject.update(2) // Prints "4"
publishSubject.update(3) // Prints "6"
速查表
LightweightObservable |
Combine |
---|---|
PublishSubject |
PassthroughSubject |
Variable |
CurrentValueSubject |
此外,使用 Combine.Publisher
的属性 values
,您可以在异步序列中使用 Observable
for await value in observable.values {
// ...
}
CocoaPods 是 Cocoa 项目的依赖项管理器。有关用法和安装说明,请访问他们的网站。要使用 CocoaPods 将 Lightweight Observable 集成到您的 Xcode 项目中,请在您的 Podfile
中指定它
pod 'LightweightObservable', '~> 2.0'
Carthage 是一个去中心化的依赖项管理器,可构建您的依赖项并为您提供二进制框架。要使用 Carthage 将 Lightweight Observable 集成到您的 Xcode 项目中,请在您的 Cartfile
中指定它
github "fxm90/LightweightObservable" ~> 2.0
运行 carthage update 以构建框架并将构建的 LightweightObservable.framework
拖到您的 Xcode 项目中。
Swift Package Manager 是一种用于自动化 Swift 代码分发的工具,并且已集成到 swift
编译器中。它尚处于早期开发阶段,但 Lightweight Observable 确实支持在受支持的平台上使用它。
设置好 Swift 包后,将 Lightweight Observable 添加为依赖项就像将其添加到 Package.swift
的 dependencies
值一样容易。
dependencies: [
.package(url: "https://github.com/fxm90/LightweightObservable", from: "2.0.0")
]
该框架提供了三个类 Observable
、PublishSubject
和 Variable
Observable
:一个您可以订阅的 observable 序列,但无法更改底层值(不可变)。这对于避免对内部 API 的副作用很有用。PublishSubject
:Observable
的子类,该子类一开始为空,并且仅向订阅者发送新元素(可变)。Variable
:Observable
的子类,该子类以初始值开始,并将其或最新的元素重播给新的订阅者(可变)。PublishSubject
一开始为空,并且仅向订阅者发送新元素。
let userLocationSubject = PublishSubject<CLLocation>()
// ...
userLocationSubject.update(receivedUserLocation)
Variable
以初始值开始,并将其或最新的元素重播给新的订阅者。
let formattedTimeSubject = Variable("4:20 PM")
// ...
formattedTimeSubject.value = "4:21 PM"
// or
formattedTimeSubject.update("4:21 PM")
无法直接初始化 observable,因为这会导致序列永远不会改变。相反,您需要将 PublishSubject
或 Variable
强制转换为 Observable。
var formattedTime: Observable<String> {
formattedTimeSubject
}
lazy var formattedTime: Observable<String> = formattedTimeSubject
订阅者将在不同的时间收到通知,具体取决于相应 observable 的子类
PublishSubject
:一开始为空,并且仅向订阅者发送新元素。Variable
:以初始值开始,并将其或最新的元素重播给新的订阅者。声明
func subscribe(_ observer: @escaping Observer) -> Disposable
使用此方法通过闭包订阅 observable
formattedTime.subscribe { [weak self] newFormattedTime, oldFormattedTime in
self?.timeLabel.text = newFormattedTime
}
请注意,旧值 (oldFormattedTime
) 是底层类型的可选值,因为在首次调用订阅者时我们可能没有此值。
重要提示: 为避免保留循环和/或崩溃,当观察者需要 self
的实例时,始终使用 [weak self]
。
声明
func bind<Root: AnyObject>(to keyPath: ReferenceWritableKeyPath<Root, Value>, on object: Root) -> Disposable
也可以使用 Swift 的 KeyPath 功能将 observable 直接绑定到属性
formattedTime.bind(to: \.text, on: timeLabel)
当您订阅 Observable
时,该方法会返回一个 Disposable
,它基本上是对新订阅的引用。
我们需要维护它,以便正确控制该订阅的生命周期。
让我用一个小例子向您解释原因
假设您有一个 MVVM 应用程序,该应用程序使用服务层进行网络调用。一个服务在整个应用程序中用作单例。
视图模型具有对服务的引用,并订阅此服务的 observable 属性。订阅闭包现在保存在服务上的 observable 属性中。
如果视图模型被释放(例如,由于关闭了视图控制器),而 observable 属性没有以某种方式注意到,则订阅闭包将继续存在。
作为一种解决方法,我们将订阅返回的可释放对象存储在视图模型上。在释放可释放对象时,它会自动通知 observable 属性删除引用的订阅闭包。
如果您只使用单个订阅者,则可以将返回的 Disposable
存储到变量中
// MARK: - Using `subscribe(_:)`
let disposable = formattedTime.subscribe { [weak self] newFormattedTime, oldFormattedTime in
self?.timeLabel.text = newFormattedTime
}
// MARK: - Using a `bind(to:on:)`
let disposable = dateTimeViewModel
.formattedTime
.bind(to: \.text, on: timeLabel)
如果您有多个观察者,则可以将所有返回的 Disposable
存储在 Disposable
数组中。(为了匹配 RxSwift 的语法,此 pod 包含一个名为 DisposeBag
的类型别名,它是 Disposable
的数组)。
var disposeBag = DisposeBag()
// MARK: - Using `subscribe(_:)`
formattedTime.subscribe { [weak self] newFormattedTime, oldFormattedTime in
self?.timeLabel.text = newFormattedTime
}.disposed(by: &disposeBag)
formattedDate.subscribe { [weak self] newFormattedDate, oldFormattedDate in
self?.dateLabel.text = newFormattedDate
}.disposed(by: &disposeBag)
// MARK: - Using a `bind(to:on:)`
formattedTime
.bind(to: \.text, on: timeLabel)
.disposed(by: &disposeBag)
formattedDate
.bind(to: \.text, on: dateLabel)
.disposed(by: &disposeBag)
DisposeBag
正如它所说的那样,是一个可释放对象的包(或数组)。
如果您创建一个其底层类型符合 Equatable
的 Observable,则可以使用特定过滤器订阅更改。因此,此 pod 包含方法
typealias Filter = (NewValue, OldValue) -> Bool
func subscribe(filter: @escaping Filter, observer: @escaping Observer) -> Disposable {}
使用此方法,只有在相应的过滤器匹配(返回 true
)时,才会通知观察者。
此 pod 附带一个预定义的过滤器方法,称为 subscribeDistinct
。使用此方法订阅 observable 只会在新值与旧值不同时通知观察者。这对于防止不必要的 UI 更新很有用。
通过扩展 Observable
,您可以随意添加更多过滤器,如下所示
extension Observable where T: Equatable {}
您可以通过访问属性 value
来获取 Observable
的当前值。但是,最好始终订阅给定的 observable!此快捷方式只应在测试期间使用。
XCTAssertEqual(viewModel.formattedTime.value, "4:20")
使用给定的方法,您的视图模型可能如下所示
final class ViewModel {
// MARK: - Public properties
/// The current date and time as a formatted string (**immutable**).
var formattedDate: Observable<String> {
formattedDateSubject
}
// MARK: - Private properties
/// The current date and time as a formatted string (**mutable**).
private let formattedDateSubject: Variable<String> = Variable("\(Date())")
private var timer: Timer?
// MARK: - Instance Lifecycle
init() {
// Update variable with current date and time every second.
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] _ in
self?.formattedDateSubject.value = "\(Date())"
}
}
您的视图控制器如下所示
final class ViewController: UIViewController {
// MARK: - Outlets
@IBOutlet private var dateLabel: UILabel!
// MARK: - Private properties
private let viewModel = ViewModel()
/// The dispose bag for this view controller. On it's deallocation, it removes the
/// subscription-closures from the corresponding observable-properties.
private var disposeBag = DisposeBag()
// MARK: - Public methods
override func viewDidLoad() {
super.viewDidLoad()
viewModel
.formattedDate
.bind(to: \.text, on: dateLabel)
.disposed(by: &disposeBag)
}
也欢迎您查看示例应用程序,以便更好地了解这种方法 🙂
Felix Mau (me(@)felix.hamburg)
LightweightObservable 在 MIT 许可证下可用。有关更多信息,请参见 LICENSE 文件。