Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to update each custom tableview cell data at the same time?

I am using Xcode 8.2.1 with Swift 3. I have one custom UITableViewCell containing a UITextView. When I type into the textView, the custom cell automatically grows. This part is working fine. I also want to update my other cells with the same data when I type in a specific cell. For example:

When I type text into the 5th cell, automatically enter the same data in the 11th, 15th and 18th cells. Those cells must be grown automatically as well. I've added data to the specific indexes of the array. But it doesn't get reflected in the tableView. How do I achieve this functionality?

Please go through following code I've implemented so far. This updateCellHeight method is a custom delegate method. This method is called when the user types in the textView.

func updateCellHeight(indexPath: IndexPath, comment: String) {

    let currentCellLyricVal = self.traduzioneArray[indexPath.row]["rics"]
    tempIndexArray.removeAll()

    for (index,element) in traduzioneArray.enumerated() {

        let lyricVal = element["rics"]
        if currentCellLyricVal == lyricVal {
            tempIndexArray.append(index)
        }
    }
    for index in tempIndexArray {
        self.traduzioneArray[index]["ione"] = comment
        self.lyricsTableView.beginUpdates()
        self.lyricsTableView.endUpdates()
    }
}

func textViewDidChange(_ textView: UITextView) {
    self.delegate.updateCellHeight(indexPath: self.cellIndexPath, comment: textView.text)
}
like image 279
IKKA Avatar asked Jun 12 '17 08:06

IKKA


2 Answers

I think the issue with automatic data entering is the fact that reloading of cells results in resignFirstResponder() call.

This seems quite logical as on data reloading you actually give up some cells and ask the table view for new ones through cellForRowAt:. The old ones might not be the same so they resignFirstResponder().

One way to update the text of a cell is to directly alter the cell contents. You can do this for text view contents. Unfortunately for cell height there is no way to directly alter the cell without triggering heightForRowAt:.

UPDATE2: There IS a solution for iOS 8+ combining direct cell manipulation and AutomaticDimension on the table view.
Working flawlessly for UITextView. Check updates below. This is how the final result looks like:

enter image description here

UPDATE1: Code example for direct cell manipulation added

Use simple model to hold table view data:

// Simple model, for the example only
class Model {
    // The value of the text field
    var value: String?
    // The type of the cell
    var type: CustomCell.CellType = .typeA

    init(value: String) {
        self.value = value
    }

    init(value: String, type: CustomCell.CellType) {
        self.value = value
        self.type = type
    }
}

// Init the model.
var tableData = [
    Model(value: "Cell Type A", type: .typeA),
    Model(value: "Cell Type B", type: .typeB),
    Model(value: "Cell Type B", type: .typeB),
    Model(value: "Cell Type A", type: .typeA),
    Model(value: "Cell Type B", type: .typeB)]

And delegate like this:

// Delegate to update visible cells in the table view when text field value change.
protocol TextChangeDelegate {
    func textChangedAtCell(_ cell: CustomCell, text: String?)
}

Then init your custom cell:

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

    // Will crash if identifier is not registered in IB
    let cell = tableView.dequeueReusableCell(withIdentifier: "customCell") as! CustomCell

    // For this to work you need to update the model on each text field text change
    cell.textValue.text = tableData[indexPath.row].value
    cell.index = indexPath.row
    cell.type = tableData[indexPath.row].type
    cell.textChangeDelegate = self

    return cell
}

In the Custom Cell:

class CustomCell: UITableViewCell {

    // The type of the cell. Cells with same type will be updated simultaneously.
    enum CellType {
        case typeA
        case typeB
    }

    // Very simple prototype cell with only one text field in it
    @IBOutlet weak var textValue: UITextField!

    // Default type
    var type = CellType.typeA
    // Index in the model. Not optional (or other special assumptions on initial value)
    // for simplicity of the example.
    var index = 0
    var textChangeDelegate: TextChangeDelegate?
}

