0

I am using a variation of the technique mentioned in this post to add and remove table view cells dynamically.

Initially, the table view cells looks like this:

enter image description here

Then, I add a new cell to section 1. Section 1 is the section above the "RESULTS" section. So I expect the new cell to appear below the cell with the name "h". But no! It turns into this!

enter image description here

The new cell is added in section 2 (The "RESULTS" section) and is added below the cell with the name "b". What's even more surprising is that the second cell in section 2 has disappeared!

Here is how I add the cell:

I have an array of cells here:

var cells: [[UITableViewCell]] = [[], [], []]

each subarray in the array represents a section. In viewDidLoad, I added some cells to sections 0 to 2 by calling:

addCellToSection(1, cell: someCell)

addCellToSection is defined as

func addCellToSection(section: Int, cell: UITableViewCell) {
    cells[section].append(cell)
    tableView.insertRowsAtIndexPaths([NSIndexPath(forRow: cells[section].endIndex - 1, inSection: section)], withRowAnimation: .Top)
}

And the table view data source methods are defined in the same way as the aforementioned post.

I tried to print the number of cells in each section when I add the cell:

print("no. of rows in section 1: \(self.tableView(tableView, numberOfRowsInSection: 1))")
print("no. of rows in section 2: \(self.tableView(tableView, numberOfRowsInSection: 2))")

And the printed values are consistent i.e. when I add a new cell, the no. of rows increase by 1. But the weird thing is that it keeps placing rows in the wrong position.

Extra info: how I create the cell:

I first dequeue the cells from the prototype cells. I then call viewWithTag to get the text fields that are in the cell and add them to a [(UITextField, UITextField)]. Don't know whether this matters.

Community
  • 1
  • 1
Sweeper
  • 213,210
  • 22
  • 193
  • 313
  • 1
    Do not abuse the view as model. – vadian Jul 11 '16 at 09:53
  • Yeah... I know that. But I can't think of a model for my text fields. I need to track the text in the text field y'know. Maybe an enum with associated values will work. But how is this related to the weird behavior? @vadian – Sweeper Jul 11 '16 at 09:55
  • You can track the text fields with callbacks for example a closure in `cellForRowAtIndexPath` assigned to a property in the custom cell and called in `textFieldDidEndEditing`. – vadian Jul 11 '16 at 10:35

1 Answers1

1

Okay so first of all, you should never store UITableView cells in some custom collection. This is and should be done by iOS, not you.

The data you are using to populate the cells are stored in some model I presume?

Your tableView should register cells using either: func registerClass(cellClass: AnyClass?, forCellReuseIdentifier identifier: String)

or

func registerNib(nib: UINib?, forCellReuseIdentifier identifier: String)

or using Prototype cells in the Xib/Storyboard.

I recommend this setup, or similar:

class MyModel {
        /* holds data displayed in cell */
        var name: String?
        var formula: String?
        init(name: String, formula: String) {
            self.name = name
            self.formula = formula
        }
    }

class MyCustomCell: UITableViewCell, UITextFieldDelegate {
    static var nibName = "MyCustomCell"

    @IBOutlet weak var nameTextField: UITextField!
    @IBOutlet weak var formulaTextField: UITextField!
    weak var model: MyModel?

    override func awakeFromNib() {
        super.awakeFromNib()
        nameTextField.delegate = self
        formulaTextField.delegate = self
    }

    func updateWithModel(model: MyModel) {
        /* update labels, images etc in this cell with data from model */
        nameTextField.text = model.name
        formulaTextField.text = model.formula
        self.model = model
    }

    /* This code only works if MyModel is a class, because classes uses reference type, and the value
     of the name and formula properies are changed in the model stored in the dictionary */
    func textFieldShouldEndEditing(textField: UITextField) -> Bool {
        let newText = textField.text
        switch textField {
        case nameTextField:
            model?.name = newText
        case formulaTextField:
            model?.formula = newText
        default:
            print("Needed by compiler..")
        }
    }
}

class MyController: UIViewController, UITableViewDataSource, UITableViewDelegate {

    @IBOutlet weak var tableVieW: UITableView!

    override func viewDidLoad() {
        super.viewDidLoad()
        /* This is not needed if you are using prototype cells in the Xib or Storyboard. 
         Convenient to use nib name as cell identifier */
        tableVieW.registerNib(UINib(nibName: MyCustomCell.nibName, bundle: nil), forCellReuseIdentifier: MyCustomCell.nibName)
        tableVieW.delegate = self
        tableVieW.dataSource = self
    }

