LoginSignup
1
3

More than 5 years have passed since last update.

RxSwiftのSamplesを読み解く <SimpleTableViewExampleViewController編>

Posted at

モチベーション

ソースを解読していく中で、理解を深めるために、また忘れないように作業内容をまとめております。
完全な自分メモです。

TableViewの表示がすごくシンプル

TableViewの表示に必要な処理これだけなんですね。

TableViewの表示設定部分
items
  .bindTo(tableView.rx.items(cellIdentifier: "Cell", cellType: UITableViewCell.self)) { (row, element, cell) in
       cell.textLabel?.text = "\(element) @ row \(row)"
  }
.disposed(by: disposeBag)

引数は3つ?のようですね。
- cellIdentifier:Identifier used to dequeue cells セルを一意にする識別子
- cellType:どんなセルのタイプを使うか
- Genericsパラメータ: Trail Closureのような形をしていますね。(中のソースを見たらTrail Closureではありませんでした)

Genericsパラメータ
{ (row, element, cell) in
       cell.textLabel?.text = "\(element) @ row \(row)"
}

どのような仕組みでTableViewを表示しているか深掘りしてみる

さて、tableView.rx.itemsがどんな形をしているのか覗いてみることにします。

public func items<S: Sequence, Cell: UITableViewCell, O : ObservableType>
        (cellIdentifier: String, cellType: Cell.Type = Cell.self)
        -> (_ source: O)
        -> (_ configureCell: @escaping (Int, S.Iterator.Element, Cell) -> Void)
        -> Disposable
        where O.E == S {
        return { source in
            return { configureCell in
                let dataSource = RxTableViewReactiveArrayDataSourceSequenceWrapper<S> { (tv, i, item) in
                    let indexPath = IndexPath(item: i, section: 0)
                    let cell = tv.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as! Cell
                    configureCell(i, item, cell)
                    return cell
                }
                return self.items(dataSource: dataSource)(source)
            }
        }
    }

ぱっと見で複雑ですね。

気を取り直して、渡されるパラメータを整理してみます。

(cellIdentifier: String, cellType: Cell.Type = Cell.self)

余談ですが、DefaultCell(ほとんどカスタムCellを使うと思いますのでムダ知識)を使う場合、
CellはUITableViewCellを継承していて、Cell.selfをデフォルト引数として渡しているので
cellType引数は省略できます。

DefaultCellを使う場合はCellType引数を省略可能
items
  .bindTo(tableView.rx.items(cellIdentifier: "Cell")) { (row, element, cell) in
     cell.textLabel?.text = "\(element) @ row \(row)"
  }

こんな感じにできます。

まずは、関数を読み解くと

func items(引数) -> (戻り値){

}

の形がシンプルな関数の形になるのでそれを念頭に置くと

戻り値がClosure
func items(cellIdentifier: String, cellType: Cell.Type = Cell.self) -> (戻り値)

// 戻り値がこの形のClosureになっていると読み取れます。
(_ source: O)
  -> (_ configureCell: @escaping (Int, S.Iterator.Element, Cell) -> Void)
  -> Disposable

どうやらTrail Closuredではなく、Closure式が戻り値として返却されているようです。

気になるのは、source(ObservableType)がどこから渡ってくるのか、
bindToすることにより、Observable Sequenceが渡されてくるようです。
reduxでのmiddle wareからdispatchが渡されるみたいなものだとイメージしています。
(こちらは、深掘りできていません。)

さて、コアな部分の

return { configureCell in
    let dataSource = RxTableViewReactiveArrayDataSourceSequenceWrapper<S> { (tv, i, item) in
      let indexPath = IndexPath(item: i, section: 0)
      let cell = tv.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as! Cell
      configureCell(i, item, cell)
      return cell
    }
    return self.items(dataSource: dataSource)(source)
}

closure式で、さらに引数にconfigureCellを受け取りDisposableを返却しています。
swiftのClosureは戻り値を省略することができるので、in句の前に戻り値が記述ありませんが、
Disposableを返却します。

宣言も見ると以下のようになっています。

(_ configureCell: @escaping (Int, S.Iterator.Element, Cell) -> Void) -> Disposable

configureCellクロージャーは、以下のようなClosure式がController側で記述していますし、
名前からもCellの見た目(ここではテキスト)を設定している部分ですね。

{ (row, element, cell) in
   cell.textLabel?.text = "\(element) @ row \(row)"
}
TableViewCell作成時の見慣れた形になっています。
let indexPath = IndexPath(item: i, section: 0)
let cell = tv.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as! Cell
configureCell(i, item, cell) // ここがClosureとして渡される部分です。
return cell

