0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Swift】カスタムセルに複数配置したTextFieldの再利用バグを防ぐ

Posted at

前回の続きです。

前回は、カスタムセルに配置したTextFieldが1つの場合でしたが、今回は複数配置した場合の再利用バグを防ぐ方法になります。
前提となるのは前回の記事を参照していただければと思います。

Step1 前回を元に土台作り

今回のカスタムセルは以下になります。
スクリーンショット 2024-02-07 15.57.05.png

前回の内容を元に、基本的な部分までは記述していきます。

ViewController.swift
import UIKit

class ViewController: UIViewController {

    @IBOutlet weak var tableView: UITableView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.register(UINib(nibName: "TableViewCell", bundle: nil), forCellReuseIdentifier: "TableViewCell")
        tableView.dataSource = self
        tapScreen()
    }
    
    //キーボードを閉じる処理。
    func tapScreen() {
        let tapGesture = UITapGestureRecognizer(target: self, action: #selector(closedKeyboard))
        tapGesture.cancelsTouchesInView = false
        view.addGestureRecognizer(tapGesture)
    }
    @objc func closedKeyboard() {
        view.endEditing(true)
    }
}

extension ViewController: UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 10
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        if let cell = tableView.dequeueReusableCell(withIdentifier: "TableViewCell", for: indexPath) as? TableViewCell {
            cell.updateLabel(count: "\(indexPath.row + 1)")
            return cell
        } else {
            return UITableViewCell()
        }
    }
}
TableViewCell.swift
import UIKit

protocol TableViewCellDelegate: AnyObject {
    //TextFieldの変更を通知するdelegateを作成
    func textFieldEditing(cell: TableViewCell, value: String)
}

class TableViewCell: UITableViewCell {
    
    weak var delegate: TableViewCellDelegate?
    
    @IBOutlet weak var countLabel: UILabel!
    @IBOutlet weak var textField1: UITextField!
    @IBOutlet weak var textField2: UITextField!
    @IBOutlet weak var textField3: UITextField!
    @IBOutlet weak var textField4: UITextField!
    
    override func awakeFromNib() {
        super.awakeFromNib()
        textField1.delegate = self
        textField2.delegate = self
        textField3.delegate = self
        textField4.delegate = self
    }
    
    func updateLabel(count: String) {
        countLabel.text = "\(count)番目のセル"
    }
}

extension TableViewCell: UITextFieldDelegate {
    func textFieldDidChangeSelection(_ textField: UITextField) {
        self.delegate?.textFieldEditing(cell: self, value: textField.text!)
    }
}

ここまでをビルドすると、以下のようになります。
S__4218885.gif

このように、8段目のセルに、1段目に入力したはずの値がセルの再利用によって入力されてしまっています。
今回は進めるにつれてこの再利用のバグが変化していくので、逐一確認していきます。

Step2 Modelを作成し仮保存処理

前回同様、セルの再利用を防ぐために入力した値を仮保存するためのModelを作成します。

testDataModel.swift
struct TestDataModel {
    var testText: String = ""
}

カスタムセル上にtextFieldは4つありますが、Modelの変数は一つで大丈夫です。
Controller側でModelをインスタンス化します。また、Model型の配列も作成しておきます。
また、作成したModel型の配列に対して、空の値をセルの数分だけ追加します。理由の詳細は前回の記事を参考にしてください。

ViewController.swift
import UIKit

class ViewController: UIViewController {

    @IBOutlet weak var tableView: UITableView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.register(UINib(nibName: "TableViewCell", bundle: nil), forCellReuseIdentifier: "TableViewCell")
        tableView.dataSource = self
        tapScreen()
        setData()
    }
    
    var testDataModel = TestDataModel()
    //カスタムセルに配置したtextFieldの数だけ作成
    var textFieldData1: [TestDataModel] = []
    var textFieldData2: [TestDataModel] = []
    var textFieldData3: [TestDataModel] = []
    var textFieldData4: [TestDataModel] = []
    
    //キーボードを閉じる処理。
    func tapScreen() {
        let tapGesture = UITapGestureRecognizer(target: self, action: #selector(closedKeyboard))
        tapGesture.cancelsTouchesInView = false
        view.addGestureRecognizer(tapGesture)
    }
    @objc func closedKeyboard() {
        view.endEditing(true)
    }
    
    func setData() {
        for _ in 1...10 {
            textFieldData1.append(testDataModel)
            textFieldData2.append(testDataModel)
            textFieldData3.append(testDataModel)
            textFieldData4.append(testDataModel)
        }
    }
}
//(以下省略)

