RxSwift で循環参照を起こさない書き方を考える
Binder の独自実装
RxSwift などでデータバインディングする場合、次のようにクロージャを渡すことがあります。
viewModel.text
.subscribe(onNext: { [weak label] text in
label?.text = text
})
.disposed(by: disposeBag)
循環参照を起こさないように weak
でオブジェクトをキャプチャする必要があります。
RxCocoa で提供されている Binder
を使用すると、循環参照を起こさずに記述できます。
Observable<String?>
を UILabel にバインディングする場合は、次のように記述します。
viewModel.text
.bind(to: label.rx.text)
.disposed(by: disposeBag)
rx.text
は RxCocoa で次のように実装されています。
extension Reactive where Base: UILabel {
public var text: Binder<String?> {
return Binder(self.base) { label, text in
label.text = text
}
}
}
上述の要領で Binder
を独自に実装することができます。
extension Reactive where Base: ViewController {
var text: Binder<String?> {
return Binder(base) { vc, text in
vc.label.text = text
}
}
}
このように Binder
を実装することで、次のようなメリットがあります。
-
weak
でオブジェクトをキャプチャするコードがなくなる - subscribe 部分からクロージャが消えてコードが簡潔に見えるようになる
Binder の独自実装による問題
しかし、次のような問題もあると思います。
- 循環参照を完全に防げていない
- 本質的ではない実装の量産
循環参照を完全に防げていない
次のような実装は、強い参照でキャプチャするので循環参照を引き起こします。
extension Reactive where Base: ViewController {
var text: Binder<String?> {
let vc = base
return Binder(base) { _, text in // 第一パラメータを使用していない
vc.setText(text) // vc を weak キャプチャしていない
}
}
}
Binder
はクロージャの第一パラメータ使用することで、循環参照を防ぐことができます。
第一パラメータを使用しないのであれば、 weak
キャプチャする必要があります。
結局、循環参照を防ぐためには、プログラマが実装時に注意を払う必要があるということになります。
本質的ではない実装の量産
Observable
から値を受け取りたいがために、都度 Binder を実装することは、本質的ではない処理を量産していることを意味しています。
Rx を使用していなければ、値を受け取るためのセッターやメソッドがあれば十分だからです。
RxSwift を利用したとたんに、 subscribe
や Binder
でクロージャを記述しなければならないことは、Rx を利用するメリットと同時にデメリットを授かっていると言えます。
循環参照を起こさない書き方を考える
上述の問題を冗長なコードと捉えて、処理を共通化してみます。
循環参照を防ぐための weak
キャプチャする処理を共通化して、セッターやメソッドだけ渡して subscribe
できるようにします。
ObservableType にメソッドを追加
RxSwift の ObservableType
に次のようなメソッドを追加します。
import RxSwift
import RxCocoa
public extension ObservableType {
// セッターでバインド
func bind<Object: AnyObject>(to object: Object, keyPath: ReferenceWritableKeyPath<Object, E>) -> Disposable {
return bind { [weak object] element in
object?[keyPath: keyPath] = element
}
}
// メソッドでバインド
func bind<Object: AnyObject>(to object: Object, selector: @escaping (Object) -> (E) -> Void) -> Disposable {
return bind { [weak object] element in
object.map(selector)?(element)
}
}
}
このような拡張をすると、subscribe
時にセッターやメソッドだけを渡すことができます。
循環参照も起きません。
次のようにして使います。
セッターでバインド
class ViewController: UIViewController {
....
var text: String?
....
override func viewDidLoad() {
super.viewDidLoad()
viewModel.text
.bind(to: self, keyPath: \.text)
.disposed(by: disposeBag)
}
メソッドでバインド
class ViewController: UIViewController {
....
func setText(_ text: String?) {
}
....
override func viewDidLoad() {
super.viewDidLoad()
viewModel.text
.bind(to: self, selector: ViewController.setText)
.disposed(by: disposeBag)
}
ViewController.setText
のように (クラス名).(インスタンスメソッド)
のような記述をした場合は、インスタンスメソッドではなく、カリー化された関数になります。
上述の例の setText
の場合は次のような型の関数です。
let foo: (ViewController) -> (String?) -> () = ViewController.setText
ViewController.setText
自体は特定の ViewController
オブジェクトとは無関係な関数です。
bind(to: ...
で渡している具体的なオブジェクトを内部の実装で weak キャプチャしているので循環参照は発生しません。
ReferenceWritableKeyPath
についても同様に、特定のオブジェクトとは無関係ですので循環参照を回避することができます。
Author And Source
この問題について(RxSwift で循環参照を起こさない書き方を考える), 我々は、より多くの情報をここで見つけました https://qiita.com/takehito-koshimizu/items/26e09241e9fb8d1e845f著者帰属:元の著者の情報は、元のURLに含まれています。著作権は原作者に属する。
Content is automatically searched and collected through network algorithms . If there is a violation . Please contact us . We will adjust (correct author information ,or delete content ) as soon as possible .