Expand UITableViewCell (custom) on tap - random extra cells get selected

In my project (UIKit, programmatic UI) I have a UITableView with sections. The cells use a custom class. On load all cells just show 3 lines of info (2 labels). On tap, all contents will be displayed. Therefor I've setup my custom cell class to have two containers, one for the 3 line preview and one for the full contents. These containers are added/removed from the cell's content view when needed when the user taps the cell by calling a method (toggleFullView) on the custom cell class. This method is called from the view controller in didSelectRowAt:

Code Block
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let annotation = annotationsController.getAnnotationFor(indexPath)
//Expandable cell
guard let cell = tableView.cellForRow(at: indexPath) as? AnnotationCell else { return }
cell.toggleFullView()
tableView.reloadRows(at: [indexPath], with: .none)
// tableView.reloadData()
}

Basically it works, but there are some issues:
  1. I have to double tap the cell for it to expand and again to make it collapse again. The first tap will perform the row animation of tableView.reloadRows(at: [indexPath], with: .none) and the second tap will perform the expanding. If I substitute reloadRows with tableView.reloadData() the expanding and collapsing will happen after a single tap! But that is disabling any animations obviously, it just snaps into place. How Do I get it to work with one tap?

  2. When the cell expands, some other random cells are also expanded. I guess this has something to do with reusable cells, but I have not been able to remedy this.

  3. I want to be the expanded cell to collapse once I tap another cell to expand, how do I perceive that?

