UITableViewCell:セルの重複を防ぐ

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

f:id:SumJun-Blog:20220212091840p:plain:w200

環境

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

結果

画像なのでわかりづらいですが、重複はなくなりました。

f:id:SumJun-Blog:20220212092308p:plain:w200

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