UITableView
swift4

Swift4 TableViw の実装 (...を自分のためだけに整理)

Swift4 TableViw の実装を自分のためだけに整理

Swift4 ... インターネットでググりながら試行錯誤して来た結果...
自分なりの TableView の実装の形ができて来たので、自分のためだけに整理してみました。

最近作る TableView はこんな感じです。
セルの数が固定で、一部のセルをボタンとして機能させます。
データ数が変化するデータの表示には向かない内容の記事です...。 ^^;

swift4_tableview_sample_3.gif

XIBでUIの設定

自分は UITableViewController は好まないので、 ViewController に UITableView を貼り付けます。

UITableView の Outlet設定を 図のように 3箇所 設定しています。
swift4_tableview_set_1.png

また、 Style は Grouped を好んで選択しています。
swift4_tableview_set_2.png

コーディング

あとは ViewController に TableView を実装します。

セルのタイトル

固定のセルのTableView を作成する機会が多いので、最近はセル名を固定で定義し、 IndexPath から名称を取得できるようにしています。

ViewController.swift
extension ViewController {

    internal struct TableDef {

        //~~~ 各セクション ~~~~
        //項目名: セクション1
        static let Section1Rows = [
            "cell A",
            "cell B",
            "cell C",
            ]

        enum Section1RowIndex: Int, Codable {
            case
            cell_A = 0,
            cell_B = 1,
            cell_C = 2
        }
        // ...

        //~~~ セクション (以下の3つの配列の順序を合わせる) ~~~
        //セクション名
        static let Section = [
            "section 0",
            "section 1",
            "section 2",
        ]

        enum SectionRowIndex: Int, Codable {
            case
            section0 = 0,
            section1 = 1,
            section2 = 2
        }

        //項目名 (各セクションをまとめる)
        static let Rows = [
            TableDef.Section1Rows,
            TableDef.Section1Rows,
            TableDef.Section1Rows,
        ]


        //項目名を返す
        static func row(_ indexPath: IndexPath) -> String? {
            guard (0 <= indexPath.section), (indexPath.section < Rows.count) else {
                return nil
            }

            let lists = Rows[indexPath.section]

            guard (0 <= indexPath.row), (indexPath.row < lists.count) else {
                return nil
            }

            //正常終了
            return lists[indexPath.row]
        }
    }
}

KVO (Key Value Observer)

最近よく実装する形は、最近覚えた KVO を利用し、変化がある時は TableView の該当するセルの中を更新しています。
(自分の場合は、セクションごと更新しています。)

ViewController.swift
    override func viewDidLoad() {
        super.viewDidLoad()

        self.initKVO()
    }

    fileprivate func initKVO() {

        //KVO: battery
        if let status = xxx.status {

            let kvo = status.observe(\.xxx) { (status, change) in
                //change is Always nil. (⇨ Objective-Cで表現できるプロパティではない。)
                print(change)

                //TableViewを更新(セクション単位で更新するメソッドを用意)
                self.updateSection(xxx: status.xxx)
            }

            self.keyValueObservations.append(kvo)
        }
    }

セクションをたたむ

こちらの記事 の「セクションをたたむ様子」 が好きなので、無駄に多用してます。
(コードは参考にさせてもらった記事がとてもわかりやすいので割愛 ^^;

セルをボタン的に扱う

あとは、UIButtonがあるカスタムセルの用意がめんどくさいときは、特定のセルをボタンのように扱っています。
ひとつのセルに触れた時だけ、選択状態にしています。

ViewController.swift
extension ViewController: UITableViewDataSource, UITableViewDelegate {

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

        //セルの作成
        let cell = self.createCell(tableView, indexPath)

        return cell
    }

    func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {

        //セルに触れた時にボタンとして振る舞う場合のみ indexPath を返す
        if (self.isTouchCellIndexPath(indexPath)) {
            return indexPath
        }

        return nil
    }

    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        //print(indexPath)

        //セルから指を離したときにコマンドを実行
        self.onClickCell(indexPath)
    }
}

extension ViewController {

    //セルの作成
    internal func createCell(_ tableView: UITableView, _ indexPath: IndexPath) -> UITableViewCell {

        //セルの作成
        let cell = UITableViewCell(style: .value1, reuseIdentifier: "cell")

        if (true) {
            //デバッグ用
            cell.textLabel?.text        = TableDef.row(indexPath)
            cell.detailTextLabel?.text  = self.getString(indexPath)
            //cell.accessoryType = UITableViewCellAccessoryType.disclosureIndicator
        }

        //選択状態 (セルによっては有効にしたい)
        cell.selectionStyle = self.isTouchCellIndexPath(indexPath) ? (.default) : (.none)

        //指を離したときに .selectionStyle を元に戻したい
        if (cell.selectionStyle == .default) {

            // https://qiita.com/taketomato/items/e0e45f0d768109abef2f
            let gesture = UILongPressGestureRecognizer(target: self, action: #selector(tapCell(_:)))
            gesture.minimumPressDuration = 0
            cell.addGestureRecognizer(gesture)
        }

        return cell
    }
}

セルをボタンとして扱うかどうかを判定するためのisTouchCellIndexPathメソッド内の処理は、固定セル前提の実装なので...このような実装をしています。

ViewController.swift
    fileprivate func isTouchCellIndexPath(_ indexPath: IndexPath) -> Bool {

        //タップしたい項目だけを登録
        let list: [Int : [Int]] = [
            TableDef.SectionRowIndex.section0.rawValue :
                [
                    TableDef.Section1RowIndex.cell_A.rawValue,
                    TableDef.Section1RowIndex.cell_B.rawValue,
                    TableDef.Section1RowIndex.cell_C.rawValue,
            ],

            TableDef.SectionRowIndex.section2.rawValue :
                [
                    TableDef.Section1RowIndex.cell_B.rawValue,
            ],
        ]

        return list[indexPath.section]?.contains(indexPath.row) ?? false
    }

全体的なViewControllerのコードは gist にアップしてみました。(XCode 9.2 で確認)

まとめながら気づいた点

Cell作成時にaddGestureRecognizerしたけど...破棄のことを考慮してなかった...。 ^^;