前回の続きです。
前回は、カスタムセルに配置したTextFieldが1つの場合でしたが、今回は複数配置した場合の再利用バグを防ぐ方法になります。
前提となるのは前回の記事を参照していただければと思います。
Step1 前回を元に土台作り
前回の内容を元に、基本的な部分までは記述していきます。
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()
}
}
}
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!)
}
}
このように、8段目のセルに、1段目に入力したはずの値がセルの再利用によって入力されてしまっています。
今回は進めるにつれてこの再利用のバグが変化していくので、逐一確認していきます。
Step2 Modelを作成し仮保存処理
前回同様、セルの再利用を防ぐために入力した値を仮保存するためのModel
を作成します。
struct TestDataModel {
var testText: String = ""
}
カスタムセル上にtextField
は4つありますが、Model
の変数は一つで大丈夫です。
Controller
側でModel
をインスタンス化します。また、Model
型の配列も作成しておきます。
また、作成したModel型の配列に対して、空の値をセルの数分だけ追加します。理由の詳細は前回の記事を参考にしてください。
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
側でセルの初期化処理を記述します。
//(省略)
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
に代入していく作業を行います。
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
にて、入力した値を代入することで仮保存する処理も記述します。
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()
}
}
}
前回はこれで問題なくできましたが、今回はこのままではダメです。
この状態だと、以下のようになります。
セルの再利用は防げていますが、一つのTextField
に入力した値が同じセルの他のTextField
にも反映されてしまいます。
これは、セルの通知を送る側の処理と、Controller
側の通知を受け取る側の処理に問題があるからです。
Step5 通知を送る側の処理を修正
まず、delegate
メソッドを一つにまとめず、分割します。
一つにまとめていると、いずれかのTextField
に入力があった際に、他のTextField
にも通知が送られてしまうからです。
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
を定数に設定します。
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
側でカスタムセルの変更に合わせて修正を行います。
コンパイルエラーが起きていると思うので、自動入力してあげましょう。
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
メソッドに対して分割していきます。
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)
}
}
総括
以上のことを踏まえると、以下のようになります。
一つのカスタムセルで複数のTextField
を扱う場合、肝心なのはセルからController
へ通知を送る際に、分割することです。
一番苦戦したのは、定数を内部引数などに設定して統一していたことです。
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
を使った保存処理を記事にしてみようと思います。
最終的なコードは以下になります。
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)
}
}
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!)
}
}
struct TestDataModel {
var testText: String = ""
}
最後まで閲覧していただき、ありがとうございました!