Gunosy Tech Blog

Gunosy Tech Blogは株式会社Gunosyのエンジニアが知見を共有する技術ブログです。

RxSwiftにおける孫からの祖父母孝行

こんにちは

こちらはGunosy Advent Calendar 2018、6日目の記事です。

メディア事業本部新規事業開発室のyutanimです。 現在業務にてiOSを書いているので、その周辺のことを書いて行こうかと思います。  

RxSwiftの孫Viewからのイベント受け取り

今回書くのは、RxSwift において孫Viewからイベントを受け取る実装についてです。 状態やイベントを伝播するのに便利なRxSwiftですが、ネストしたViewからイベントを受け取るのは少し面倒です。 そのような状況でどのように実装手法があるのかご紹介させて頂ければと思います。

 

実装パターン

今回は下記のようにUITableViewCellの中にFollowButtonがある状況で考えてみます

f:id:yutanim:20181204174245p:plain:w300
イメージ画面

※コードの例は必要最低限の箇所のみ抜き出しています。
※本来イベントはVCでsubscribeせず、vm等で処理を行うのが正しいですが、今回は簡略的にVCで書いています。

1. Singletonでささっと実装

SingletonでFollowButtonManagerのようなものを作り、 それに管理してもらうやり方です。 こちらはコード量こそは少なくなりますが、 今ケースのように、多数の状態を管理するようなものには向いていないため、 あまりおすすめすることはできません。 皆さんが実装される際には上記の2つで行うことをおすすめします。

class TableViewFollowManager {
    static let shared = TableViewFollowManager()
    let followButtonTapped: PublishSubject<()> = .init()
}

final class TableViewCell: UITableViewCell {
    @IBOutlet weak var followButton: UIButton!
    
    override func awakeFromNib() {
        super.awakeFromNib()
        followButton.rx.tap
            .asObservable()
            .subscribe(TableViewFollowManager.shared.followButtonTapped)
            .disposed(by: disposeBag)
    }
    
}

class ViewController: UIViewController {
    func setupTableView() {
        TableViewFollowManager.shared.followButtonTapped.subscribe(onNext: { _ in
            //何かやる
        }).disposed(by: disposeBag)
    }
}
  • メリット

    • コードの記述量が少ない
  • デメリット

    • グローバルに公開されているので状態の変化を追いにくい
    • dispose周りの見通しが悪く、処理を行うのが難しいため、拡張する際にボトルネックになりやすい

2. Delegateに任せる

よく見るDelegateによって伝播するやり方です。 このやり方だと見通しは良く、かつ孫Viewが多数のボタンなどを持ってる際に Delegateでまとめられることができます。 ただ、1つや2つのボタンの際に書くのは若干面倒な気がします。
※なおprepareForReuseでdisposeBagを入れ替えないとdisposeされません。

protocol ExampleCellDelegate: class {
    func didFollowButtonTapped()
}

class ViewController: UIViewController {    
   private let didTappedButton: PublishSubject<()> = .init()
   func setupTableView() { 
        item.drive(
            tableView
                .rx
                .items(cellIdentifier: "TableViewCell", cellType: TableViewCell.self)) { [unowned self] _, _ , cell in
                cell.bind(delegate: self)
            }.disposed(by: disposeBag)
        didTappedButton.asObservable().subscribe(onNext: { _ in
            //処理を書く
        }).disposed(by: disposeBag)
    }
}

extension ViewController: ExampleCellDelegate {
    func didFollowButtonTapped() {
        didTappedButton.onNext(())
    }    
}

final class TableViewCell: UITableViewCell {   
    override func prepareForReuse() {
        super.prepareForReuse()
        self.disposeBag = DisposeBag()
    }    
 
    func bind(delegate: ExampleCellDelegate) {
        followButton.rx.tap.asObservable().subscribe(onNext: {
            delegate.didFollowButtonTapped()
        }).disposed(by: disposeBag)
    }
}
  • メリット

    • 処理の見通しがよい
    • 多数のボタン等などがある際にまとめやすい
  • デメリット

    • 1個や2個のために書くのは少し面倒臭い

3. ReactiveのExtensionで作る

Reactiveを拡張し、Propertyを外部公開することによって 親Viewでsubscribeするやり方です。 このやり方だと、見通しも良く、簡単に書くことが出来ます。 多数のボタンを置いた際に親Viewが多数のオブジェクトをSubscribeすることになりますが、 これがベストケースだと考えます。
※こちらもprepareForReuseでdisposeBagを入れ替えないとdisposeされません。

final class TableViewCell: UITableViewCell {

    override func prepareForReuse() {
        super.prepareForReuse()
        self.disposeBag = DisposeBag()
    }    
}

extension Reactive where Base: TableViewCell {
    var followButtonDidTapped: ControlEvent<()> {
        return base.followButton.rx.tap
    }
}

class ViewController: UIViewController {
   func setupTableView() { 
        item.drive(
            tableView
                .rx
                .items(cellIdentifier: "TableViewCell", cellType: TableViewCell.self)) { [unowned self] _, _ , cell in
                cell.rx.followButtonDidTapped
                    .asObservable()
                    .subscribe(self.didFollowButtonTapped)
                    .disposed(by: cell.disposeBag)
            }.disposed(by: disposeBag)
        didFollowButtonTapped.asObservable().subscribe(onNext: { _ in
            //処理を書く
        }).disposed(by: disposeBag)
    }
}
  • メリット

    • 処理の見通しがよい
    • 割と簡潔に書ける
  • デメリット

    • ボタンなどControlEventが増えてきた際に少し面倒くさい

おわりに

iOSのネストしたViewからのイベント受け取りについて書きました。 執筆中に、「孫」という言葉で、 小学生の頃に祖父母からもらったお年玉を母親が預かってくれて その後行方不明になってることを思い出しました。 引き続きGunosy Advent Calendar 2018 をお楽しみくださいね!

 [追記] ResponderChainを使ったパターンはなぜないの?という意見を社内外から多く頂きましたが、 そちらは今後別途書く予定があったため、今回は割愛させていただきました🙏