Edited at

[Swift]ひたすら楽してUITableView

多分これが一番楽だと思います


UITableViewとは


  • めちゃくちゃ使うパーツ

  • Cellを入れ替えられるので便利

  • 状態によって、indexPathやCellTypeを出し分けてると徐々に訳が解らなくなる

毎日使うモノだからこそ、楽にしたいですね。


楽とは


  • 速い

  • かんたん

  • 誰でもできる

  • 変更しやすい(重要)


備考

https://qiita.com/tattn/items/dc7dfe2fceec00bb4ff7

こちらのUITableView+.swiftを使わせていただいております

ありがとうございます。


1.シンプルなコード

import UIKit

class ThirdViewController: UIViewController {

@IBOutlet weak var hogeTableView: UITableView!

enum CellType {
case aaa
case bbb
case ccc
}

let cellTypes:[CellType] = [.aaa,
.bbb,
.ccc]

override func viewDidLoad() {
super.viewDidLoad()

hogeTableView.delegate = self
hogeTableView.dataSource = self
hogeTableView.rowHeight = UITableViewAutomaticDimension

}
}

extension ThirdViewController: UITableViewDelegate,UITableViewDataSource {

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return cellTypes.count
}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cellType = cellTypes[indexPath.row]

switch cellType {
case .aaa:
let cell = tableView.dequeueReusableCell(with: AAATableViewCell.self, for: indexPath)
return cell
case .bbb:
let cell = tableView.dequeueReusableCell(with: BBBTableViewCell.self, for: indexPath)
return cell
case .ccc:
let cell = tableView.dequeueReusableCell(with: CCCTableViewCell.self, for: indexPath)
return cell
}
}

}


結果

Simulator Screen Shot - iPhone 8 Plus - 2018-08-29 at 21.17.10.png


解説

UITableViewが煩雑になるのはindexPathという位置に応じたcellを場合分けで出そうとするからです。

なので、cellのtypeによって以下のように返却できると非常にシンプルになります。

当たり前ですがCellは位置に依存しないように定義するべきです。


func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

let cellType = cellTypes[indexPath.row]

switch cellType {
case .aaa:
let cell = tableView.dequeueReusableCell(with: AAATableViewCell.self, for: indexPath)
return cell
case .bbb:
let cell = tableView.dequeueReusableCell(with: BBBTableViewCell.self, for: indexPath)
return cell
case .ccc:
let cell = tableView.dequeueReusableCell(with: CCCTableViewCell.self, for: indexPath)
return cell
}
}

CellTypeをどのように定義するかは、Swiftになって大分書きやすくなりました。

実際にUITableViewがどのような順でcellを表示するかは、cellTypesで定義してやります。


enum CellType {
case aaa
case bbb
case ccc
}

let cellTypes:[CellType] = [.aaa,
.bbb,
.ccc]

ちなみに高さは最近AutoLayout任せなことが多いです(重い時以外)

この方法が楽かどうかは人によるかもしれません。


hogeTableView.rowHeight = UITableViewAutomaticDimension


2.cellの出し分けをする

ここまでなら、この書き方の恩恵をまだ感じないかもしれません。

次は、UserTypeにより2パターンの画面構成が考えられる場合です。


import UIKit

class SecondViewController: UIViewController {

@IBOutlet weak var hogeTableView: UITableView!

enum CellType {
case aaa
case bbb
case ccc
}

enum UserType {
case normal
case premium
}

var userType:UserType = .normal

func cellTypes() -> [CellType] {
switch userType {
case .normal:
return [.aaa,.bbb,.bbb,.bbb]
case .premium:
return [.aaa,.bbb,.ccc,.ccc]
}
}

override func viewDidLoad() {
super.viewDidLoad()

hogeTableView.delegate = self
hogeTableView.dataSource = self
hogeTableView.rowHeight = UITableViewAutomaticDimension

}
}

extension SecondViewController: UITableViewDelegate,UITableViewDataSource {

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return cellTypes().count
}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cellType = cellTypes()[indexPath.row]

switch cellType {
case .aaa:
let cell = tableView.dequeueReusableCell(with: AAATableViewCell.self, for: indexPath)
return cell
case .bbb:
let cell = tableView.dequeueReusableCell(with: BBBTableViewCell.self, for: indexPath)
return cell
case .ccc:
let cell = tableView.dequeueReusableCell(with: CCCTableViewCell.self, for: indexPath)
return cell
}
}

}


結果

userTyle = .normal

Simulator Screen Shot - iPhone 8 Plus - 2018-08-29 at 21.31.08.png

userTyle = .premium

Simulator Screen Shot - iPhone 8 Plus - 2018-08-29 at 21.32.38.png


解説


enum UserType {
case normal
case premium
}

var userType:UserType = .normal

func cellTypes() -> [CellType] {
switch userType {
case .normal:
return [.aaa,.bbb,.bbb,.bbb]
case .premium:
return [.aaa,.bbb,.ccc,.ccc]
}
}