Step3 再利用セルの初期化

次に、TableViewの中でセルの再利用を防ぎます。前回同様、Controller側でセルの初期化処理を記述します。

ViewController.swift
//(省略)
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    if let cell = tableView.dequeueReusableCell(withIdentifier: "TableViewCell", for: indexPath) as? TableViewCell {
        //セルの再利用を防ぐため、初期化を行う
        cell.textField1.text = nil
        cell.textField2.text = nil
        cell.textField3.text = nil
        cell.textField4.text = nil
            
        cell.updateLabel(count: "\(indexPath.row + 1)")
        cell.delegate = self
        return cell
    } else {
        return UITableViewCell()
    }
}

ここまでが準備になります。
大体、ここまでは前回とほぼ同じになりますが、ここからやり方が少し変わります。

Step4 入力した内容を仮保存する

前回のように、入力した内容を作成した配列に追加し、該当するセルのModelに代入していく作業を行います。

ViewController.swift
extension ViewController: TableViewCellDelegate {
    func textFieldEditing(cell: TableViewCell, value: String) {
        let path = tableView.indexPathForRow(at: cell.convert(cell.bounds.origin, to: tableView))
        let indexPath = path?.row
        textFieldData1.append(TestDataModel(testText: value))
        textFieldData1[indexPath!] = TestDataModel(testText: value)
        textFieldData2.append(TestDataModel(testText: value))
        textFieldData2[indexPath!] = TestDataModel(testText: value)
        textFieldData3.append(TestDataModel(testText: value))
        textFieldData3[indexPath!] = TestDataModel(testText: value)
        textFieldData4.append(TestDataModel(testText: value))
        textFieldData4[indexPath!] = TestDataModel(testText: value)
    }
}

また、TableViewDataSourceにて、入力した値を代入することで仮保存する処理も記述します。

ViewController.swift
extension ViewController: UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 10
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        if let cell = tableView.dequeueReusableCell(withIdentifier: "TableViewCell", for: indexPath) as? TableViewCell {
            //セルの再利用を防ぐため、初期化を行う
            cell.textField1.text = nil
            cell.textField2.text = nil
            cell.textField3.text = nil
            cell.textField4.text = nil
            
            cell.textField1.text = textFieldData1[indexPath.row].testText
            cell.textField2.text = textFieldData2[indexPath.row].testText
            cell.textField3.text = textFieldData3[indexPath.row].testText
            cell.textField4.text = textFieldData4[indexPath.row].testText
            
            cell.updateLabel(count: "\(indexPath.row + 1)")
            cell.delegate = self
            return cell
        } else {
            return UITableViewCell()
        }
    }
}

前回はこれで問題なくできましたが、今回はこのままではダメです。
この状態だと、以下のようになります。

S__4218901.gif

セルの再利用は防げていますが、一つのTextFieldに入力した値が同じセルの他のTextFieldにも反映されてしまいます。
これは、セルの通知を送る側の処理と、Controller側の通知を受け取る側の処理に問題があるからです。

Step5 通知を送る側の処理を修正

まず、delegateメソッドを一つにまとめず、分割します。
一つにまとめていると、いずれかのTextFieldに入力があった際に、他のTextFieldにも通知が送られてしまうからです。

TableViewCell.swift
protocol TableViewCellDelegate: AnyObject {
    //TextFieldの変更を通知するdelegateを作成
    func textFieldEditing1(cell: TableViewCell, value: String)
    func textFieldEditing2(cell: TableViewCell, value: String)
    func textFieldEditing3(cell: TableViewCell, value: String)
    func textFieldEditing4(cell: TableViewCell, value: String)
}

次に、textFieldDidChangeSelectionの内容もそれに合わせて分割します。
この時、引数valueに対してそれぞれのTextFieldを定数に設定します。

TableViewCell.swift
extension TableViewCell: UITextFieldDelegate {
    func textFieldDidChangeSelection(_: UITextField) {
        self.delegate?.textFieldEditing1(cell: self, value: textField1.text!)
        self.delegate?.textFieldEditing2(cell: self, value: textField2.text!)
        self.delegate?.textFieldEditing3(cell: self, value: textField3.text!)
        self.delegate?.textFieldEditing4(cell: self, value: textField4.text!)
    }
}

カスタムセル側の処理は以上になります。

Step6 通知を受け取る側の処理を修正

次に、Controller側でカスタムセルの変更に合わせて修正を行います。
コンパイルエラーが起きていると思うので、自動入力してあげましょう。

