47
45

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Money ForwardAdvent Calendar 2015

Day 25

あの面倒で可読性の悪いcellForRowAtIndexPath内の分岐をスッキリ・サッパリ!Shoyuというライブラリを作りました

Last updated at Posted at 2015-12-22

iOSアプリには必須のUITableView。
幾つかのアプリ開発を経験し、UITableViewの実装をもっと簡潔に出来ないか考え続けてきました。
試行錯誤した結果、なかなかできの良いライブラリを作ることに成功しました。

yukiasai/Shoyu

UITableViewを普通に実装した時の問題点

UITableViewの実装は、UITableViewDelegateUITableViewDataSourceを実装するのが一般的です。
このデリゲートパターンはCocoaのフレームワークで多用されており、とっても優れたデザインパターンだと思っています。

ですが問題点もあります。
テーブルの構成がシンプルなうちはいいのですが、ある程度複雑になってきた時にデリゲートメソッドがとんでもないことになってしまうことってよくありますよね。
大抵の開発者たちがこのデリゲートメソッド内の分岐がやばくなっちゃう問題にぶち当たると思います。

iOSの風習に添い普通に書くと、大体こんな感じのコードになります。

func numberOfSectionsInTableView(tableView: UITableView) -> Int {
    // 全体でセクションが3個
    return 3
}

func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    switch section {
    case 0:
        // 1番目のセクションはローが5個
        return 5
    case 1:
        // 2番目のセクションはローが3個
        return 3
    case 2:
        // 3番目のセクションはローが3個
        return 3
    default:
        fatalError()
    }
}

func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    switch (indexPath.section, indexPath.row) {
    case (0, _):
        let cell = tableView.dequeueReusableCellWithIdentifier("MemberCell") as! MemberTableViewCell
        // MemberTableViewCellの設定を行う
        return cell
    case (1, _):
        let cell =  tableView.dequeueReusableCellWithIdentifier("GroupCell") as! GroupTableViewCell
        // GroupTableViewCellの設定を行う
        return cell
    case (2, _):
        let cell =  tableView.dequeueReusableCellWithIdentifier("PhotoCell") as! PhotoTableViewCell
        // PhotoTableViewCellの設定を行う
        return cell
    default:
        fatalError()
    }
}

func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
    switch (indexPath.section, indexPath.row) {
    case (0, _):
        // 1番目のセクションはローの高さが52
        return 52
    case (1, _):
        // 2番目のセクションはローの高さが52
        return 52
    case (2, _):
        // 3番目のセクションはローの高さが100
        return 100
    default:
        fatalError()
    }
}

func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
    switch (indexPath.section, indexPath.row) {
    case (0, _):
        // 1つ目のセクション内のローが選択された時の処理
        break
    case (1, _):
        // 2つ目のセクション内のローが選択された時の処理
        break
    case (2, _):
        // 3つ目のセクション内のローが選択された時の処理
        break
    default:
        fatalError()
    }
}

その他必要に応じて多数のメソッドを実装するのですが、大体これら全部にswitch文が必要だと思って良いです。