当たり前だと思いますが、cellの形を自由に外から決められるようにしたいから、
closure式を渡す形になっているわけですね。納得です。

さてCellの形を決めているのは理解できましたが、DataSourceを作成する部分をどのように作っているか深掘りしてみたいと思います。

さらに深掘り

let dataSource = RxTableViewReactiveArrayDataSourceSequenceWrapper<S> { (tv, i, item) in
      (処理)
      return cell
    }

つまり、RxTableViewReactiveArrayDataSourceSequenceWrapperのinstanceを作成して、dataSourceという形にしています。中のソースも見ましたが、このDataSourceはRx TableViewのDataSourceであり、通常のTableViewのDataSourceとは異なります。

少しRxTableViewReactiveArrayDataSourceSequenceWrapperをソースを見てみると、

RxTableViewReactiveArrayDataSourceSequenceWrapper
  override init(cellFactory: @escaping CellFactory) {
        super.init(cellFactory: cellFactory)
    }
RxTableViewReactiveArrayDataSource
  typealias CellFactory = (UITableView, Int, Element) -> UITableViewCell
  let cellFactory: CellFactory  
  init(cellFactory: @escaping CellFactory) {
     self.cellFactory = cellFactory
  }

なるほど、initialize処理が走ると、CellFactoryの上部で記述した、closure式を内部で保持するようにしているわけですね。このClosureは先にCellの形を決めている部分です。

さて、Cell表示に欠かすことのできないcellForRowAtメソッドもどきは、
以下のように保持されたClosure式をCallしておりました。

cellForRowAt
override func _tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        return cellFactory(tableView, indexPath.item, itemModels![indexPath.row])
    }

このcellForRowAtはどこから実行されるんだろうと調べて見ましたら、
RxTableViewDataSourceProxyというクラスがCallしていました。
ちょっとだけ触れると、

controllerにcellForRowAtメソッドを書く場合
tableView.dataSouce = self

単純にControllerからTableView DataSourceを操作する場合は、このように記述すると思いますが、
今回は、RxTableViewDataSourceProxyがTableView DataSourceを操作しておりました。

RxTableViewDataSourceProxy
public class RxTableViewDataSourceProxy
    : DelegateProxy
    , UITableViewDataSource   // ←このProxyがDataSourceをdelegateしているようです。
    , DelegateProxyType {
    (省略)

    // UITableViewDataSourceのcellForRowAtを実行
    public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        // ここで上部で宣言した、RxTableViewReactiveArrayDataSourceのcellForRowAtメソッドが実行されています。
        return (_requiredMethodsDataSource ?? tableViewDataSourceNotSet).tableView(tableView, cellForRowAt: indexPath)
    }

    (省略)
// https://github.com/ReactiveX/RxSwift/issues/907
    private func refreshTableViewDataSource() {
        if self.tableView?.dataSource === self {
            if _requiredMethodsDataSource != nil && _requiredMethodsDataSource !== tableViewDataSourceNotSet {
                self.tableView?.dataSource = self
            }
            else {
                self.tableView?.dataSource = nil
            }
        }
    }

最後にDataSourceを眺めてみることにします。雰囲気理解できました。
observedEventについては深掘りできておりません。

class RxTableViewReactiveArrayDataSourceSequenceWrapper<S: Sequence>
    : RxTableViewReactiveArrayDataSource<S.Iterator.Element>
    , RxTableViewDataSourceType {
    typealias Element = S

    override init(cellFactory: @escaping CellFactory) {
        super.init(cellFactory: cellFactory)
    }

    func tableView(_ tableView: UITableView, observedEvent: Event<S>) {
        UIBindingObserver(UIElement: self) { tableViewDataSource, sectionModels in
            let sections = Array(sectionModels)
            tableViewDataSource.tableView(tableView, observedElements: sections)
        }.on(observedEvent)
    }
}

