エンターテイメント!!

遊戯王好きのJavaエンジニアのブログ。バーニングソウルを会得する特訓中。

UItableViewのセルに配置した部品のデータの管理方法

きっかけ

「テーブルで部品を配置して」と仕事上頼まれて、配置したはいいが、項目が表示されないときにnilになることを忘れていて、金曜日の定時間際に慌てて修正しようとしたけど、パニックって時間だけ浪費して終わってしまったので、土日にじっくり勉強したので晒す。

余談

じっくりと言っても、ジャンフェスとか行ってきたから、実際は、あんまり時間をかけてない。
というか、ジャンフェス混みすぎだろ。。。
終わり間際に行ったつもりだったのに、3時間待ちとか聞いてない。
10年くらい前なら、終わり間際はガラガラだよってのを聞いたのに。。。。
鬼滅のせいか?
やたらと鬼滅って単語が物販列に並んでる最中に飛び交ってた印象がある。

実装

完成品は、下記のgithubにあげてます。

https://github.com/suzaku-tec/swift-sample/tree/master/table-layout-sample/TableLayoutSample

パニックった経緯

最初は、iphoneXの画面上にテーブル内のセルの全部の項目が表示されており、firebaseに入力されたデータを登録していた。
なんかく実装できていたので、余裕じゃんって思ってて、調子ぶっこいて、だらだらテストしていた。
で、「別端末で表示しても大丈夫だよな?」って思って、iphone8で実証を始めた。
AutoLayoutを利用することを心がけていたので、事象が起きた画面に遷移するまでは、全然レイアウトの問題はなかった。
問題の画面に到達。
そうしたら、テーブル内のセルが一部非表示になっていたが、気にせず入力してfirebaseに更新する処理を実行したら、例外が。。。
スタックトレースの内容を見ていると、なぜかnilにアクセスしようとしていて落ちていた。
対象のセルは、画面に非表示だった一番最後のセル。
意味がわからなかったので、再現性を確かめるべく、スクロールして値を入力してみたら、今度は、画面に表示の最上部のセルがnilで落ちた。。。

最初は、取ってくるところが間違っていると思ったが、それならiphoneXでも発生するだろうと思って、悩んでいたが、画面に非表示になるとエラーになるということに気づいて、真因が分かった。
ただ、原因が分かったからと言って、対応方法が分かったというのは、別問題なのよね。。。

対応方法を考えているうちに、時間だけが過ぎていき、パニック状態に。。。。

対応方法

実際の現場で試したわけではないが、大まかな対応方法を検証できた。

対応としては、protocolで独自のイベントを作って、セルの編集イベントを検知したら、tableViewにイベントを通知して、ViewControllerに保持しているdatasourceに入力内容を保存するようにした。

作ったprotocol

protocol InputTextTableCellDelegate {
    func textFieldDidEndEditing(cell: BaseCell, value: String) -> ()
}

イベントの検知方法

cell側

class BaseCell: UITableViewCell, UITextFieldDelegate {
    
    @IBOutlet weak var label: UILabel!
    @IBOutlet weak var textField: UITextField! {
        didSet {
            textField.delegate = self
        }
    }
    
    var index: Int = -1

    var delegate: InputTextTableCellDelegate! = nil
    
    func textFieldDidEndEditing(_ textField: UITextField) {
        print("textFieldDidEndEditing")
        self.delegate.textFieldDidEndEditing(cell: self, value: textField.text!)
    }
}

作ったプロトコルを保持しておき、TextFieldの変更イベントを検出したら、protocolを叩くようにしている。 ※self.delegate.textFieldDidEndEditing(cell: self, value: textField.text!)の部分が実際に通知している箇所

table側

extension ViewController: UITableViewDelegate, UITableViewDataSource, InputTextTableCellDelegate {
    func textFieldDidEndEditing(cell: BaseCell, value: String) {
        datasource[cell.index].text  = value
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        datasource.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        
        let cell = table.dequeueReusableCell(withIdentifier: "base") as! BaseCell
        
        let index = indexPath.row
        
        cell.label.text = datasource[index].label
        cell.textField.text = datasource[index].text
        cell.index = index
        cell.delegate = self
        
        return cell
    }
}

前述した func textFieldDidEndEditing(cell: BaseCell, value: String) が、セルが変更あると叩いてくるので、このメソッドの中で、datasourceの値を更新するようにしている。
delegateの設定とかの話は割愛

SwiftのTableの実装は厄介

Swiftのテーブルは、非表示になるとリソースを有効活用するために、非表示のセルをcellForRow(at: <IndexPath>)で取得するとnilが返ってくるものらしい。
以前も同じ轍を踏んだ気がする。。。
でも、再度表示すると値が残っているから、てっきり、cellForRow(at: <IndexPath>)でいつでも取得できるものだと勘違いしてしまった。
cellForRow(at: <IndexPath>)は、誤解を招きやすいので、削除してもらいたい気分。

テーブルを使う際は、いつでもセルが復元可能なように、データを実装側で管理する必要があるってのは、常に頭に入れておく必要がある。
それに伴って、swiftのAPI的に、セルを取得するようなメソッドは、削除してもらいたい。
入力保管で開発をやってると、結構な確率でドツボにハマる気がする。。。

雑記

protocolの知識が怪しい。。。
やりたいことの通知方法の概念は分かっているのだが、いざやるとなると戸惑ってしまう。
分かっていても、実際にやれるとは限らないんだなって痛感した。。。

参考サイト

【Xcode】文字入力できるTableViewCellサンプル - 文系プログラマの勉強ノート

大変助かりました。