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:
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:
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!