2

I have a cell that contains a few stackviews, bottom stackView contains a textView and a custom separator.

my cell

I want to create an option, when user tap on cell, it shows whole text of tapped text view, so number of maximum lines in that cell is 0 and in other cells should be 3.

I used this tutorial http://www.roostersoftstudios.com/2011/04/14/iphone-uitableview-with-animated-expanding-cells/ and I modified it a little, my code:

func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCellWithIdentifier(iden_tableViewCell4) as! TableViewCell4


    if let selectedCellIP = selectedIndexPath {
        if indexPath == selectedCellIP {
            cell.textTextVIew.textContainer.maximumNumberOfLines = 0
        }
        else {
            cell.textTextVIew.textContainer.maximumNumberOfLines = textVIewMaxNumberOfLines
        }

    }
    else {
        cell.textTextVIew.textContainer.maximumNumberOfLines = textVIewMaxNumberOfLines
    }

    return cell
}

func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
    tableView.deselectRowAtIndexPath(indexPath, animated: true)

    //The user is selecting the cell which is currently expanded
    //we want to minimize it back
    if selectedIndexPath == indexPath {
        selectedIndexPath = nil
        tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)

        return
    }

    //First we check if a cell is already expanded.
    //If it is we want to minimize make sure it is reloaded to minimize it back
    if let selIndexPath = selectedIndexPath {

        let previousPath = selIndexPath
        selectedIndexPath = indexPath
        tableView.reloadRowsAtIndexPaths([previousPath], withRowAnimation: .Fade)

    }


    //Finally set the selected index to the new selection and reload it to expand
    selectedIndexPath = indexPath
    tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: .Fade)

}

in my viewDidLoad I set tableView.estimatedRowHeight = 160 tableView.rowHeight = UITableViewAutomaticDimension

Expansion and contraction work well, but textView height of other cells has a strange behavior. When first cell is extended, an I scroll down, the last is extended too, and should not be. enter image description here

Skyfox
  • 85
  • 1
  • 10
  • what does your heightForRowAtIndexPath look like? – jacob bullock Jan 25 '16 at 16:15
  • 1
    I don't use it because of `tableView.estimatedRowHeight = 160 tableView.rowHeight = UITableViewAutomaticDimension` – Skyfox Jan 25 '16 at 16:30
  • There shouldn't be a `heightForRowAtIndexPath` when using `estimatedRowHeight` + `UITableViewAutomaticDimension`. – SwiftArchitect Jan 25 '16 at 16:30
  • From another projects I know, that if in if-else statement in cellForRow something is set and in else branch it isn't unset, It can have strange behavior too, but I don't think this is my case, because I have all if else branches in cellForRow covered, or haven't I? – Skyfox Jan 25 '16 at 16:36

2 Answers2

1

Dynamically Resize UITableViewCell upon Selection

Playing with individual selection indexes is dangerous business. Not only are you likely to miss corner case conditions in didSelectRowAtIndexPath, it cannot possibly work for multiple selections.
You should split the cell expansion/compression notification into 2 distinct blocks (no need to keep track of selectedIndexPath, more robust code):

Expand

override func tableView(_ tableView: UITableView,
                        didSelectRowAt indexPath: IndexPath) {
    self.reloadRowsAtIndexPaths(tableView,
                                indexPath:indexPath)
}

Contract

override func tableView(_ tableView: UITableView,
                        didDeselectRowAt indexPath: IndexPath) {
    self.reloadRowsAtIndexPaths(tableView,
                                indexPath:indexPath)
}

Selection is destroyed by selectRowAtIndexPath

This prevents didDeselectRowAtIndexPath from ever being invoked. A workaround is to cache the entire selection, not individual indexes, and to restore such selection after reload.

Complete code:

Notice when and how cacheSelectedRows is maintained. This uses a plain UITableViewCell. All it needs is a reuseIdentifier.

class TableViewController: UITableViewController {
    var cacheSelectedRows:[IndexPath]? = nil

    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.estimatedRowHeight = 160
        tableView.rowHeight = UITableViewAutomaticDimension
    }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 8
    }

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "reuseIdentifier", for: indexPath)

        if let textLabel = cell.textLabel {
            textLabel.backgroundColor = UIColor.clear

            textLabel.numberOfLines = 1
            if let cacheSelectedRows = cacheSelectedRows {
                textLabel.numberOfLines = (cacheSelectedRows.contains(indexPath)) ? 0 : 1
            }
            textLabel.text = "\(1 + indexPath.row), Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
        }
        return cell
    }

    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        self.reloadRowsAtIndexPaths(tableView, indexPath:indexPath)
    }

    override func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
         self.reloadRowsAtIndexPaths(tableView, indexPath:indexPath)
    }

    func reloadRowsAtIndexPaths(_ tableView: UITableView, indexPath: IndexPath) {
        cacheSelectedRows = tableView.indexPathsForSelectedRows

        tableView.reloadRows(at: [indexPath], with: .fade)

        // Restore selection
        if let cacheSelectedRows = cacheSelectedRows {
            for path in cacheSelectedRows {
                self.tableView.selectRow(at: path, animated: false, scrollPosition: .none)
            }
        }
    }
}

Demo

Animated demo


► Find this solution on GitHub and additional details on Swift Recipes.

SwiftArchitect
  • 47,376
  • 28
  • 140
  • 179
  • Ok, let's say I split expansion/contraction into two distinct blocks, as you suggest (`didSelect` and `didDeselect`). My question now is, what about cellForRow, what should be my if statement to chose right number of max lines for my textView, because as you say I don't need track selectedIndexPath? I don't really see your goal, could you give me a hint? – Skyfox Jan 25 '16 at 18:20
  • Created project, added demo and link. – SwiftArchitect Jan 26 '16 at 21:10
0

I didn't find answer, why the strange behavior of textView and cell height is happening, but I have solution for my problem.

So it looks like tableView.estimatedRowHeight = 160 and tableView.rowHeight = UITableViewAutomaticDimension don't respect textView's maximum number of lines for all cells. cellForRow works fine, it sets the limitation, but the rows height is sometimes expanded and sometimes contracted.

So I unwrapped textView and the bottom separator from StackView, replaced TextView with Label and set autolayaut constraints. Now it works fine, without strange behavior.

Skyfox
  • 85
  • 1
  • 10