    private var dictionaryWithModelsForSection: Dictionary<Int, [MyModel]>!

    func numberOfSectionsInTableView(tableView: UITableView) -> Int {
        let sectionCount = dictionaryWithModelsForSection.keys.count
        return sectionCount
    }

    func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        guard let models: [MyModel] = modelsForSection(section) else {
            return 0
        }
        let rowCount = models.count
        return rowCount
    }

    private func modelsForSection(section: Int) -> [MyModel]? {
        guard section < dictionaryWithModelsForSection.count else {
            return nil
        }
        let models = dictionaryWithModelsForSection[section]
        return models
    }

    private func modelAtIndexPath(indexPath: NSIndexPath) -> MyModel? {
        guard let models = modelsForSection(indexPath.section) where models.count > indexPath.row else {
            return nil
        }
        let model = models[indexPath.row]
        return model
    }


    func addRowAtIndexPath(indexPath: NSIndexPath, withModel model: MyModel) {
        add(model: model, atIndexPath: indexPath)
        tableVieW.insertRowsAtIndexPaths([indexPath], withRowAnimation: .None)
    }

    private func add(model model: MyModel, atIndexPath indexPath: NSIndexPath) {
        guard var models = modelsForSection(indexPath.section) where indexPath.row <= models.count else { return }
        models.insert(model, atIndex: indexPath.row)
        dictionaryWithModelsForSection[indexPath.section] = models
    }

    func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCellWithIdentifier(MyCustomCell.nibName, forIndexPath: indexPath)
        return cell
    }

    func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
        guard let
            cell = cell as? MyCustomCell,
            model = modelAtIndexPath(indexPath) else { return }
        cell.updateWithModel(model)
    }
}

If you want to insert a cell you can use the method addRowAtIndexPath:withModel i wrote in MyController above, you need to call that from some function creating the corresponding model...

Sajjon
  • 8,938
  • 5
  • 60
  • 94
  • But then how do I keep track the text in the text fields in the cell? Do I put the text fields in the model? That sounds weird because mixing views and models is not a good idea right? Also, why can this cause the weird behavior? – Sweeper Jul 11 '16 at 10:11
  • @Sweeper just save all data you need in your model :), you are showing to texts, so have two string properties in your model, then set the labels based on those values in "updateWithModel" – Sajjon Jul 11 '16 at 10:28
  • I have updated my answer to include the labels in the cell and data in the model – Sajjon Jul 11 '16 at 10:37
  • @Sajjon The OP uses editable text fields which makes it a bit more difficult to maintain the model. – vadian Jul 11 '16 at 10:43
  • @vadian okay, so the labels should be changed to UITextFields and then the cells should implement UITextFieldDelegate and then you can implement your own delegate to propagate the changes back to the UITableViewDataSource and set the data in the model. – Sajjon Jul 11 '16 at 10:45
  • @Sweeper did you get that, what I meant with the delegate pattern? – Sajjon Jul 11 '16 at 10:46
  • @Sweeper I have updated my answer to using textField and setting the data in the model, which works since the model is a class, **wont work if MyModel is a struct.** – Sajjon Jul 11 '16 at 11:15
  • @Sweeper that's great! :) Hmm actually I am unsure regarding the model property in the Cell, I think it should be weak.... Does it work when it is weak? Would be strange if the cell holds a strong reference to the model, it should be owned by the Dictionary in the UITableViewDatasource class (maybe that is your UIViewController as in my example)... – Sajjon Jul 11 '16 at 13:06
  • I am now writing code. I'll tell you when I'm finished. – Sweeper Jul 11 '16 at 13:07
  • The code is too hard for my little brain to understand. I can understand the model part, but the code in the table view delegate/data source is a lot more complicated. I think I need to take a rest now. Maybe I can figure this out tomorrow. Thanks for the advice anyways! :) – Sweeper Jul 11 '16 at 13:27
  • @Sweeper You can do it! The GUI is beautiful, you are almost there, you are a ⭐️! Okay so the dictionary, in my example called * dictionaryWithModelsForSection* contains all the data/info your TableView (via the UITableViewDataSource) needs. You know how dictionaries work right? It contains Key-Value, the Key is the sectionIndex (Int) and the value is a list of models, one model for each row in that section. So the number of sections in your tableView is equal to the number of key-values in the dictionary. And the number of cells (rows) for a given section is equal to the model list count. – Sajjon Jul 11 '16 at 13:31