iOSアプリには必須のUITableView。
幾つかのアプリ開発を経験し、UITableViewの実装をもっと簡潔に出来ないか考え続けてきました。
試行錯誤した結果、なかなかできの良いライブラリを作ることに成功しました。
UITableViewを普通に実装した時の問題点
UITableView
の実装は、UITableViewDelegate
とUITableViewDataSource
を実装するのが一般的です。
このデリゲートパターンは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
-
UITableViewDelegate
とUITableViewDataSource
の未対応のメソッドが結構ある- 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頂けると幸いです!
今後ともShoyuをよろしくお願いします。