ViewContoller.swift
extension ViewController: TableViewCellDelegate {
    func textFieldEditing1(cell: TableViewCell, value: String) {
        <#code#>
    }
    
    func textFieldEditing2(cell: TableViewCell, value: String) {
        <#code#>
    }
    
    func textFieldEditing3(cell: TableViewCell, value: String) {
        <#code#>
    }
    
    func textFieldEditing4(cell: TableViewCell, value: String) {
        <#code#>
    }
    
    func textFieldEditing(cell: TableViewCell, value: String) {
        let path = tableView.indexPathForRow(at: cell.convert(cell.bounds.origin, to: tableView))
        let indexPath = path?.row
        textFieldData1.append(TestDataModel(testText: value))
        textFieldData1[indexPath!] = TestDataModel(testText: value)
        textFieldData2.append(TestDataModel(testText: value))
        textFieldData2[indexPath!] = TestDataModel(testText: value)
        textFieldData3.append(TestDataModel(testText: value))
        textFieldData3[indexPath!] = TestDataModel(testText: value)
        textFieldData4.append(TestDataModel(testText: value))
        textFieldData4[indexPath!] = TestDataModel(testText: value)
    }
}

このようになると思うので、一まとめになっていたのを、それぞれのdelegateメソッドに対して分割していきます。

ViewController.swift
extension ViewController: TableViewCellDelegate {
    func textFieldEditing1(cell: TableViewCell, value: String) {
        let path = tableView.indexPathForRow(at: cell.convert(cell.bounds.origin, to: tableView))
        let indexPath = path?.row
        textFieldData1.append(TestDataModel(testText: value))
        textFieldData1[indexPath!] = TestDataModel(testText: value)
    }
    
    func textFieldEditing2(cell: TableViewCell, value: String) {
        let path = tableView.indexPathForRow(at: cell.convert(cell.bounds.origin, to: tableView))
        let indexPath = path?.row
        textFieldData2.append(TestDataModel(testText: value))
        textFieldData2[indexPath!] = TestDataModel(testText: value)
    }
    
    func textFieldEditing3(cell: TableViewCell, value: String) {
        let path = tableView.indexPathForRow(at: cell.convert(cell.bounds.origin, to: tableView))
        let indexPath = path?.row
        textFieldData3.append(TestDataModel(testText: value))
        textFieldData3[indexPath!] = TestDataModel(testText: value)
    }
    
    func textFieldEditing4(cell: TableViewCell, value: String) {
        let path = tableView.indexPathForRow(at: cell.convert(cell.bounds.origin, to: tableView))
        let indexPath = path?.row
        textFieldData4.append(TestDataModel(testText: value))
        textFieldData4[indexPath!] = TestDataModel(testText: value)
    }
}

総括

以上のことを踏まえると、以下のようになります。

S__4218902.gif

一つのカスタムセルで複数のTextFieldを扱う場合、肝心なのはセルからControllerへ通知を送る際に、分割することです。
一番苦戦したのは、定数を内部引数などに設定して統一していたことです。

ViewController.swift
extension TableViewCell: UITextFieldDelegate {
    func textFieldDidChangeSelection(_ textField: UITextField) {
        self.delegate?.textFieldEditing1(cell: self, value: textField.text!)
        self.delegate?.textFieldEditing2(cell: self, value: textField.text!)
        self.delegate?.textFieldEditing3(cell: self, value: textField.text!)
        self.delegate?.textFieldEditing4(cell: self, value: textField.text!)
    }
}

これにより、通知を分割したとしても、配置してあるすべてのTextFieldが対象となってしまうため、どこか一つに入力した際にすべてのセルに同じ値が入力されてしまいました。
そのため、定数をしっかりと分けてあげることが、TextFieldを入力のあったところだけ分別できるようになります。
TableViewに関してはまだまだ未知数なところが多いので、色々試しながら知識を増やしていこうと思います。
次回は、カスタムセルとRealmを使った保存処理を記事にしてみようと思います。

最終的なコードは以下になります。

ViewController.swift
import UIKit

class ViewController: UIViewController {

