How to get value from TextFields and append that value in DataModel in TableViewCell in Swift?

34 Views Asked by At

I have a TableView containing an Array of currencies. The array can have multiple elements, and TableView cell contains a TextField.

enter image description here

Users may or may not add prices for all the currencies. I have implemented textFieldDidEndEditing delegate method to get textField's value. This is my CellForRowAt:


struct PriceModel {
    var country : CountryListData?
    var price : String?
    var cancel : Bool?
    
    init(country: CountryListData? = nil, price: String? = nil, cancel: Bool? = nil) {
        self.country = country
        self.price = price
        self.cancel = cancel
    }
}


// MARK: - Variables
    var priceArray = [PriceModel]()
    var countryArray = [CountryListData]()
    var onSaveClick : (([PriceModel])->Void)?
    var textFieldValues: [IndexPath: String] = [:]



func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "PriceTableViewCell", for: indexPath) as! PriceTableViewCell
        let obj = self.countryArray[indexPath.row]
        cell.editPriceCodeLabel.text = obj.iso_code ?? ""
        cell.editPriceTextField.tag = indexPath.row
        cell.editPriceTextField.delegate = self
        if let value = self.textFieldValues[indexPath] {
            cell.editPriceTextField.text = value
            print("value --> \(value) at index --> \(indexPath.row)")
            let price = PriceModel(country: obj, price: value, cancel: true)
            print("price --> \(price)")
            self.priceArray.append(price)
            print("priceArray --> \(self.priceArray)")
        }
        
        cell.selectionStyle = .none
        return cell
    }

// MARK: - TextField Delegate
extension PriceScreen: UITextFieldDelegate {
    func textFieldDidEndEditing(_ textField: UITextField) {
        let indexPath = IndexPath(row: textField.tag, section: 0)
        self.textFieldValues[indexPath] = textField.text ?? ""
        print("self.textFieldValues --> \(self.textFieldValues)")
        self.retailPriceTableView.reloadRows(at: [indexPath], with: .automatic)
    }
}

I have a problem with my code. I noticed that if I do not dismiss the keyboard, the value from the text field does not get added to the self.textFieldValues[indexPath] variable. I am not sure if I am using the correct method to get the values. Can someone please help me or provide guidance? Any type of assistance would be greatly appreciated.

Thank You!

1

There are 1 best solutions below

0
DonMag On

Whether assigning the text field's .delegate or, using a more "Swifty" approach with a closure, you want to update your data as the field is being edited, rather than on textFieldDidEndEditing.

So, in your PriceTableViewCell we'll declare a closure:

var priceClosure: ((PriceTableViewCell, String) -> ())?

then, we'll add a target action to the text field:

textField.addTarget(self, action: #selector(didEdit(_:)), for: .editingChanged)

and, this selector/func:

@objc func didEdit(_ sender: UITextField) {
    // inform the controller that the text field text was edited
    priceClosure?(self, textField.text ?? "")
}

In your controller, in cellForRowAt, we'll assign the closure:

// add the closure
c.priceClosure = { [weak self] aCell, aStr in
    guard let self = self,
          let idx = self.tableView.indexPath(for: aCell)
    else { return }
    self.myData[idx.row].price = aStr
    self.updateTotal()
}

That closure will be called as the field is being edited ... no waiting for the user to tap into a different field or manually dismiss the keyboard.

Here is a quick complete example...


Simple label & text field cell class - with the closure:

class PriceTableViewCell: UITableViewCell {
    
    var priceClosure: ((PriceTableViewCell, String) -> ())?
    
    let label = UILabel()
    let textField = 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, textField].forEach { v in
            v.translatesAutoresizingMaskIntoConstraints = false
            contentView.addSubview(v)
        }
        let g = contentView.layoutMarginsGuide
        NSLayoutConstraint.activate([
            
            label.leadingAnchor.constraint(equalTo: g.leadingAnchor),
            label.centerYAnchor.constraint(equalTo: g.centerYAnchor),
            
            textField.topAnchor.constraint(equalTo: g.topAnchor),
            textField.trailingAnchor.constraint(equalTo: g.trailingAnchor),
            textField.bottomAnchor.constraint(equalTo: g.bottomAnchor),
            textField.widthAnchor.constraint(equalToConstant: 120.0),
            
        ])
        
        textField.borderStyle = .roundedRect
        textField.keyboardType = .decimalPad
        textField.addTarget(self, action: #selector(didEdit(_:)), for: .editingChanged)
    }
    @objc func didEdit(_ sender: UITextField) {
        // inform the controller that the text field text was edited
        priceClosure?(self, textField.text ?? "")
    }
    
}

Simplified PriceModel:

struct PriceModel {
    var name: String = ""
    var price: String = ""
}

Example controller class - with a "running total" label at the top:

class PriceScreenVC: UIViewController {
    
    var myData: [PriceModel] = [
        PriceModel(name: "A", price: ""),
        PriceModel(name: "B", price: ""),
        PriceModel(name: "C", price: ""),
        PriceModel(name: "D", price: ""),
    ]
    
    let runningTotalLabel = UILabel()
    let tableView = UITableView()
    
    override func viewDidLoad() {
        super.viewDidLoad()

        [runningTotalLabel, tableView].forEach { v in
            v.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(v)
        }
        let g = view.safeAreaLayoutGuide
        NSLayoutConstraint.activate([
            
            runningTotalLabel.topAnchor.constraint(equalTo: g.topAnchor, constant: 8.0),
            runningTotalLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 8.0),
            runningTotalLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -8.0),
            
            tableView.topAnchor.constraint(equalTo: runningTotalLabel.bottomAnchor, constant: 8.0),
            tableView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 8.0),
            tableView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -8.0),
            tableView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -8.0),
            
        ])
        
        tableView.register(PriceTableViewCell.self, forCellReuseIdentifier: "priceCell")
        tableView.dataSource = self
        tableView.delegate = self
        
        runningTotalLabel.textAlignment = .center
        runningTotalLabel.text = "Total: 0"
    }

    func updateTotal() {
        var t: Double = 0
        myData.forEach { p in
            if let val = Double(p.price) {
                t += val
            }
        }
        runningTotalLabel.text = "Total: \(t)"
    }
}


extension PriceScreenVC: UITableViewDataSource, UITableViewDelegate {
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return myData.count
    }
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let c = tableView.dequeueReusableCell(withIdentifier: "priceCell", for: indexPath) as! PriceTableViewCell
        
        c.label.text = myData[indexPath.row].name
        c.textField.text = myData[indexPath.row].price

        // add the closure
        c.priceClosure = { [weak self] aCell, aStr in
            guard let self = self,
                  let idx = self.tableView.indexPath(for: aCell)
            else { return }
            self.myData[idx.row].price = aStr
            self.updateTotal()
        }
        
        return c
    }
    
}

Looks like this when running:

enter image description here