UITableViewCell:セルの重複を防ぐ
UITableViewをセットしてスクロールしてみると、セルの重複が発生してしまう。 その場合の解決方法を備忘録として残します。お役に立てれば幸いです。

環境
Xcode:13.1
Swift:5.5.1
全体
先に全体をお見せします。今回はStoryboardを使用せず、コードのみとなります。
//ViewController.swift:画面に表示させるクラス import UIKit class ViewController: UIViewController { ///セルに表示させるタイトルの配列 let array = ["オオカミ", "イヌ", "ネコ", "ウマ", "ウシ", "ブタ", "ゴリラ", "オランウータン", "サル", "カバ", "シカ", "ライオン", "ヒョウ", "トラ", "クマ", "パンダ", "ハト", "ツル", "ニワトリ", "タカ", "ワシ", "カピバラ", "ゾウ", "キリン", "ラクダ", "カメレオン", "コウモリ", "ネズミ", "ハムスター", "ヘビ", "サメ", "イルカ", "シャチ", "クジラ", "クラゲ", "サケ", "サンマ", "ブリ", "イカ", "タコ", "ウナギ", "アンコウ"] ///tableView let tableView = UITableView() ///セル登録の際に用いるID let cellId = "cellId" override func viewDidLoad() { super.viewDidLoad() //背景 view.backgroundColor = .white //デリゲート指定 tableView.delegate = self tableView.dataSource = self //背景 tableView.backgroundColor = .white //セルの登録(ID付与) tableView.register(TableViewCell.self, forCellReuseIdentifier: cellId) //何も表記されていない余計なセルを削除するため、UIViewを設置 tableView.tableFooterView = UIView(frame: .zero) //TableViewの表示 view.addSubview(tableView) //TableViewの配置 tableView.anchor(top: view.topAnchor, bottom: view.bottomAnchor, left: view.leftAnchor, right: view.rightAnchor) //↑bottom: view.bottomAnchorの指定を忘れないように。 } } //MARK: - UITableViewDelegate, UITableViewDataSource extension ViewController: UITableViewDelegate, UITableViewDataSource { //セルの数 func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { //セルに表示させるタイトルの配列の数を指定 return array.count } //セルの中身 func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { //TableViewCellインスタンスの生成 let cell = tableView.dequeueReusableCell(withIdentifier: cellId) as! TableViewCell //セル内のUI設定 cell.setCustomCell(title: array[indexPath.row]) //設定したセルを返す(TableViewに反映する) return cell } //セルの高さ func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { return 80 } }
//TableViewCell.swift:セルとその中身のUIを設定するクラス。 import UIKit class TableViewCell: UITableViewCell { ///セル内のUI設定 func setCustomCell(title: String) { /* サブビューの削除 ①セルの重複が起きないように、一旦セル内のサブビュー(セル内に設置したUIたち)を削除。 理由:UITableViewのデリメソcellForRowAtは、生成したセルを再利用する仕組みがある。 そのため、ある一定までスクロールしていくと、既に生成したセルの再利用(再表示)と新たに生成されるセルが重複して表示されてしまう。 これを防ぐために、再利用されるセルを削除する必要がある。 ②さらにタグで指定したサブビューだけを削除する理由は、 全てのサブビューを削除してしまうと、UITableViewCellのサブビューであるタップを感知するUIResponderまで削除してしまうため。 */ self.subviews.forEach { subView in if subView.tag == 1 { subView.removeFromSuperview() } } //新たにセル内のUI(サブビュー)をセット //タイトル let titleLabel = UILabel() titleLabel.text = title titleLabel.textColor = .gray titleLabel.font = .systemFont(ofSize: 20, weight: .regular) titleLabel.textAlignment = .center //タグ指定→理由:削除するものを指定するために titleLabel.tag = 1 //表示 self.addSubview(titleLabel) //配置 titleLabel.anchor(centerY: self.centerYAnchor, centerX: self.centerXAnchor, width: 200) } }
//View+Extesion.swift:各UIを配置する処理 import UIKit extension UIView { ///Viewの配置設定 /// - Parameters: /// - top:基準となる上方向の対象物を設定 /// - bottom:基準となる下方向の対象物を設定 /// - left:基準となる左方向の対象物を設定 /// - right:基準となる右方向の対象物を設定 /// - centerY:指定した対象物内におけるY軸による位置を設定 /// - centerX:指定した対象物内におけるX軸による位置を設定 /// - width:幅の設定 /// - height:高さの設定 /// - topPadding:基準となる上方向の対象物との距離を設定 /// - bottomPadding:基準となる下方向の対象物との距離を設定 /// - leftPadding:基準となる左方向の対象物との距離を設定 /// - rightPadding:基準となる右方向の対象物との距離を設定 func anchor(top: NSLayoutYAxisAnchor? = nil, bottom: NSLayoutYAxisAnchor? = nil, left: NSLayoutXAxisAnchor? = nil, right: NSLayoutXAxisAnchor? = nil, centerY: NSLayoutYAxisAnchor? = nil, centerX: NSLayoutXAxisAnchor? = nil, width: CGFloat? = nil, height: CGFloat? = nil, topPadding: CGFloat = 0, bottomPadding: CGFloat = 0, leftPadding: CGFloat = 0, rightPadding: CGFloat = 0) { self.translatesAutoresizingMaskIntoConstraints = false if let top = top { self.topAnchor.constraint(equalTo: top, constant: topPadding).isActive = true } if let bottom = bottom { self.bottomAnchor.constraint(equalTo: bottom, constant: -bottomPadding).isActive = true } if let left = left { self.leftAnchor.constraint(equalTo: left, constant: leftPadding).isActive = true } if let right = right { self.rightAnchor.constraint(equalTo: right, constant: -rightPadding).isActive = true } if let centerY = centerY { self.centerYAnchor.constraint(equalTo: centerY).isActive = true } if let centerX = centerX { self.centerXAnchor.constraint(equalTo: centerX).isActive = true } if let width = width { self.widthAnchor.constraint(equalToConstant: width).isActive = true } if let height = height { self.heightAnchor.constraint(equalToConstant: height).isActive = true } } }
解決方法
TableViewCellクラスのメソッド「setCustomCell」に記述した下記で解決できます。
self.subviews.forEach { subView in if subView.tag == 1 { subView.removeFromSuperview() } }
UITableViewCellのサブビューを指定し、さらにその中でタグ番号が1番のサブビューを削除する処理をしております。
何故タグで指定したサブビューのみを削除するのか
UITableViewCellの階層内でサブビューに該当するものの中に、UIResponderというユーザーのタップを感知(受信)する機能を持ったサブビューが存在するためです。これごと削除してしまうと、セルのタップ(押下)ができなくなってしまいます。そのためタグで指定したサブビューのみを削除する処理にしております。
下記はセル内にセットするUILabel(サブビュー)にタグ付けする記述です。
//タイトル let titleLabel = UILabel() titleLabel.text = title titleLabel.textColor = .gray titleLabel.font = .systemFont(ofSize: 20, weight: .regular) titleLabel.textAlignment = .center //タグ指定→理由:削除するものを指定するために titleLabel.tag = 1
結果
画像なのでわかりづらいですが、重複はなくなりました。

以上になります。最後まで読んでいただきましてありがとうございます。