cellTypes()を関数にすることで、出し分けを行っています。

この方法は非常に強力です。画面内に何を表示するか、しないのかの条件分岐が多い場合は見通しが良くなります。複雑怪奇な条件や、1Cell毎に出す出さないの判定も、CellTypeを配列に追加していくだけなので楽です。

cellを返している関数の中身が先程とほとんど変わっていないところがみそです。

普通にやると、この中で複雑な条件分岐が発生します。

もちろん、画面操作の途中でuserTypeを変更することも可能です。

画面のモードによって、cellの生成部が荒らされることはありません(heightを返す関数でも同様)


3.sectionが複数の場合

さすがにsectionの管理が入ってくると若干複雑です。


import UIKit

class ViewController: UIViewController {

@IBOutlet weak var hogeTableView: UITableView!

enum SectionType {
case section1
case section2
case section3
}
enum CellType {
case aaa
case bbb
case ccc
}

let sectionTypes:[SectionType] = [.section1,
.section2,
.section3]
let cellTypes:[SectionType:[CellType]] = [.section1:[.aaa,.aaa,.bbb],
.section2:[.aaa,.ccc],
.section3:[.bbb,.ccc,.ccc]]

override func viewDidLoad() {
super.viewDidLoad()

hogeTableView.delegate = self
hogeTableView.dataSource = self
hogeTableView.rowHeight = UITableViewAutomaticDimension
}

func cellData(_ indexPath:IndexPath) -> CellData {
let data = CellData()
data.title = "section:\(indexPath.section) row:\(indexPath.row)"
return data
}
}

extension ViewController: UITableViewDelegate,UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int {
return sectionTypes.count
}

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return cellTypes[sectionTypes[section]]!.count
}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cellType = cellTypes[sectionTypes[indexPath.section]]![indexPath.row]
let data = cellData(indexPath)

switch cellType {
case .aaa:
let cell = tableView.dequeueReusableCell(with: AAATableViewCell.self, for: indexPath)
cell.setup(cellData: data)
return cell
case .bbb:
let cell = tableView.dequeueReusableCell(with: BBBTableViewCell.self, for: indexPath)
cell.setup(cellData: data)
return cell
case .ccc:
let cell = tableView.dequeueReusableCell(with: CCCTableViewCell.self, for: indexPath)
cell.setup(cellData: data)
return cell
}
}

func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
let header = tableView.dequeueReusableCell(withIdentifier: "SectionHeaderTableViewCell")
return header
}

}

class CellData:NSObject {
var title:String = ""
}


結果

Simulator Screen Shot - iPhone 8 Plus - 2018-08-29 at 21.52.57.png


解説

しかしやってることは変わりません。

cellTypesをSectionTypeと[CellType]のDictionaryにしました。

cell供給部分は基本的に変化していません。


enum SectionType {
case section1
case section2
case section3
}
enum CellType {
case aaa
case bbb
case ccc
}

let sectionTypes:[SectionType] = [.section1,
.section2,
.section3]
let cellTypes:[SectionType:[CellType]] = [.section1:[.aaa,.aaa,.bbb],
.section2:[.aaa,.ccc],
.section3:[.bbb,.ccc,.ccc]]

もちろん実際に使用する時は、cellTypesが関数になることがあると思います。

ところで、同じCellTypeでも、Cell毎に表示するコンテンツは異なるはずです。

なので、indexPathに応じたデータモデルを受け取る関数を作りましょう。

func cellData(_ indexPath:IndexPath) -> CellData {

let data = CellData()
data.title = "section:\(indexPath.section) row:\(indexPath.row)"
return data
}

すると、cell供給部はシンプルに保てます。

もちろんcell側で解釈することになるわけで、今持ってるデータとCellに適したデータセットが異なる場合は、ViewModelを作ることになるかもしれません。

しかし大体の場合は、画面で持っているデータモデルがそのまま使用できる事が多いと思います。

case .aaa:

let cell = tableView.dequeueReusableCell(with: AAATableViewCell.self, for: indexPath)
cell.setup(cellData: data)
return cell

 

考える流れはこうです。

1.表示するデータのセットがある

2.各cellをどのように表示するか、形式を定義する(CellType)

3.cellを表示する順番を定義する

4.indexPathに応じたデータを受け取る

5.cell供給部は、「このcellの時はこれを返す」だけに集中する(実際にそのcellが要求されるかは考えない


おわりに

今まで色んな書き方を見てきましたが、最近はこの書き方に落ち着きました。

この書き方ではsectionの数やcellの数を数える必要も、マジックナンバーを書く必要もありません。なので追加も分割も容易です。

CellはCellで定義して、TableViewはどれを出すか選ぶだけなので、安心してハードな場合分けもできます。

あと簡単です。Swift歴1ヶ月でも理解できると思います。

お試しあれ。