如何在 属性 上过滤 Observable?

How do I filter a Observable on a property that is an Observable?

问题是我有一个协议,其中包含我想要在可用时对其进行排序的属性。因为我正在努力让一切变得超级反应。

protocol DeviceConnectionProtocol {
...
    var id : Observable<String> { get }
...
}

我现在的情况是,无论 url/name/etc 是否更改,我都想找到我连接的最后一个设备。

  class DeviceFinder {

    let rx_DeviceList = Variable([DeviceConnectionProtocol]())
    let disposeBag = DisposeBag()

    init() {
        SMOIPConnection.FindDevices().subscribe(onNext : { smoip in
            self.rx_DeviceList.value.append(smoip)
        }).addDisposableTo(disposeBag)

        MockDevice.FindDevices().subscribe(onNext : { mock in
            self.rx_DeviceList.value.append(mock)
        }).addDisposableTo(disposeBag)

    }

}

...

这是我目前的排序功能。但它不可行,因为 device.id.map returns 一个 Observable 而不是过滤操作所需的 Bool

struct LastConnectedDevice {

    private static let lastConnectedKeyForID = "lastConnected"

    static func get() -> Observable<DeviceConnectionProtocol>{
    let lastID = UserDefaults.standard.string(forKey: lastConnectedKeyForID)
       return DeviceFinder().rx_DeviceList.asObservable().flatMap{list in
            return Observable.from(list)
            }.filter { (device : DeviceConnectionProtocol) -> Bool in
                return device.id.map{ id in
                    return id == lastID
                }
        }
    }
}

诊断

根据我对您问题的理解,您在执行过滤操作时遇到困难,因为该属性是 Observable<Int> 而不是 Int。这意味着您不能仅使用相等运算符检查 ID,因为您 need to get out of the Rx monad.

继续使用 Rx Monad

更优雅的 FRP 解决方案:

  1. 定义您的设备可观察对象。
  2. 定义可观察的设备 ID。
  3. Zip他们在一起。
  4. Filter by checking 最后一个设备 ID。
  5. Return 到您想要的数据类型 (DeviceConnectionProtocol)。
  6. 使用最后一个设备执行必要的代码。

这是 RxSwift 代码。我的一些 class 定义可以在下面的代码块中看到。

// Observable<DeviceConnectionProtocol>
let devices = rx_DeviceList
    .asObservable()
    .flatMap { array in Observable.from(array) } 

// Observable<Int>
let deviceIDs = devices
    .flatMap { device in device.id } 

Observable.zip(devices, deviceIDs) { [=10=] } 

    // data type of (DeviceConnectionProtocol, Int)
    .filter { [=10=].1 == lastID }
    .map { [=10=].0 }
    .subscribe(onNext: lastDeviceSelected)

Monad 只是一种表达范式、处理方式或思维方式的方式。你总是可以退出 RxMonad,这意味着离开数据流并返回命令式代码。这是程序员比较习惯的。


退出 Rx Monad 的简单方法

这是一个(有点混乱的)解决方案。 TL;DR 转到 如何同步退出 RxMonad 部分。

import RxSwift

protocol DeviceConnectionProtocol {
    var id : Observable<String> { get }
}


class Person : DeviceConnectionProtocol {
    var myName: String! = nil

    init(name: String) {
        self.myName = name
    }

    var id: Observable<String> {
        return Observable.create { [unowned self] obx in
            obx.onNext(self.myName)
            return Disposables.create()
        }
    }
}
let rx_DeviceList = Variable([DeviceConnectionProtocol]())
let disposeBag = DisposeBag()

let lastID = "Hillary"

func lastDeviceSelected(device: DeviceConnectionProtocol) {
    if let person = device as? Person {
        print(person.myName + " was found!")
    }
}

rx_DeviceList
    .asObservable()
    .flatMap { array in Observable.from(array) }
    .filter{ (device: DeviceConnectionProtocol) -> Bool in

        // How to exit the RxMonad synchronously

        // Have a result variable
        var currentID = ""

        // Subscribe on the observable on the default schedulers (main thread) and assign result
        device.id.subscribe(onNext: { (id: String) in currentID = id })

        // Return result
        return lastID == currentID
    }
    .subscribe(onNext: lastDeviceSelected)

rx_DeviceList.value = [Person(name: "Donald"), Person(name: "Goofy"), Person(name: "Hillary")]

结果:

Hillary was found!

请注意,结束的序列(例如 [1,2,3,4,5])可以使用 takeLast 运算符,但由于某些原因,您的情况不同并且它不起作用。

参考和注释

  1. 如果你想要在 Rx Playground 上运行的完整代码,这里有 Github Gist

  2. RxMarbles 上可视化的 zip 运算符。

  3. zip运算符documentation.

  4. filter 运算符在 RxMarbles 上可视化。

  5. filter运算符documentation.

  6. 这是一个Swift 3的解决方案。