Header

Swift Version CI Status Code Coverage Version License Platform

特性

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 的项目

如果您的最低所需版本大于等于 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

CocoaPods 是 Cocoa 项目的依赖项管理器。有关用法和安装说明,请访问他们的网站。要使用 CocoaPods 将 Lightweight Observable 集成到您的 Xcode 项目中,请在您的 Podfile 中指定它

pod 'LightweightObservable', '~> 2.0'
Carthage

Carthage 是一个去中心化的依赖项管理器,可构建您的依赖项并为您提供二进制框架。要使用 Carthage 将 Lightweight Observable 集成到您的 Xcode 项目中,请在您的 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.swiftdependencies 值一样容易。

dependencies: [
    .package(url: "https://github.com/fxm90/LightweightObservable", from: "2.0.0")
]

如何使用

该框架提供了三个类 ObservablePublishSubjectVariable

– 创建和更新 PublishSubject

PublishSubject 一开始为空,并且仅向订阅者发送新元素。

let userLocationSubject = PublishSubject<CLLocation>()

// ...

userLocationSubject.update(receivedUserLocation)

– 创建和更新 Variable

Variable 以初始值开始,并将其或最新的元素重播给新的订阅者。

let formattedTimeSubject = Variable("4:20 PM")

// ...

formattedTimeSubject.value = "4:21 PM"
// or
formattedTimeSubject.update("4:21 PM")

– 创建 Observable

无法直接初始化 observable,因为这会导致序列永远不会改变。相反,您需要将 PublishSubjectVariable 强制转换为 Observable。

var formattedTime: Observable<String> {
    formattedTimeSubject
}
lazy var formattedTime: Observable<String> = formattedTimeSubject

– 订阅更改

订阅者将在不同的时间收到通知,具体取决于相应 observable 的子类

– 基于闭包的订阅

声明

func subscribe(_ observer: @escaping Observer) -> Disposable

使用此方法通过闭包订阅 observable

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 功能将 observable 直接绑定到属性

formattedTime.bind(to: \.text, on: timeLabel)

– 内存管理 (Disposable / DisposeBag)

当您订阅 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

如果您创建一个其底层类型符合 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 文件。