Help us understand the problem. What is going on with this article?

[RxSwift] Variable -> Observer への単方向バインディングをシンプルに書く

More than 3 years have passed since last update.

最近、RxSwiftを始めました。(ので、まだ初心者です)

いろいろとコードを書きながら勉強中なのですが、VariableからObserverへ単方向バインディングするコードが少し長いなぁ、と感じたので改善策を考えてみました。

基本な書き方

以下のようなViewModelがあって。

class ArticleViewModel {
    var title = Variable<String>("Titleですよ")
    var content = Variable<String>("Contentですよ")
}

以下のようなViewControllerがあるとします。

class ArticleViewController: UIViewController {

    @IBOutlet weak var articleTitleLabel: UILabel!
    @IBOutlet weak var articleContentLabel: UILabel!

    let viewModel = ArticleViewModel()
    let disposeBag = DisposeBag()

    override func viewDidLoad() {

        // Title
        viewModel.title
            .asObservable()
            .bindTo(articleTitleLabel.rx_text)
            .addDisposableTo(disposeBag)

        // Content
        viewModel.content
            .asObservable()
            .bindTo(articleContentLabel.rx_text)
            .addDisposableTo(disposeBag)
    }
}

titleプロパティについて見てみると、

  1. Variable<String>型であるtitleを、
  2. .asObservable()Observable<String>に変換して、
  3. .bindTo()UILabel.rx_textプロパティに単方向バインディングし
  4. .addDisposableTo()でメモリ管理をしています。

いわゆる「Observable<T>Observer<T>は接続可能である」というRxの特徴を端的に表したコードかと思います。

しかし、今回はプロパティが2つだけですが、これが増えてくると一気に可読性が悪くなる、というよりも ボイラープレートなコードが多すぎる ような気がしてきます。

コンセプト

mapfilterなどのオペレーターを途中に挟んで加工することもあると思うので一概には言えませんが、例えばコンセプトとして以下のように書けたらスッキリするのではないでしょうか?

viewModel.title   >>= articleTitleLabel.rx_text   // Title
viewModel.content >>= articleContentLabel.rx_text // Content

実際にはこのように書くことは出来ないのですが、このコードは本当にやりたいことの本質、つまり「viewModel.titlearticleTitleLabel.rx_textを単純にバインドしたい」だけを過不足無く表現できています。

>>=は、Haskellにおけるバインドなのですが、この演算子にこだわる理由はありません)

実装

コンセプトは良いと思うのですが、残念ながらSwiftでは前述したようなコードを書くことが(たぶん)出来ません。

>>=を演算子としてオーバーライドすることは出来ますが、メモリ管理用のdisposeBagaddDisposableTo()に渡すためには、クラス内のインスタンスメソッドとして宣言する必要がありますが、Swiftでは演算子をクラス内に定義することは出来ません。

ということで、演算子は諦めてインスタンスメソッドで書いてみます。

func bind<T>(variable: Variable<T>, _ observer: AnyObserver<T>) {
    variable
        .asObservable()
        .bindTo(observer)
        .addDisposableTo(disposeBag)
}

すると以下のように書けるようになります。

bind(viewModel.title,   articleTitleLabel.rx_text)   // Text
bind(viewModel.content, articleContentLabel.rx_text) // Content

演算子ほどではありませんが、かなり短かくなりましたし、余計な視覚ノイズが少なくなってスッキリして可読性も良いように感じます。

共通化

ところで、さきほどのbind()メソッドは利用したいクラス毎に定義し直す必要があるのでしょうか?

まさか。

SwiftにはProtocol Extensionsという仕組みがあります。これで共通化してみます。

protocol RxManaged {
    var disposeBag: DisposeBag { get }
}

extension RxManaged {
    func bind<T>(variable: Variable<T>, _ observer: AnyObserver<T>) {
        variable
            .asObservable()
            .bindTo(observer)
            .addDisposableTo(disposeBag)
    }
}

最終的なコードは以下のようになりました。

class ArticleViewController: UIViewController, RxManaged {

    @IBOutlet weak var articleTitleLabel: UILabel!
    @IBOutlet weak var articleContentLabel: UILabel!

    let viewModel = ArticleViewModel()
    let disposeBag = DisposeBag()

    override func viewDidLoad() {
        bind(viewModel.title,   articleTitleLabel.rx_text)   // Text
        bind(viewModel.content, articleContentLabel.rx_text) // Content        
    }
}

最後に

この記事は特定の領域のみにしかフォーカスしていません。実際の問題はもっと複雑なケースが多いでしょう。

しかし、RxSwiftにから標準で提供された機能だけをそのまま利用するのではなく、共通コードを書くことでコードの見通しを良くすることが出来るのではないかと思いました。

というわけで、RxSwiftを勉強し始めて「同じような疑問・不満を持つ人もいるかも?」と思って記事にしてみましたが、もし良いやり方をご存知の方がいましたら是非教えて下さい。

YusukeHosonuma
iOSテスト本とか書きました。Qiitaでなくブログに書くことも多いです。 📕 iOSアプリ開発自動テストの教科書 / iOSテスト全書 💻 iOS / Swift / Haskell / Go / Java / Ruby 🎤 https://speakerdeck.com/yusukehosonuma
http://blog.penginmura.tech/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away