0

I created expandable UITableViewCell class that modifies constaint to achieve the effect.

import UIKit

class ExpandableCell: UITableViewCell {

@IBOutlet weak var img: UIImageView!


@IBOutlet weak var imgHeightConstraint: NSLayoutConstraint!


var isExpanded:Bool = false
    {
    didSet
    {
        if !isExpanded {
            self.imgHeightConstraint.constant = 0.0

        } else {
            self.imgHeightConstraint.constant = 128.0
        }
    }
}

}

View Controller

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

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell:ExpandableCell = tableView.dequeueReusableCell(withIdentifier: "ExpandableCell") as! ExpandableCell
    cell.img.image = UIImage(named: imgs[indexPath.row])
    cell.isExpanded = false
    return cell
}

// TableView Delegate methods
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    print(indexPath.row)
    guard let cell = tableView.cellForRow(at: indexPath) as? ExpandableCell
        else { return }

        UIView.animate(withDuration: 0.3, animations: {
            let offset: CGPoint = self.tableView.contentOffset

            tableView.beginUpdates()
            cell.isExpanded = !cell.isExpanded

            self.tableView.layer.removeAllAnimations()
            self.tableView.setContentOffset(offset, animated: false)
            tableView.scrollToRow(at: indexPath, at: UITableViewScrollPosition.top, animated: true)
            tableView.endUpdates()



        })

    }

func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
    guard let cell = tableView.cellForRow(at: indexPath) as? ExpandableCell
        else { return }
    UIView.animate(withDuration: 0.3, animations: {
        tableView.beginUpdates()
        cell.isExpanded = false
        tableView.endUpdates()
    })
}

Everything works fine for the few first rows, after scroll down, cells are not behaving properly, seems like it scrolls to the top, then the image dissapears.

I know it might have something to do with not storing boolean isExpanded value in some array to keep track of it, but nonetheless I guess it's something wrong with refreshing the TableView.

enter image description here

EDIT

Sulthan answer helped me a lot, but the problem with scrolling to the top especially when tapping the lowest cells keeps persisting.

Tried this UITableView scrolls to top when reloading cells with changing cell heights

with estimatedRowHeight to the closest value but it does not solve my issue in case of expandable cells.

Anybody got an idea how to fix it?

Community
  • 1
  • 1
theDC
  • 6,364
  • 10
  • 56
  • 98
  • Cells are reused. You should save the `isExpanded` outside your views, into your model. E.g. create a `Set` with expanded rows. – Sulthan Jan 21 '17 at 09:46
  • create a cell model (struct or class) for each of the table cell and store the imagename and if it is expanded in this model and then in the array. then it is in one place. – muescha Jan 21 '17 at 09:55

1 Answers1

1

Start by saving the expanded cells in your controller:

var expandedRows = Set<Int>()

That use them:

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell:ExpandableCell = tableView.dequeueReusableCell(withIdentifier: "ExpandableCell") as! ExpandableCell
    cell.img.image = UIImage(named: imgs[indexPath.row])

    // this!
    cell.isExpanded = self.expandedRows.contains(indexPath.row)
    return cell
}

And when changing the expanded state:

if self.expandedRows.contains(indexPath.row) {
    self.expandedRows.remove(indexPath.row)
    cell.isExpanded = false
} else {
    self.expandedRows.add(indexPath.row)
    cell.isExpanded = true
}

Of course, there is another possibility. Since your cells are expanded when they are selected, you can just react to that event in your cell:

class ExpandableCell : UITableViewCell {
    override func setSelected(_ selected: Bool, animated: Bool) {
        super.setSelected(selected, animated: animated)

        self.imgHeightConstraint.constant = selected ? 128.0 : 0.0
    }
}

Full example code:

