0

Background: A single ViewController contains 4 custom prototype cells. The first one is always visible and has a UITextField. Prototype cells 2, 3 and 4 contain an image, label and label respectively. However, prototype cell 4 is based on an array and can have 1 or multiple rows depending on the input in the first cell.

When a certain String is entered in the first cell, a check is made to see if an Object exists with that String in one of its properties. If the value is incorrect, the first cell changes height and lay-out. If the value is correct, the first cell changes height, and most importantly the 3 other prototype cells expand to show the details of the object that corresponds to the String input. If the user then enters another String that is incorrect, the cells collapse.

Problem: Animating this expand/collapse of the 3 other cells. I'm having trouble figuring out how to define the numberOfRowsInSection() method and the code block (step 2 in my code below) between beginUpdates() and endUpdates(). Even without the arraycells implementation, calling the reloadRows() after insertRows() does not seem to work.

What I tried:

  • reloadData() = correctly shows the data, but I cannot use it because it won't give the necessary animation.
  • beginUpdates() and endUpdates() without anything in between = gives the following error:

    Invalid update: invalid number of rows in section 0. The number of rows contained in an existing section after the update (6) must be equal to the number of rows contained in that section before the update (1), plus or minus the number of rows inserted or deleted from that section (0 inserted, 0 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out).'

I believe this is related to the fact that the last prototype cell is based on an array and I'm not reloading the actual data of the cells, just the view.

  • other combinations with insertRowsAtIndexPath, reloadRowsAtIndexPath, ... = give similar errors related to the number of rows.

Any help would be immensely appreciated!

Simplified Code:

class TableViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, updateUITableView {

    var people: [Person] = [
        Person(name: "name", image: "imagename1", description: "description1", children: ["name1","name2","name3"]),
        Person(name: "name2", image: "imagename3", description: "description2", children: ["name4", "name5", "name6"])
    ]

    enum Flag {
        case start
        case match
        case noMatch
    }
    var flag = Flag.start

    var globalObject: Person?

    @IBOutlet var tableView: UITableView!

    func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        switch (flag) {
        case .start :
            return 1
        case .noMatch:
            return 1
        case .match:
            return 3 + (globalObject?.children.count)! //PROBLEM!
        }
    }

    //START - Input received from tableViewCell
    func checkName(senderCell: SearchInputTableViewCell, name: String) {
        // STEP 1: Check if person exists
        let person = checkPersonNameMatch(nameInput: name)
        globalObject = person //save globally for cellForRowsAtIndexPath

        // STEP 2: Show/hide cells
        toggleCellsVisibility(person: person)
    }

    //STEP 1:
    func checkPersonNameMatch(nameInput: String) -> Person? {
        guard let index = people.index(where: {$0.name == nameInput}) else {
            flag = .noMatch
            return nil
        }
        let personFound = people[index]
        flag = .match
        globalObject = personFound
        return personFound
    }

    //STEP 2 = PROBLEM!
        func toggleCellsVisibility(person: Person?) {
    if person != nil { //Cells appear
        UIView.animate(withDuration: 0.7, animations: {

            self.tableView.beginUpdates()

            let indexPath: IndexPath = IndexPath(row: 1, section: 0) //for Image cell
            let indexPath2: IndexPath = IndexPath(row: 2, section: 0) //for Description cell
            //let indexPath3: IndexPath = IndexPath[....] //for array in Children cell

            self.tableView.insertRows(at: [indexPath], with: .bottom)
            self.tableView.insertRows(at: [indexPath2], with: .bottom)

            self.tableView.reloadRows(at: [indexPath], with: .bottom)
            self.tableView.reloadRows(at: [indexPath2], with: .bottom)
            //... a for-loop to reload the array cells here?

            self.tableView.endUpdates()
        })
    } else { //Cells disappear
        UIView.animate(withDuration: 0.4, animations: {
            self.tableView.beginUpdates()
            //... same code as above?
            self.tableView.endUpdates()
        })
    }
}

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        switch indexPath.row {
        case 0:
            let cellIdentifier = "InputCell"
            let inputCell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as! InputTableViewCell

            inputCell.delegate = self

            switch (flag) {
            case .start:
                self.tableView.rowHeight = 187
            case .match:
                self.tableView.rowHeight = 170
            case .noMatch:
                self.tableView.rowHeight = 200
            }

            return inputCell

        case 1:
            let cellIdentifier = "ImageCell"
            let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as! ImageTableViewCell

            cell.image?.image = UIImage(named: (globalObject?.image)!)

            switch (flag) {
            case .start:
                self.tableView.rowHeight = 0
            case .match:
                self.tableView.rowHeight = 170
            case .noMatch:
                self.tableView.rowHeight = 0
            }

            return cell

        case 2:
            let cellIdentifier = "DescriptionCell"
            let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as! DescriptionTableViewCell

            cell.description?.text = globalObject?.description

            switch (flag) {
            case .start:
                self.tableView.rowHeight = 0
            case .match:
                self.tableView.rowHeight = 54
            case .noMatch:
                self.tableView.rowHeight = 0
            }

            return cell

        default:
            let cellIdentifier = "ChildrenCell"
            let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) as! ChildrenTableViewCell

            cell.childName?.text = globalObject?.children[(indexPath as IndexPath).row - 3]

            switch (flag) {
            case .start:
                self.tableView.rowHeight = 0
            case .match:
                self.tableView.rowHeight = 44
            case .noMatch:
                self.tableView.rowHeight = 0
            }

            return cell
        }
    }
}
Aovib
  • 27
  • 7

