quantityTextfields are getting ghost values (values that are not putted by user in text field) Even when only one text field is changed, I think this is because how TableView Cells are reused, but how to resolve it
Below is the declaration of quantityTextFields
private var quantityTextFields: [Int:[String: UITextField]] = [:] // Map material codes to text fields
extension CompetitorSaleThruVC: UITableViewDelegate, UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
selectedMaterials.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tblView.dequeueReusableCell(withIdentifier: "competitorCell", for: indexPath) as! CompetitorCell
let material = selectedMaterials[indexPath.row]
cell.quantityTextField.text = ""
cell.label.text = material.value
//quantityTextFields[material.value] = cell.quantityTextField
quantityTextFields[indexPath.row] = [material.value:cell.quantityTextField]
// cell.quantityTextField.text = textFieldValues[indexPath.row]
cell.quantityTextField.tag = indexPath.row
cell.selectionStyle = .none
cell.quantityTextField.addTarget(self, action: #selector(textFieldDidChange(_:)), for: .editingChanged)
cell.quantityTextField.delegate = self
return cell
if navigatedFromCompetitorDisplayCell == true && material.value == competitorMaterial {
cell.quantityTextField.text = String(compSaleQuantity)
}
}
}
Cells are reused ... trying to track a UI element (such as a text field) in a cell and handling its changes the way you are doing it is going to be prone to errors and "mis-matches."
A much better approach is to handle the text field editing in your cell class, and then use a "callback" closure to let the controller know the field has been edited.
So, we add a closure like this to the cell class:
class CompetitorCell: UITableViewCell, UITextFieldDelegate {
// "callback" closure
var quantityEdited: ((CompetitorCell, Int) -> ())?
In the cell class, we add the target and handle editing:
// in cell init
{
// action when field is edited
quantityTextField.addTarget(self, action: #selector(edited(_:)), for: .editingChanged)
}
@objc func edited(_ sender: UITextField) {
let str = sender.text ?? ""
// execute the "callback" closure
self.quantityEdited?(self, Int(str) ?? 0)
}
Then, in your controller in cellForRowAt
:
// set the "callback" closure
cell.quantityEdited = { [weak self] theCell, theValue in
// safely unwrap optionals
guard let self = self else { return }
guard let indexPath = self.tableView.indexPath(for: theCell) else { return }
// update our data
self.materials[indexPath.row].quantity = theValue
}
Here is a complete example - no @IBOutlet
or @IBAction
connections... just assign a blank view controller's custom class to CompTableVC
Simple Data Struct
struct Material {
var name: String = ""
var quantity: Int = 0
}
Cell Class - with label and text field
class CompetitorCell: UITableViewCell, UITextFieldDelegate {
// "callback" closure
var quantityEdited: ((CompetitorCell, Int) -> ())?
static let identifier: String = "CompetitorCell"
let label = UILabel()
let quantityTextField = UITextField()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() {
label.translatesAutoresizingMaskIntoConstraints = false
quantityTextField.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(label)
contentView.addSubview(quantityTextField)
let g = contentView.layoutMarginsGuide
NSLayoutConstraint.activate([
label.topAnchor.constraint(equalTo: g.topAnchor, constant: 0.0),
label.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
label.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
label.heightAnchor.constraint(equalToConstant: 42.0),
quantityTextField.topAnchor.constraint(equalTo: label.bottomAnchor, constant: 8.0),
quantityTextField.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
quantityTextField.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
quantityTextField.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0),
])
quantityTextField.borderStyle = .roundedRect
quantityTextField.keyboardType = .numberPad
// action when field is edited
quantityTextField.addTarget(self, action: #selector(edited(_:)), for: .editingChanged)
// so we can see the framing
label.backgroundColor = .cyan
quantityTextField.backgroundColor = .yellow
}
@objc func edited(_ sender: UITextField) {
let str = sender.text ?? ""
// execute the "callback" closure
self.quantityEdited?(self, Int(str) ?? 0)
}
}
Sample Controller Class
class CompTableVC: UIViewController, UITableViewDataSource, UITableViewDelegate {
var materials: [Material] = []
let tableView = UITableView()
let totalLabel = UILabel()
var doneButton: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor(white: 0.95, alpha: 1.0)
var cfg = UIButton.Configuration.filled()
cfg.title = "Done"
doneButton = UIButton(configuration: cfg, primaryAction: UIAction() { _ in
self.view.endEditing(true)
})
totalLabel.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(totalLabel)
doneButton.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(doneButton)
tableView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(tableView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
totalLabel.centerYAnchor.constraint(equalTo: doneButton.centerYAnchor, constant: 0.0),
totalLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
totalLabel.heightAnchor.constraint(equalTo: doneButton.heightAnchor),
doneButton.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
doneButton.leadingAnchor.constraint(equalTo: totalLabel.trailingAnchor, constant: 20.0),
doneButton.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
tableView.topAnchor.constraint(equalTo: doneButton.bottomAnchor, constant: 20.0),
tableView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 0.0),
tableView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: 0.0),
tableView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: 0.0),
])
totalLabel.backgroundColor = .systemRed
totalLabel.textColor = .white
tableView.register(CompetitorCell.self, forCellReuseIdentifier: CompetitorCell.identifier)
tableView.dataSource = self
tableView.delegate = self
// let's create some sample data
materials = (0..<20).map {
let formatter = NumberFormatter()
formatter.numberStyle = .spellOut
var m: Material = Material()
m.name = formatter.string(for: $0)!
m.quantity = 1
return m
}
// handle keyboard covering cells...
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name: UIResponder.keyboardWillShowNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide), name: UIResponder.keyboardWillHideNotification, object: nil)
// only show "Done" button if editing is active
doneButton.isHidden = true
// update the "Total" label
updateTotal()
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return materials.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: CompetitorCell.identifier, for: indexPath) as! CompetitorCell
// set the label and text field
cell.label.text = "Material: " + materials[indexPath.row].name
let q = materials[indexPath.row].quantity
cell.quantityTextField.text = q > 0 ? "\(q)" : ""
// set the "callback" closure
cell.quantityEdited = { [weak self] theCell, theValue in
// safely unwrap optionals
guard let self = self else { return }
guard let indexPath = self.tableView.indexPath(for: theCell) else { return }
// update our data
self.materials[indexPath.row].quantity = theValue
// update "Total" label
self.updateTotal()
}
return cell
}
func updateTotal() {
let t = materials.reduce(0) { $0 + $1.quantity }
totalLabel.text = "Total: \(t)"
}
@objc func keyboardWillShow(notification: NSNotification) {
guard let keyboardFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return }
// Get the size of the keyboard
let keyboardHeight = keyboardFrame.height
// Adjust the tableView's contentInset and scrollIndicatorInsets
let contentInsets = UIEdgeInsets(top: 0, left: 0, bottom: keyboardHeight, right: 0)
tableView.contentInset = contentInsets
tableView.scrollIndicatorInsets = contentInsets
// Scroll to the active text field if it's hidden by the keyboard
if let activeTextField = findActiveTextField(),
let cell = activeTextField.superview?.superview as? UITableViewCell,
let indexPath = tableView.indexPath(for: cell) {
tableView.scrollToRow(at: indexPath, at: .middle, animated: true)
}
self.doneButton.isHidden = false
}
@objc func keyboardWillHide(notification: NSNotification) {
// Reset the tableView's contentInset and scrollIndicatorInsets
let contentInsets = UIEdgeInsets.zero
tableView.contentInset = contentInsets
tableView.scrollIndicatorInsets = contentInsets
self.doneButton.isHidden = true
}
func findActiveTextField() -> UITextField? {
for cell in tableView.visibleCells {
if let textField = cell.contentView.subviews.compactMap({ $0 as? UITextField }).first(where: { $0.isFirstResponder }) {
return textField
}
}
return nil
}
}
Looks like this when running:
You'll be able to edit the "quantity" in each cell ... it will update the data source ... and you can scroll up and down without the "ghost values" problem.