My custom cell class (partly):
Code Block
[AnnotationCell full cell class](https://developer.apple.com/forums/content/attachment/caacbef0-2e20-4528-b86a-859d1ceb460f){: .log-attachment}
import UIKit
class AnnotationCell: UITableViewCell, SelfConfiguringAnnotationCell {
//MARK: - Actions
///Expand and collapse the cell
func toggleFullView() {
showFullDetails.toggle()
if showFullDetails {
//show the full version
if contentView.subviews.contains(previewDetailsView) {
previewDetailsView.removeFromSuperview()
}
if !contentView.subviews.contains(fullDetailsView) {
contentView.addSubview(fullDetailsView)
}
} else {
//show the preview version
if contentView.subviews.contains(fullDetailsView) {
fullDetailsView.removeFromSuperview()
}
if !contentView.subviews.contains(previewDetailsView) {
contentView.addSubview(previewDetailsView)
}
}
UIView.animate(withDuration: 1.2) {
self.layoutIfNeeded()
}
}


Would be great to at least no longer have multiple random cells expand on tap

Accepted Reply

Assuming your constraints / resizing mask can give UIKit enough info to correctly resize the cell, calling beginUpdates / endUpdates on the table view should cause it to resize any visible cells with animation. You shouldn’t actually need that animation block you’re currently using, I think.

Replies

Why are you reloading any rows? Don’t you just need to call setNeedsLayout on your cell’s content view? Or if you’re using auto layout you probably don’t even have to do that. Just adding/removing your subviews should work.
If I don't reload rows or reloadData, nothing happens ...?
Well, there’s probably something going on in the code you’re not showing. Perhaps you have a gesture recognizer or target/action set up somewhere that’s being attached to the cell or a subview but never removed when the cell is reused. Perhaps your toggle method is being called twice, causing it to appear as if nothing has changed.

A simpler, complete example (small enough to post all the code) would be easier to diagnose.
Thank you for your reply There's indeed a gesturerecognizer attached to the cell/tableview. How would that interfere?

Code Block Swift
   private func configureTableView() {
    tableView = UITableView(frame: view.bounds, style: .grouped)
    tableView.delegate = self
    tableView.dataSource = self
    tableView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
    tableView.backgroundColor = .clear
    view.addSubview(tableView)
     
    tableView.register(AnnotationCell.self, forCellReuseIdentifier: AnnotationCell.reuseIdentifier)
    tableView.register(AnnotationHeaderCell.self, forHeaderFooterViewReuseIdentifier: AnnotationHeaderCell.reuseIdentifier)
     
    //Dynamic sizing cells
    tableView.sectionHeaderHeight = 40
    tableView.rowHeight = UITableView.automaticDimension
    tableView.estimatedRowHeight = 70
     
    //Tap and hold to edit annotation
    let tapAndHold = UILongPressGestureRecognizer(target: self, action: #selector(tapAndHoldCell))
    tapAndHold.minimumPressDuration = 0.3
    tableView.addGestureRecognizer(tapAndHold)
  }

Code Block Swift
   @objc private func tapAndHoldCell(recognizer: UILongPressGestureRecognizer) {
    if recognizer.state == .ended {
      guard let indexPath = tableView.indexPathForRow(at: recognizer.location(in: self.tableView)) else { return }
      let annotation = annotationsController.getAnnotationFor(indexPath)
       
      let viewController = AnnotationDetailsViewController(withAnnotation: annotation)
      viewController.delegate = self
      navigationController?.pushViewController(viewController, animated: true)
    }
  }


Ok, now I got some action with .SetNeedslayout() It animates and changes back and forth with one click, BUT ...
The cell does not adjust its size to the content and stays the size of the first view (previewDetailsView). How could I make it so it sizes to fit the content ? (which it does with the reloadData way)

Code Block Swift
   ///Expand and collapse the cell
   func toggleFullView() {
     showFullDetails.toggle()
     
     if showFullDetails {
       //show the full version
       if contentView.subviews.contains(previewDetailsView) {
         previewDetailsView.removeFromSuperview()
       }
       if !contentView.subviews.contains(fullDetailsView) {
         contentView.addSubview(fullDetailsView)
       }
     } else {
       //show the preview version
       if contentView.subviews.contains(fullDetailsView) {
         fullDetailsView.removeFromSuperview()
       }
       if !contentView.subviews.contains(previewDetailsView) {
         contentView.addSubview(previewDetailsView)
       }
     }
    //Invalidate the layout
    contentView.setNeedsLayout()
     
     UIView.animate(withDuration: 1.2) {
      self.layoutIfNeeded()
     }
   }


Assuming your constraints / resizing mask can give UIKit enough info to correctly resize the cell, calling beginUpdates / endUpdates on the table view should cause it to resize any visible cells with animation. You shouldn’t actually need that animation block you’re currently using, I think.
First and foremost, thank you very much for taking the time and effort to answer my questions. This is my first project and some things are hard to find an answer for.

It now works beautifully, here is the working code for those who might experience the same problems.

in my ViewController
Code Block Swift
   func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    //Expandable cell
    guard let cell = tableView.cellForRow(at: indexPath) as? AnnotationCell else { return }
    cell.toggleFullView()
    tableView.beginUpdates()
    tableView.endUpdates()
  }

and the reworked toggleMethod:
Code Block Swift
   ///Expand and collapse the cell
  func toggleFullView() {
    //Show the full contents
    print("ShowFullDetails = \(showFullDetails.description.uppercased())")
    if showFullDetails {
      print("Show full contents")
      if contentView.subviews.contains(previewDetailsView) {
        previewDetailsView.removeFromSuperview()
      }
      if !contentView.subviews.contains(fullDetailsView) {
        contentView.addSubview(fullDetailsView)
      }
      NSLayoutConstraint.activate([
        fullDetailsView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: padding),
        fullDetailsView.leadingAnchor.constraint(equalTo: checkmarkView.trailingAnchor, constant: padding),
        fullDetailsView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -2 * padding),
        fullDetailsView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -padding)
      ])
    //Show preview contents
    } else {
      print("Show preview contents")
      if contentView.subviews.contains(fullDetailsView) {
        fullDetailsView.removeFromSuperview()
      }
      if !contentView.subviews.contains(previewDetailsView) {
        contentView.addSubview(previewDetailsView)
      }
      NSLayoutConstraint.activate([
        previewDetailsView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: padding),
        previewDetailsView.leadingAnchor.constraint(equalTo: checkmarkView.trailingAnchor, constant: padding),
        previewDetailsView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -2 * padding),
        previewDetailsView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
      ])
    }
     
    showFullDetails.toggle()
    //Invalidate current layout &
    self.setNeedsLayout()
  }


Now only a reusable cell issue remains, but i'll make a new topic about that! Thanks again!