2 Answers2

0

My suggestion would be: do not do any inserting or removing of these rows. Just keep the rows there all the time — with zero height (and clipsToBounds set to true). Thus they are invisible to the user until you need them. When you need them, just change the height(s) and call beginUpdates and endUpdates.

matt
  • 515,959
  • 87
  • 875
  • 1,141
  • I tried that approach, but it gave me the 'invalid number of rows' error as stated above. The difference between my situation and the one that you linked is the dependency of my data. The data (and number) of cells that appear/expand is based on the input in the first cell. So it's unclear to me how I can already define them at the start, when I don't know in advance how many there will be and what data they will contain. – Aovib Oct 28 '16 at 14:55
  • "when I don't know in advance how many there will be and what data they will contain" Okay, that's fair enough. But then you must always update your _model_ before you call `beginUpdates`. Basically this is no different from _any_ time you add a row. And in your case, as far as I can tell, there is still no need to do any _row_ insertions. – matt Oct 28 '16 at 14:56
  • So I would change your model and then reload the data with all the heights at 0. Now you've gotten through step one. Now change the heights and call `beginUpdates` and `endUpdates` as I said before. That's step two. Done! – matt Oct 28 '16 at 14:59
  • Thanks! I think I understand my problem more clearly now. But with only using begin/endUpdates() I still get that "invalid number of rows " error. I believe I have the height adjustments covered by the switch statements in cellForRowAtIndexPaths() already. So it must have something to do with updating the model as you mention. I thought the switch inside numberOfRowsInSection() combined with the if/else statement in toggleCellsVisibility would take care of the model update? Is the flag or globalObject variable used wrong somehow? – Aovib Oct 28 '16 at 16:35
0

I would suggest changing your data layout using enums, so that way, you don't have to worry about the number of rows, which you can calculate when you have the data. Example code as follows:

enum CellType {
    case textField
    case labelCell
    case array
}

In your tableView contain something related to the

class TableViewController: UITableViewController {
    var order = [CellType]()

    func calculateRowOrder() {
        // You can also customize which cells you want here, or if they're dynamic
        order = [.textField, .labelCell]

        // Add as many .array as child cells
        for _ in globalObject?.children ?? [] {
            order.append(.array)
        }
    }
}

And then you can more easily do the methods for the data source:

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return order.count
}

And the cell customization:

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let row = indexPath.row

    let cellType = order[row]

    switch cellType {
    case .textField:
        <Customize textField>
    case .label:
        <Customize label>
    case .array:
        // You can figure out which child element you're customizing
        <Customize array>
    }
}

Anytime you're data changes or you need to reload data, recalculate the order, and reload the data. Good luck.

Sealos
  • 562
  • 3
  • 15
  • When I implemented this in my code I got some strange results... Upon entering a value in the first cell and reloading the tableView with reloadData(), I only see the last couple of cells. When I then touch the screen anywhere and scroll, the screen suddenly and instantly shows all cells correctly. What could cause this glitch? Maybe it has something to do with the flag and the moment I need to call calculateRows()? I put calculateRows() in viewDidLoad() (to get the first cell upon opening the screen) and in toggleCellsVisibilty() to recalculate and get the new rows. – Aovib Oct 28 '16 at 21:01
  • In addition, while making the code a lot more readable and clean, it still does not fix the beginUpdates()/endUpdates() crash I have in my original post. I still get the same error message regarding "invalid number of rows". :/ – Aovib Oct 28 '16 at 21:03
  • You don't need to use beginUpdates()/endUpdates(), just tableView.reloadData() – Sealos Oct 29 '16 at 18:38