class ExpandableCell : UITableViewCell {
    var label: UILabel?
    var expandableView: UIView?
    var heightConstraint: NSLayoutConstraint?

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)

        self.initViews()
    }

    override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)

        self.initViews()
    }

    private func initViews() {
        self.contentView.clipsToBounds = true

        let label = UILabel()
        label.autoresizingMask = [.flexibleWidth, .flexibleBottomMargin]
        label.frame = CGRect(x: 0, y: 0, width: self.contentView.frame.size.width, height: 44)

        self.label = label
        self.contentView.addSubview(label)

        let expandableView = UIView()
        expandableView.translatesAutoresizingMaskIntoConstraints = false
        self.contentView.addSubview(expandableView)

        let horizontalConstraints = NSLayoutConstraint.constraints(
            withVisualFormat: "H:|-0-[view]-0-|", options: [], metrics: nil, views: ["view": expandableView]
        )
        NSLayoutConstraint.activate(horizontalConstraints)

        let verticalConstraints = NSLayoutConstraint.constraints(
            withVisualFormat: "V:|-44-[view]-0@999-|", options: [], metrics: nil, views: ["view": expandableView]
        )
        NSLayoutConstraint.activate(verticalConstraints)

        let heightConstraint = NSLayoutConstraint(
            item: expandableView, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: 0
        )
        NSLayoutConstraint.activate([heightConstraint])

        self.expandableView = expandableView
        self.heightConstraint = heightConstraint
    }

    var isExpanded:Bool = false {
        didSet {
            self.heightConstraint?.constant = self.isExpanded ? 128 : 0
        }
    }
}

class ViewController: UITableViewController {
    var expandedRows: Set<Int> = Set()

    override func viewDidLoad() {
        super.viewDidLoad()

        self.tableView.register(ExpandableCell.self, forCellReuseIdentifier: "ExpandableCell")
        self.tableView.rowHeight = UITableViewAutomaticDimension
        self.tableView.allowsMultipleSelection = false
    }

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

    override func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
        return 44.0
    }

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

        cell.label!.text = "Cell at row: \(indexPath.row)"
        cell.isExpanded = expandedRows.contains(indexPath.row)
        return cell
    }

    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        print("Selecting: \(indexPath.row)")
        self.expandedRows.insert(indexPath.row)

        if let cell = tableView.cellForRow(at: indexPath) as? ExpandableCell {
            cell.isExpanded = true
        }

        self.tableView.beginUpdates()
        self.tableView.endUpdates()
    }

    override func tableView(_ tableView: UITableView, didDeselectRowAt indexPath: IndexPath) {
        print("Deselecting: \(indexPath.row)")
        self.expandedRows.remove(indexPath.row)

        if let cell = tableView.cellForRow(at: indexPath) as? ExpandableCell {
            cell.isExpanded = false
        }

        self.tableView.beginUpdates()
        self.tableView.endUpdates()
    }
}
Sulthan
  • 128,090
  • 22
  • 218
  • 270
  • First approach does not animate correctly, could you try it out and tell me if it works for you? Second approach does not animate at all. Anyway thanks for your help, still could try it out and possibly fix the issues? – theDC Jan 21 '17 at 15:43
  • @DCDC How are you starting the animation? It should be enough to call `tableView.beginUpdates()` followed by `tableView.endUpdates()`. – Sulthan Jan 21 '17 at 15:45
  • It almost works, but anytime I tap a cell that is below the screen(one have to scroll down to it) it gets back to the top of tableView, maybe it's something wrong with reshreshing tableView, could you try it out? – theDC Jan 21 '17 at 15:55
  • @DCDC Well, if your table is not set to "multiselect" mode, selecting a cell will deselect, that is, collapse all the other cells. That could cause the jump. – Sulthan Jan 21 '17 at 15:59
  • I enabled multiselection - it does not solve the issue – theDC Jan 21 '17 at 16:04
  • @DCDC I have added full example code, it works both with multiselection and single selection. – Sulthan Jan 21 '17 at 19:35
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/133724/discussion-between-dcdc-and-sulthan). – theDC Jan 21 '17 at 21:01
  • Hi @DCDC Could you solve this problem with a different method than the accepted answer? I havent been able to sort it myself :( – Juan Felipe Gallo Sep 27 '17 at 02:11