func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath)
func tableView(tableView: UITableView, willDisplayHeaderView view: UIView, forSection section: Int)
func tableView(tableView: UITableView, willDisplayFooterView view: UIView, forSection section: Int)
func tableView(tableView: UITableView, didEndDisplayingCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath)
func tableView(tableView: UITableView, didEndDisplayingHeaderView view: UIView, forSection section: Int)
func tableView(tableView: UITableView, didEndDisplayingFooterView view: UIView, forSection section: Int)
func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat
func tableView(tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat
func tableView(tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat
func tableView(tableView: UITableView, estimatedHeightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat
func tableView(tableView: UITableView, estimatedHeightForHeaderInSection section: Int) -> CGFloat
func tableView(tableView: UITableView, estimatedHeightForFooterInSection section: Int) -> CGFloat
func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView?
func tableView(tableView: UITableView, viewForFooterInSection section: Int) -> UIView?
// ...まだまだあるよ...

これの不便な点を以下に挙げてみます。

  • UITableViewの見た目とコードの見た目が乖離している
    • どのセクションにどのローが表示されるか追うのとっても大変
  • switch文を一箇所いじると全箇所いじる必要がある
    • 要素追加、削除するとバグが発生する可能性がとっても高い

その他、皆様いろいろと苦労されているとは思いますが、特に2つ目は嫌というほどハマってきたのではないでしょうか。

Shoyuで書くとどうなるか

さて、上記コードをShoyuを使ってスッキリ・サッパリしてみましょう。

tableView.source = Source()
    .createSection { section in
        section.createRows(5) { (_, row: Row<MemberTableViewCell>) in
            row.height = 52
            row.configureCell = { cell, _ in }
            row.didSelect = { _ in }
        }
    }
    .createSection { section in
        section.createRows(3) { (_, row: Row<GroupTableViewCell>) in
            row.height = 52
            row.configureCell = { cell, _ in }
            row.didSelect = { _ in }
        }
    }
    .createSection { section in
        section.createRows(3) { (_, row: Row<PhotoTableViewCell>) in
            row.height = 100
            row.configureCell = { cell, _ in }
            row.didSelect = { _ in }
        }
}

このコードを見て以下のように感じてくれていたら嬉しいです。

  • みじかっ!!!
    • switch文1つで全ての構造を表すようなイメージ
  • 見通し良っ!!!
    • UITableViewのツリー構造インデントで表現されている
  • switch文なっ!!!
    • switch地獄からの開放
  • その他利点
    • ローの型をジェネリクスで指定できるのでキャスト不要

ただいくつか欠点もありまして、、

  • 循環参照のことを意識しないとメモリリークする場合がある
  • セクションとローの数が多いとデータ構造を初期化するのに時間がかかる
    • O(n) : 10,000行で0.1sec
  • UITableViewDelegateUITableViewDataSourceの未対応のメソッドが結構ある
    • PullReqお待ちしております

Shoyuの利用シーン

設定画面

例えば、こんな悩みを抱えている人がいたとします。

「設定画面って静的ページっぽいけどやっぱり動的な部分って多くて、セルのタイプがたくさんあるし、条件によってはセルを非表示にさせたかったりするんですよね。。。」

おまかせあれ。
Rowはジェネリクスでセルのタイプを指定できますし、Row.heightForプロパティで0を返せばセルが非表示になります。

section.createRow { row: Row<TableViewCell> in     // ジェネリクスでセルのタイプを指定する
    row.heightFor = { [weak self] _ -> CGFloat? in
        return self?.visibleCell ? 52 : 0    // セルを表示させたくない場合は0を返す
    }
    row.configureCell = { cell, _ in }
    row.didSelect = { _ in }
}

一覧画面

またまた、こんな悩みを抱えている人がいたとします。

「一覧画面ってオブジェクトの配列に対応したセルを表示したいけど、デリゲートメソッド内の条件分岐一箇所いじったら配列の要素アクセス時にアウトオブバウンスが起きることがあって辛い。。。」

例えばツイート一覧を表示させたい場合、大抵の場合はツイートオブジェクトの配列を何処かに保持していると思います。
配列に対応したRowを生成する機能も、しなっと実装されています。

// var tweets: [Tweet]
section.createRows(tweets) { (tweet: Tweet, row: Row<TweetTableViewCell>) in
    row.height = 52
    row.configureCell = { cell in
        cell.nameLabel.text = tweet.name
        cell.textLabel.text = tweet.text
    }
}

最後に

まだまだ未対応の機能があったり、UICollectionViewにも導入したいと考えていますので、気づいたことがあったらPullReq頂けると幸いです!

yukiasai/Shoyu

今後ともShoyuをよろしくお願いします。

47
45
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
47
45

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?