RxSwift4で廃止になった Variable のリプレイス


調べてみました😼

Deprecates Variable in favor of BehaviorRelay

4.0.0-rc.0より廃止

Deprecates Variable in favor of BehaviorRelay.

VariableBehaviorRelayに取って代わる形で、廃止になります🙋🍂

廃止の経緯

Issue #1501

  • 標準的なクロスプラットフォームのコンセプトではない
    • ReactiveXの思想にないということ・・・🤔?)
  • イベント管理において拡張性が低い
  • 命名が*Relayに対して一貫していない
  • Rx内の他のものと比べてみると、メモリの管理モデルに一貫性がない(dealloc時にCompleteされる)

とのこと。

ただしDeprecation warningsは出ない

  • Variableはかなり多用されている
  • マルチスレッドでのBehaviorRelay利用には、危険なパターンがある(後述👇)
  • どうやって廃止を進めるか明確にはまだ決まっていない

などの理由があるみたいです。

Variable & BehaviorRelay

Variable とは

Deprecated.swift#L170

  • BehaviorSubjectのラッパー
  • Errorは流れない
  • 変数が解放された時にCompleteが流れる
  • 値のアクセスはvalueプロパティのgetter/setterで行う
  • import RxSwiftで利用

RxSwift + MVVMアーキテクチャで非常に使いやすいAPIです。

BehaviorRelay とは

BehaviorRelay.swift

  • BehaviorSubjectのラッパー
  • ErrorCompleteは流れない
  • 値のアクセスはvalueプロパティとaccept(_ event: Element)で行う
  • import RxCocoaで利用

accept(_ event: Element)

BehaviorRelay.swift
// Accepts `event` and emits it to subscribers
public func accept(_ event: Element) {
    _subject.onNext(event)
}

valueプロパティ

BehaviorRelay.swift
/// Current value of behavior subject
public var value: Element {
    // this try! is ok because subject can't error out or be disposed
    return try! _subject.value()
}

Sample Codes

Variable

Playground
import RxSwift

let bag = DisposeBag()
let relay = Variable("🍎")

func bind() {
    relay.asObservable()
        .subscribe(onNext: { value in
            print(value)
        }).disposed(by: bag)
}

// Apples
bind()
relay.value = "🍏"

Output

🍎
🍏

BehaviorRelay

import RxSwiftは、DisposeBagMainScheduler利用のために行なっています。
実際にはMainSchedulerは指定しなくても良いですが、asDriver()利用に合わせて便宜的に書いています。

asDriver()利用

Playground
import RxSwift
import RxCocoa

let bag = DisposeBag()
let relay = BehaviorRelay(value: "🍎")

func bind() {
    relay.asDriver()
        .drive(onNext: { value in
            print(value)
        })
        .disposed(by: bag)
}

// Apples
bind()
relay.accept("🍏")

asObservable()利用

Playground
import RxSwift
import RxCocoa

let bag = DisposeBag()
let relay = BehaviorRelay(value: "🍎")

func bind() {
    relay.asObservable()
        .observeOn(MainScheduler.instance) 
        .subscribe(onNext: { value in
            print(value)
        })
        .disposed(by: bag)
}

// Apples
bind()
relay.accept("🍏")

Output

🍎
🍏

マルチスレッド + BehaviorRelay に注意

Variableでは、以下のような書き方で値を更新することができました。

Playground
let variable = Variable(["🍎"])
variable.value.append("🍏")

BehaviorRelayではvalueプロパティはReadOnlyのため、同じような書き方はできません。

Playground
let relay = BehaviorRelay(value: ["🍎"])
relay.value.append("🍏") // error: cannot use mutating member on immutable value: 'value' is a get-only property

代替案としては以下のようなパターンが考えられます。

let relay = BehaviorRelay(value: ["🍎"])
var copy = relay.value
copy.append("🍏")
relay.accept(copy)

ですが、これはマルチスレッドで行う場合、アンチパターンと言えるでしょう。

なぜならrelay.valueをリードした後、すぐに別スレッドでrelay.valueに新たな値がセットされる可能性があります。relay.accept(copy)の時に意図せずに上書きしてしまうことになります。


Thread A: var copy = relay.value     // relayの値は["🍎"]
Thread B: relay.accept(["🍏", "🍏"])  // relayの値は["🍏", "🍏"]
Thread A: copy.append("🍏") 
         relay.accept(copy)         // relayの値は["🍎", "🍏"]

こちらは、Issue #1501にも記載がありますが、この問題を解決する方法は今の所ないみたいです。

Variable の今後

Issue #1501

現状、以下のような想定をされているそうです🤔💡

  • typealias Variable = BehaviorRelayとし、解放時Completeされるのを廃止する
  • Deprecation warningsを追加する
  • RxSwiftからVariableをなくす
  • 次のメジャーアップデートでは、少なくともVariableはRxCocoaに移す

まとめ

RxCocoaをインポートする違和感

Issue #1501でも指摘されていますが・・・
BehaviorRelayが入っているRxCocoaには、他にもUI関連のコンポーネントが含まれています。単純なVariableの代用としてCleanArchitectureやMVVMなどのUIレイヤー以外でimport RxCocoaを行うのは、違和感があると思いました。余計なインポートを行なっているように感じるためです。

とはいえ、現状、この構成がすぐに覆ることはないと思います。

わたしたちにできること

私たちにできることは、

  • 早めにリプレイスについて考える(問題を早めに検知する)
  • タイミングを見極める
  • マルチスレッドでの操作などチームでルールを決める

などでしょうか。実際に良いルールなどあればぜひ共有いただきたいです🙋

VariableでできたことがBehaviorRelayではできなくなっている!などの場合は積極的にIssue出しをしましょう。
例えば、RxSwift4のリリース当初、Can't bind ObservableType to BehaviorRelayというIssueが上がっていましたが、こちらはすぐに追加対応されました。

ちなみにaddDisposableToも廃止になったので注意です。

Adds Disposable extension disposed(by:) equivalent to addDisposableTo that is meant to replace it in future 4.0 version.

以上です🎉🎉🎉
ありがとうございました😽

Thanks to