// Please take a look at `DelegateProxyType.swift`
class RxTableViewReactiveArrayDataSource<Element>
    : _RxTableViewReactiveArrayDataSource
    , SectionedViewDataSourceType {
    typealias CellFactory = (UITableView, Int, Element) -> UITableViewCell

    var itemModels: [Element]? = nil

    func modelAtIndex(_ index: Int) -> Element? {
        return itemModels?[index]
    }

    func model(at indexPath: IndexPath) throws -> Any {
        precondition(indexPath.section == 0)
        guard let item = itemModels?[indexPath.item] else {
            throw RxCocoaError.itemsNotYetBound(object: self)
        }
        return item
    }

    let cellFactory: CellFactory

    init(cellFactory: @escaping CellFactory) {
        self.cellFactory = cellFactory
    }

    override func _tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return itemModels?.count ?? 0
    }

    override func _tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        return cellFactory(tableView, indexPath.item, itemModels![indexPath.row])
    }

    // reactive

    func tableView(_ tableView: UITableView, observedElements: [Element]) {
        self.itemModels = observedElements

        tableView.reloadData()
    }
}

さて、Disposedの戻り値を返却している、関数は以下のものですね。

return { configureCell in
    let dataSource = (dataSource作成処理)
    return self.items(dataSource: dataSource)(source)
}

少し中を覗いてみたいと思います。

UITableView+Rx
  public func items<
            DataSource: RxTableViewDataSourceType & UITableViewDataSource,
            O: ObservableType>
        (dataSource: DataSource)
        -> (_ source: O)
        -> Disposable
        where DataSource.Element == O.E {
        return { source in
            // This is called for sideeffects only, and to make sure delegate proxy is in place when
            // data source is being bound.
            // This is needed because theoretically the data source subscription itself might
            // call `self.rx.delegate`. If that happens, it might cause weird side effects since
            // setting data source will set delegate, and UITableView might get into a weird state.
            // Therefore it's better to set delegate proxy first, just to be sure.
            _ = self.delegate
            // Strong reference is needed because data source is in use until result subscription is disposed
            return source.subscribeProxyDataSource(ofObject: self.base, 
                   dataSource: dataSource, 
                   retainDataSource: true) 
            { [weak tableView = self.base] (_: RxTableViewDataSourceProxy, event) -> Void in
                guard let tableView = tableView else {
                    return
                }
                dataSource.tableView(tableView, observedEvent: event)
            }
        }
    }

まずは、関数を読み解くと

func items(引数) -> (戻り値){

}

の形がシンプルな関数の形になるのでそれを念頭に置くと

引数
(dataSource: DataSource)
戻り値
(_ source: O) -> Disposable

戻り値がClosureとなっているわけですね。
sourceは上でもでてきた、Observable Sequenceですね。

メイン処理部分
// sourceはObservableSequenceを引数に受け取っている
return { source in
     _ = self.delegate
     // self.base = tableView
     return source.subscribeProxyDataSource(ofObject: self.base, 
          dataSource: dataSource, 
          retainDataSource: true) 
     { [weak tableView = self.base] (_: RxTableViewDataSourceProxy, event) -> Void in
         guard let tableView = tableView else {
            return
         }
         dataSource.tableView(tableView, observedEvent: event)
     }
}

このsubscribeProxyDataSource以降は難解で詳しく追えていないのですが、
delegateを結びついているところだけ

source.subscribeProxyDataSource
extension ObservableType {
            func subscribeProxyDataSource<P: DelegateProxyType>(ofObject object: UIView, dataSource: AnyObject, retainDataSource: Bool, binding: @escaping (P, Event<E>) -> Void)
                -> Disposable {
                let proxy = P.proxyForObject(object)  // ここでtableView.dataSource.delegateと結びつけています
(省略

P.proxyForObject(object)からrefreshTableViewDataSource()が呼び出されています。

RxTableViewDataSourceProxy
private func refreshTableViewDataSource() {
        if self.tableView?.dataSource === self {
            if _requiredMethodsDataSource != nil && _requiredMethodsDataSource !== tableViewDataSourceNotSet {
                self.tableView?.dataSource = self
            }
            else {
                self.tableView?.dataSource = nil
            }
        }
    }

整理すると、

iOSのライフサイクルの中で、tableView DataSourceのcellForRowAtが実行されると、RxTableViewDataSourceProxyがTableViewのDataSourceとBindされているので、

RxTableViewReactiveArrayDataSourceSequenceWrapper
override func _tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        return cellFactory(tableView, indexPath.item, itemModels![indexPath.row])
    }

が実行される。

また、cellFactoryはgenericsで、下記のものが実行される。

{ (tv, i, item) in
                    let indexPath = IndexPath(item: i, section: 0)
                    let cell = tv.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as! Cell
                    configureCell(i, item, cell)
                    return cell
                }

GenericsでProtocol制約を複数したいときは&でつなげる

GenericsのProtocol制約複数
DataSource: RxTableViewDataSourceType & UITableViewDataSource

こういうことが出来るわけですね。

1
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
3