    @IBOutlet weak var tableView: UITableView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.register(UINib(nibName: "TableViewCell", bundle: nil), forCellReuseIdentifier: "TableViewCell")
        tableView.dataSource = self
        tapScreen()
        setData()
    }
    
    var testDataModel = TestDataModel()
    //カスタムセルに配置したtextFieldの数だけ作成
    var textFieldData1: [TestDataModel] = []
    var textFieldData2: [TestDataModel] = []
    var textFieldData3: [TestDataModel] = []
    var textFieldData4: [TestDataModel] = []
    
    //キーボードを閉じる処理。
    func tapScreen() {
        let tapGesture = UITapGestureRecognizer(target: self, action: #selector(closedKeyboard))
        tapGesture.cancelsTouchesInView = false
        view.addGestureRecognizer(tapGesture)
    }
    @objc func closedKeyboard() {
        view.endEditing(true)
    }
    
    func setData() {
        for _ in 1...10 {
            textFieldData1.append(testDataModel)
            textFieldData2.append(testDataModel)
            textFieldData3.append(testDataModel)
            textFieldData4.append(testDataModel)
        }
    }
}

extension ViewController: UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 10
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        if let cell = tableView.dequeueReusableCell(withIdentifier: "TableViewCell", for: indexPath) as? TableViewCell {
            //セルの再利用を防ぐため、初期化を行う
            cell.textField1.text = nil
            cell.textField2.text = nil
            cell.textField3.text = nil
            cell.textField4.text = nil
            
            cell.textField1.text = textFieldData1[indexPath.row].testText
            cell.textField2.text = textFieldData2[indexPath.row].testText
            cell.textField3.text = textFieldData3[indexPath.row].testText
            cell.textField4.text = textFieldData4[indexPath.row].testText
            
            cell.updateLabel(count: "\(indexPath.row + 1)")
            cell.delegate = self
            return cell
        } else {
            return UITableViewCell()
        }
    }
}

extension ViewController: TableViewCellDelegate {
    func textFieldEditing1(cell: TableViewCell, value: String) {
        let path = tableView.indexPathForRow(at: cell.convert(cell.bounds.origin, to: tableView))
        let indexPath = path?.row
        textFieldData1.append(TestDataModel(testText: value))
        textFieldData1[indexPath!] = TestDataModel(testText: value)
    }
    
    func textFieldEditing2(cell: TableViewCell, value: String) {
        let path = tableView.indexPathForRow(at: cell.convert(cell.bounds.origin, to: tableView))
        let indexPath = path?.row
        textFieldData2.append(TestDataModel(testText: value))
        textFieldData2[indexPath!] = TestDataModel(testText: value)
    }
    
    func textFieldEditing3(cell: TableViewCell, value: String) {
        let path = tableView.indexPathForRow(at: cell.convert(cell.bounds.origin, to: tableView))
        let indexPath = path?.row
        textFieldData3.append(TestDataModel(testText: value))
        textFieldData3[indexPath!] = TestDataModel(testText: value)
    }
    
    func textFieldEditing4(cell: TableViewCell, value: String) {
        let path = tableView.indexPathForRow(at: cell.convert(cell.bounds.origin, to: tableView))
        let indexPath = path?.row
        textFieldData4.append(TestDataModel(testText: value))
        textFieldData4[indexPath!] = TestDataModel(testText: value)
    }
}

TableViewCell.swift
import UIKit

protocol TableViewCellDelegate: AnyObject {
    //TextFieldの変更を通知するdelegateを作成
    func textFieldEditing1(cell: TableViewCell, value: String)
    func textFieldEditing2(cell: TableViewCell, value: String)
    func textFieldEditing3(cell: TableViewCell, value: String)
    func textFieldEditing4(cell: TableViewCell, value: String)
}

class TableViewCell: UITableViewCell {
    
    weak var delegate: TableViewCellDelegate?
    
    @IBOutlet weak var countLabel: UILabel!
    @IBOutlet weak var textField1: UITextField!
    @IBOutlet weak var textField2: UITextField!
    @IBOutlet weak var textField3: UITextField!
    @IBOutlet weak var textField4: UITextField!
    
    override func awakeFromNib() {
        super.awakeFromNib()
        textField1.delegate = self
        textField2.delegate = self
        textField3.delegate = self
        textField4.delegate = self
    }
    
    func updateLabel(count: String) {
        countLabel.text = "\(count)番目のセル"
    }
}

extension TableViewCell: UITextFieldDelegate {
    func textFieldDidChangeSelection(_: UITextField) {
        self.delegate?.textFieldEditing1(cell: self, value: textField1.text!)
        self.delegate?.textFieldEditing2(cell: self, value: textField2.text!)
        self.delegate?.textFieldEditing3(cell: self, value: textField3.text!)
        self.delegate?.textFieldEditing4(cell: self, value: textField4.text!)
    }
}
TestDataModel.swift
struct TestDataModel {
    var testText: String = ""
}

最後まで閲覧していただき、ありがとうございました!

0
1
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
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?