轻量级可观察对象 2.2.2

LightweightObservable 2.2.2

Felix M.Felix Mau 维护。



  • Felix Mau

Header

Swift Version CI Status Code Coverage Version License Platform

特性

轻量级可观察对象是一个简单的可观察序列实现,您可以对它进行订阅。该框架旨在设计得尽可能简单且方便。整个代码只有大约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 协议🎉

这使得从 LightweightObservableCombine 的过渡更加容易,因为您可以使用 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.swiftdependencies 值一样简单。

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

如何使用

框架提供了三个类 ObservablePublishSubjectVariable

  • Observable:一个可观察的序列,您可以订阅它,但不能更改基本值(不可变)。这有助于避免对内部 API 的副作用。
  • PublishSubjectObservable 的子类,最初为空,仅对订阅者发出新元素(可变)。
  • VariableObservable 的子类,最初具有初始值,并将它或最新的元素回放给新订阅者(可变)。

– 创建和更新 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")

– 创建可观察对象

直接初始化可观察对象是不可能的,因为这会导致一个永远不会变化的序列。相反,你需要将一个 PublishSubjectVariable 转换为一个 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 文件。