特性
轻量级可观察对象是一个简单的可观察序列实现,您可以对它进行订阅。该框架旨在设计得尽可能简单且方便。整个代码只有大约100行(不包括注释)。使用轻量级可观察对象,您可以在 MVVM 应用中轻松设置 UI 绑定,处理异步网络调用等等。
致谢
该代码受到了 roberthein/observable 的很大影响。然而,我需要一种语法上更接近 RxSwift 的东西,这也就是为什么我开发了这个代码,后来出于可重用性的原因,将其移动到了 CocoaPod。
迁移指南
如果您想从 1.x.x 版本进行更新,请查看 轻量级可观察对象 2.0 迁移指南
示例
要运行示例项目,请克隆仓库,并从示例目录中打开工作区。
需求
- Swift 5.5
- Xcode 13.2+
- iOS 9.0+
针对 iOS >= 13.0 的目标项目
如果您的最低版本要求等于或高于 iOS 13.0,我建议使用 Combine 而不是将 LightweightObservable
作为依赖项添加。
如果您依赖于在订阅闭包中具有当前值和先前值,请参阅此扩展:Combine+Pairwise.swift。
2.2
以来,一个 Observable
实例符合 Swift 的 Combine
中的 Publisher
协议🎉
更新:自版本 这使得从 LightweightObservable
到 Combine
的过渡更加容易,因为您可以使用 Combine
的功能,而无需将底层的 Observable
更改为 Publisher
。
使用 Combine
函数在 PublishSubject
实例上的示例代码
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"
速查表
轻量级 Observable |
Combine |
---|---|
发布者主题 |
透传主题 |
变量 |
当前值主题 |
此外,使用 Combine.Publisher
的属性 values
,您可以在异步序列中使用 Observable
for await value in observable.values {
// ...
}
集成
CocoaPods
CocoaPods 是 Cocoa 项目的依赖管理器。有关使用和安装说明,请访问他们的网站。要在 Xcode 项目中使用 CocoaPods 集成 Lightweight Observable,请将其指定在您的 Podfile
中
pod 'LightweightObservable', '~> 2.0'
Carthage
Carthage 是一个去中心化的依赖管理器,它会构建您的依赖并提供二进制框架。要在 Xcode 项目中使用 Carthage 集成 Lightweight Observable,请将其指定在您的 Cartfile
中
github "fxm90/LightweightObservable" ~> 2.0
运行 carthage update 以构建框架,并将构建好的 LightweightObservable.framework
拖入您的 Xcode 项目。
Swift Package Manager
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
:一个可观察的序列,您可以订阅它,但不能更改基本值(不可变)。这有助于避免对内部 API 的副作用。PublishSubject
:Observable
的子类,最初为空,仅对订阅者发出新元素(可变)。Variable
:Observable
的子类,最初具有初始值,并将它或最新的元素回放给新订阅者(可变)。
PublishSubject
– 创建和更新 PublishSubject
初始为空,仅对订阅者发出新元素。
let userLocationSubject = PublishSubject<CLLocation>()
// ...
userLocationSubject.update(receivedUserLocation)
– 创建和更新变量
变量以初始值开始,并为新订阅者回放该值或最新元素。
let formattedTimeSubject = Variable("4:20 PM")
// ...
formattedTimeSubject.value = "4:21 PM"
// or
formattedTimeSubject.update("4:21 PM")
– 创建可观察对象
直接初始化可观察对象是不可能的,因为这会导致一个永远不会变化的序列。相反,你需要将一个 PublishSubject
或 Variable
转换为一个 Observable。
var formattedTime: Observable<String> {
formattedTimeSubject
}
lazy var formattedTime: Observable<String> = formattedTimeSubject
– 订阅到变化
订阅者会在不同时间被通知,取决于相应可观察对象的子类。
PublishSubject
:开始时为空,仅对订阅者发射新元素。Variable
:以初始值开始,并发送给新的订阅者或最后一次元素。
– 基于闭包的订阅
声明
func subscribe(_ observer: @escaping Observer) -> Disposable
使用此方法通过闭包订阅可观察对象
formattedTime.subscribe { [weak self] newFormattedTime, oldFormattedTime in
self?.timeLabel.text = newFormattedTime
}
请注意,旧的值(《oldFormattedTime》)是底层类型的可选值,因为我们可能没有在订阅者的初始调用中获得此值。
重要:为了避免保留周期和/或崩溃,始终在需要 self
实例的观察者时使用 [weak self]
。
- 基于 KeyPath 的订阅
声明
func bind<Root: AnyObject>(to keyPath: ReferenceWritableKeyPath<Root, Value>, on object: Root) -> Disposable
还可以使用Swift的KeyPath功能将可观察的直接绑定到一个属性上。
formattedTime.bind(to: \.text, on: timeLabel)
Disposable
/ DisposeBag
)
内存管理(当你订阅一个Observable
时,该方法返回一个Disposable
,这基本上是新的订阅的引用。
我们需要保留它,以便正确控制该订阅的生命周期。
让我通过一个小例子解释为什么
想象一下有一个使用服务层进行网络调用的MVVM应用程序。在整个应用程序中使用服务作为单例。
视图模型有一个对服务的引用并订阅了这个服务的一个可观察属性。订阅闭包现在保存在服务的可观察属性中。
如果视图模型被销毁(例如,由于视图控制器被取消显示),如果没有注意到可观察属性,订阅闭包将继续存在。
作为解决方案,我们在视图模型上存储从订阅返回的
Disposable
。当销毁Disposable
时,它会自动通知可观察属性移除所引用的订阅闭包。
如果你只使用单个订阅者,可以将返回的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的语法,这个库包含一个名为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
值
观察如果你创建了一个其底层类型符合Equatable
的Observable,你可以使用特定的过滤器来订阅变化。因此,这个库包含了一个方法。
typealias Filter = (NewValue, OldValue) -> Bool
func subscribe(filter: @escaping Filter, observer: @escaping Observer) -> Disposable {}
使用这个方法,只有在相应过滤器匹配(返回true
)时观察者才会被通知。
该库提供了一个名为subscribeDistinct
的预定义过滤器方法。使用这个方法订阅Observable,只会当新值与旧值不同时通知观察者。这可以用来防止不必要的UI更新。
你可以通过扩展Observable
来添加更多过滤器,如下所示
extension Observable where T: Equatable {}
– 获取当前值同步
你可以通过访问属性 value
来获取 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 文件。