extension CustomCell: UITextFieldDelegate {

    func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {

        // Assume we will return true. Any logic could appear here.
        // If we need to return false, don't call the delegate method.
        let result = true

        if result {
            let nsString = textField.text as NSString?
            let newString = nsString?.replacingCharacters(in: range, with: string)
            print("New: \(newString ?? "(nil)")")

            textChangeDelegate?.textChangedAtCell(self, text: newString)
        }

        return result
    }
}

This is how to implement the TextChangeDelegate in the view controller with direct cell access, i.e. without reloading data:

extension ViewController: TextChangeDelegate {

    func textChangedAtCell(_ cell: CustomCell, text: String?) {

        // Only visual update. Skip the cell we are currently editing.
        for visibleCell in tableView.visibleCells where visibleCell != cell {
            if let currentCell = visibleCell as? CustomCell,
                tableData[currentCell.index].type == cell.type {
                    currentCell.textValue.text = text
            }
        }

        // Update the model (including invisible cells too)
        for (index, data) in tableData.enumerated() where data.type == cell.type {
            tableData[index].value = text
        }

        // Avoid reloading here, as this will trigger resignFirstResponder and you will
        // have issues when jumping from text field to text field across cells.
    }
}

You now have simultaneous text update on all cells with same CellType. This example use UITextField (check UPDATE2 for UITextView).

UPDATE2 goes here. Cell with UITextView with simultaneous text AND height adjustment.

Use the model and controller from previous example (minor changes as you will use new cell). Utilize UITableViewCell and put UITextView inside. In AutoLayout set constraints to text view:

Add constraints for auto resize

Remember to disable scrolling and bounces to the text view (otherwise you will not have auto-height). Don't forget to link textView's delegate to the UITextViewCell subclass.

Then in the cell, use

func textViewDidChange(_ textView: UITextView) {
    self.textChangeDelegate?.textChangedAtCell(self, text: textView.text)
}

Then, super important, in the ViewController's viewDidLoad, add this two settings

tableView.estimatedRowHeight = 50
tableView.rowHeight = UITableViewAutomaticDimension

and register the cell:

tableView.register(UINib(nibName: "CustomTextViewCell", bundle: nil),
                   forCellReuseIdentifier: "customTextViewCell")

Finally, add this code to TextChangeDelegate implementation from UPDATE1 to do the actual height update trick:

func textChangedAtCell(_ cell: UITableViewCell, text: String?) {

    <code from UPDATE 1 goes here>

    tableView.beginUpdates()
    tableView.endUpdates()
}

Enjoy!

like image 172
kr45ko Avatar answered Oct 05 '22 23:10

kr45ko


Once you update the data set with the new data. Reload the particular cells with the updated data set.

func updateCellHeight(indexPath: IndexPath, comment: String) {
        DispatchQueue.main.async {
            let currentCellLyricVal = self.traduzioneArray[indexPath.row]["rics"]
            tempIndexArray.removeAll()

            for (index,element) in traduzioneArray.enumerated() {

                let lyricVal = element["rics"]
                //You have to skip the current cell. by checking index != indexPath.row
                if currentCellLyricVal == lyricVal, index != indexPath.row {
                    tempIndexArray.append(index)
                }
            }

            for index in tempIndexArray {
                self.traduzioneArray[index]["ione"] = comment

                let updatedIndexPath = IndexPath(row: index, section: indexPath.section)
                  if let visibleCellIndexPaths = tableView.indexPathsForVisibleRows  {
                       if visibleCellIndexPaths.contains(updatedIndexPath) {
                            tableView.reloadRows(at: [updatedIndexPath], with: .automatic)
                       }
                  }
            }
        }
    }

NOTE: If the modified data set cells are visible then only we have to reload the particular cells.

like image 29
Subramanian P Avatar answered Oct 05 '22 23:10

Subramanian P