最近、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
プロパティについて見てみると、
-
Variable<String>
型であるtitle
を、 -
.asObservable()
でObservable<String>
に変換して、 -
.bindTo()
でUILabel
の.rx_text
プロパティに単方向バインディングし -
.addDisposableTo()
でメモリ管理をしています。
いわゆる「Observable<T>
とObserver<T>
は接続可能である」というRxの特徴を端的に表したコードかと思います。
しかし、今回はプロパティが2つだけですが、これが増えてくると一気に可読性が悪くなる、というよりも ボイラープレートなコードが多すぎる ような気がしてきます。
コンセプト
map
やfilter
などのオペレーターを途中に挟んで加工することもあると思うので一概には言えませんが、例えばコンセプトとして以下のように書けたらスッキリするのではないでしょうか?
viewModel.title >>= articleTitleLabel.rx_text // Title
viewModel.content >>= articleContentLabel.rx_text // Content
実際にはこのように書くことは出来ないのですが、このコードは本当にやりたいことの本質、つまり「viewModel.title
とarticleTitleLabel.rx_text
を単純にバインドしたい」だけを過不足無く表現できています。
(>>=
は、Haskellにおけるバインドなのですが、この演算子にこだわる理由はありません)
実装
コンセプトは良いと思うのですが、残念ながらSwiftでは前述したようなコードを書くことが(たぶん)出来ません。
>>=
を演算子としてオーバーライドすることは出来ますが、メモリ管理用のdisposeBag
をaddDisposableTo()
に渡すためには、クラス内のインスタンスメソッドとして宣言する必要がありますが、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を勉強し始めて「同じような疑問・不満を持つ人もいるかも?」と思って記事にしてみましたが、もし良いやり方をご存知の方がいましたら是非教えて下さい。