Aovib Aovib - 2 months ago 7
Swift Question

(Swift) How to pass data back and forth between UITableView and UITableViewCell?

I'm a beginning iOS developer and I've been stuck on an issue for quite some time now.

Background:
I have a single viewcontroller in which I have placed a TableView (I cannot use a tableviewcontroller). It holds 4 dynamic prototype cells:
Cell 1 has an UITextField and a couple of labels, Cell 2-4 only have a label (with different types of information) and have to be hidden initially.

When an user enters a number (max. 3 digits) in the UITextField of the first cell, the number has to be compared to check if it is a correct/existing number. If this number proves correct, 2 things will have to happen: 1) the labels in the first cell will need to change layout (colour, font, size, ...) and set the data of one label. 2) the other cells in the tableview will have to appear beneath the first cell.

Problem:
I'm having trouble sending data back and forth between the first UITableViewCell and its UITableView. For the text input I use the shouldChangeCharactersInRange method in the cell class, which then limits the # of digits and calls a method in the tableView class where the data will be compared. For this I used delegation.

However, after checking whether the number is a match, I need to call a method in the cell class (from within the tableview class) that will change the layout and set the data of a label in the cell. Yet I can't seem to figure out a way to make this method call work and access the outlets.

Summarised: cell class sends number to tableview class, methods in tableview class run, tableview class sends bool to cell class where outlets need to be changed.

What I tried:
I tried setting up delegation in the other direction, but it wouldn't trigger. Using a normal method call wouldn't work either, because then the outlets are nil.

I believe my problem lies in the fact that I need to reference the same instance/object of the cell to access the outlets?

I selected and simplified the relevant pieces of code:

1) TableView class

class TableViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, updateUITableView {

var existingNumber = 200
var cellsRowHeight = false

@IBOutlet var tableView: UITableView!

//Started by InputTableViewCell when UITextField has a 3 digit-number
func checkNumberInput(inputNumber: Int) {
//Step 1: check if the number matches an existing one
let match = checkNumberMatch(inputNumber: inputNumber)

//Step 2: send a bool back to the cell class to change the layout through outlets
InputTableViewCell().changeLayout(numberMatch: match) // <--- problem

//Step 3: make the hidden cells appear
toggleCellsVisibility(numberMatch: match)
}

//Step 1
func checkNumberMatch(inputNumber: Int) -> Bool {
if inputNumber == existingNumber {
return true
} else {
return false
}
}

//Step 2
func toggleCellsVisibility(numberMatch: Bool) {
cellsRowHeight = numberMatch
if numberMatch == true { //cells appear
tableView?.beginUpdates()
tableView?.endUpdates()
//possible additional code
} else { //cells dissappear
tableView?.beginUpdates()
tableView?.endUpdates()
//possible additional code
}
}

//Step 3
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
switch (indexPath.row) {
case 0 where !cellsRowHeight || cellsRowHeight:
return 170
case 1 where !cellsRowHeight:
return 0
case 1 where cellsRowHeight:
return 54
//cases for other cells to appear/dissappear
default:
return 44
}
}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.row == 0 {
//Create tableViewCell
let cellIdentifier = "InputCell"
let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as! InputTableViewCell

cell.delegate = self

//Customize lay-out of cell
cell.preservesSuperviewLayoutMargins = false
cell.separatorInset = UIEdgeInsets.zero
cell.layoutMargins = UIEdgeInsets.zero

return cell
}
//creation of other cells
}

// MARK: - Loading
override func viewDidLoad() {
super.viewDidLoad()
tableView.tableFooterView = UIView(frame: CGRect.zero)
}}


2) Cell class:

protocol updateUITableView: class {
func checkNumberInput(inputNumber: Int)
}

class InputTableViewCell: UITableViewCell, UITextFieldDelegate {
var delegate: updateUITableView?

@IBOutlet var textField: UITextField!
@IBOutlet var letterLabel: UILabel!

//Step 2: problem!
func changeLayout(numberMatch: Bool) {
if numberMatch == true {
print("Success") //this line triggers
letterLabel?.text = "A" //is nil
//other lay-out changes
}
else {
print("Fail, please provide an existing number")
//other lay-out changes
}
}

//Set maximum character limit in textField and dismiss keyboard when character limit is reached.
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
let currentCharacterCount = textField.text?.characters.count ?? 0
let newLength = currentCharacterCount + string.characters.count - range.length

if (newLength == 3) {
textField.text = (textField.text! as NSString).replacingCharacters(in: range, with: string)

//Get text from textField
let numberInput: Int? = Int(textField.text!)
if numberInput != nil {
delegate?.checkNumberInput(number: numberInput!) //send number to tableview class
}
//Dismiss keyboard
textField.resignFirstResponder()

if (range.length + range.location > currentCharacterCount) {
return false
} else {
return true
}
}
return true
}

func viewDidLoad() {
}}


The actual problem is situated in "Step 2". Using this method I can perform the print statement, but the actual labels/outlets are nil because it is just a generic call.

Any help would be immensely appreciated! Thank you!

Answer

Pass your UITableViewCell instance in your delegate method like this

func checkNumberInput(senderCell: InputTableViewCell,inputNumber: Int)

and then you will be able to call any method on this cell's instance. So in your case.

func checkNumberInput(senderCell: InputTableViewCell,inputNumber: Int){
//...
   senderCell.changeLayout(numberMatch: match) 
//....
}

and you can call your method like

delegate?.checkNumberInput(senderCell: self, inputNumber: numberInput!)