Apple's documentation says that:
If you previously registered a class or nib file with the same reuse identifier, the class you specify in the
cellClass
parameter replaces the old entry. You may specifynil
forcellClass
if you want to unregister the class from the specified reuse identifier.
This doesn't seem to be correct as far as I can tell.
Below simple sample code demonstrates the issue. Basically, I have a slider which changes the padding value. When padding changes, it should re-register (replace the old entry) the class with reuse identifier and reload the table to show the new padding:
import UIKit
import SnapKit
extension String {
static let kPadding = Self("padding")
static let cellId = Self("cell")
}
class ViewController: UIViewController, UITableViewDataSource {
let tableView = UITableView(frame: .zero, style: .plain)
override func viewDidLoad() {
super.viewDidLoad()
registerCell()
tableView.dataSource = self
view.addSubview(tableView)
tableView.snp.makeConstraints { make in
make.horizontalEdges.top.equalToSuperview()
}
let slider = UISlider()
slider.isContinuous = false
slider.minimumValue = 0
slider.maximumValue = 100
slider.value = UserDefaults.standard.float(forKey: .kPadding)
slider.addTarget(self, action: #selector(sliderChanged(slider:)), for: .valueChanged)
view.addSubview(slider)
slider.snp.makeConstraints { make in
let padding = 15.0
make.horizontalEdges.bottom.equalTo(view.safeAreaLayoutGuide).inset(padding)
make.top.equalTo(tableView.snp.bottom).offset(padding)
}
}
@objc func sliderChanged(slider : UISlider) {
print("sliderChanged: \(slider.value)")
UserDefaults.standard.set(slider.value, forKey: .kPadding)
registerCell()
tableView.reloadData()
}
func registerCell(){
tableView.register(Cell.self, forCellReuseIdentifier: .cellId)
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
100
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: .cellId) as! Cell
cell.label.text = "Hello \(indexPath.row)"
return cell
}
}
class Cell: UITableViewCell {
let label = UILabel()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
label.font = UIFont.systemFont(ofSize: 34, weight: .bold)
contentView.addSubview(label)
label.snp.makeConstraints { make in
make.edges.equalToSuperview().inset(UserDefaults.standard.float(forKey: .kPadding))
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
However, this doesn't seem to be the case. It continues to use the previous padding and old entry is not replaced.
Am I misunderstanding what Apple's documentation is saying?
EDIT - see the note at the end of this answer...
While the posted example code is a bad approach to begin with, it does appear that the documentation is not correct. Or, it's not at all clear what is supposed to happen.
Let's look at this example (NOT using SnapKit)...
We'll create a "Base" cell class:
class CellBase: UITableViewCell {
let label = UILabel()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
label.font = UIFont.systemFont(ofSize: 20, weight: .regular)
label.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(label)
let g = contentView.layoutMarginsGuide
let h = label.heightAnchor.constraint(equalToConstant: 44.0)
h.priority = .required - 1
NSLayoutConstraint.activate([
label.topAnchor.constraint(equalTo: g.topAnchor),
label.leadingAnchor.constraint(equalTo: g.leadingAnchor),
label.trailingAnchor.constraint(equalTo: g.trailingAnchor),
label.bottomAnchor.constraint(equalTo: g.bottomAnchor),
h,
])
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
and two subclasses - CellA
and CellB
(different colors to make it easy to see which one is being used):
class CellA: CellBase {
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
label.backgroundColor = .yellow
contentView.backgroundColor = .systemYellow
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class CellB: CellBase {
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
label.backgroundColor = .cyan
contentView.backgroundColor = .systemBlue
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
By subclassing the cells, we can use a cellForRowAt
without without if/else:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cellBase") as! CellBase
cell.label.text = "Row: \(indexPath.row) - use Cell A: \(useCellA)"
return cell
}
If we register CellA
like this:
tableView.register(CellA.self, forCellReuseIdentifier: "cellBase")
a typical layout will look like this:
Or, if we register CellB
like this:
tableView.register(CellB.self, forCellReuseIdentifier: "cellBase")
we get this output:
So, let's add a button at the bottom to toggle the registration:
class ChangeCellRegVC: UIViewController, UITableViewDataSource {
let tableView = UITableView(frame: .zero, style: .plain)
let btn = UIButton()
var useCellA: Bool = true
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
btn.setTitle("Switch to Cell B", for: [])
btn.backgroundColor = .systemRed
btn.setTitleColor(.white, for: .normal)
btn.setTitleColor(.lightGray, for: .highlighted)
btn.addTarget(self, action: #selector(btnTap(_:)), for: .touchUpInside)
tableView.translatesAutoresizingMaskIntoConstraints = false
btn.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(tableView)
view.addSubview(btn)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
tableView.topAnchor.constraint(equalTo: g.topAnchor),
tableView.leadingAnchor.constraint(equalTo: g.leadingAnchor),
tableView.trailingAnchor.constraint(equalTo: g.trailingAnchor),
btn.topAnchor.constraint(equalTo: tableView.bottomAnchor, constant: 20.0),
btn.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 40.0),
btn.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -40.0),
btn.bottomAnchor.constraint(equalTo: g.bottomAnchor),
])
tableView.register(CellA.self, forCellReuseIdentifier: "cellBase")
tableView.dataSource = self
}
@objc func btnTap(_ sender: UIButton) {
useCellA.toggle()
if useCellA {
print("register CellA")
tableView.register(CellA.self, forCellReuseIdentifier: "cellBase")
} else {
print("register CellB")
tableView.register(CellB.self, forCellReuseIdentifier: "cellBase")
}
tableView.reloadData()
// update button title
btn.setTitle("Switch to Cell \(useCellA ? "B" : "A")", for: [])
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
100
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cellBase") as! CellBase
cell.label.text = "Row: \(indexPath.row) - use Cell A: \(useCellA)"
return cell
}
}
On launch, it looks like this:
If we tap the "Switch to..." button, we'll call tableView.register(...)
again, swapping between CellA
and CellB, and call
reloadData()` (relevant code):
@objc func btnTap(_ sender: UIButton) {
useCellA.toggle()
if useCellA {
print("register CellA")
tableView.register(CellA.self, forCellReuseIdentifier: "cellBase")
} else {
print("register CellB")
tableView.register(CellB.self, forCellReuseIdentifier: "cellBase")
}
tableView.reloadData()
// update button title
btn.setTitle("Switch to Cell \(useCellA ? "B" : "A")", for: [])
}
So, what do we get on button tap?
Clearly, the cells have been reloaded as "true" has changed to "false" ... but they are still dequeuing CellA
!
So, let's scroll up a few rows:
The table view had to create 3 new cells on .dequeueReusableCell
, using CellB
class as expected... but starting at "Row: 14" it's back to re-using CellA
!
If we keep scrolling:
we see that the table view now has 11 CellA
instances and 3 CellB
instances in its "reusable queue" ... and it doesn't matter which class we have currently registered.
Note also... the docs state:
You may specify nil for cellClass if you want to unregister the class from the specified reuse identifier.
But, if we do the initial load, the execute this code:
tableView.register(nil as UINib?, forCellReuseIdentifier: "cellBase")
tableView.reloadData()
cellForRowAt
will be called for each of the visible cells and they will still update!
If we then try to scroll, the app will crash in cellForRowAt
with "Fatal error: Unexpectedly found nil while unwrapping an Optional value"
The crash on scroll makes sense, even though the reloading of already queued cells does not.
EDIT
As pointed out in the comments by Paulw11, it seems the key point is the first phrase in the Discussion section of that doc page:
Prior to dequeueing any cells...
So, if you have already dequeued any cell with that Identifier, ignore everything in the Discussion section after those 5 words.