Search code examples
swifttableview

How to fix weird behavior of fade animation for inserting and deleting rows in tableView?


I have tableView constraining to the top, left, right, and bottom of the ViewController.
I put UIButton inside the view for a header which has a function for expanding and shrinking using inserting and deleting rows with fade animation.
However, when I tap the button to expand the third section in a situation where the first and the last section are expanded and the second and the third section are shrunk, the third section's cells are overlapping with the third section like this video.
enter image description here

I want to know why this is happening and the solution. Thanks in advance.

struct TwoDimentionalData {
  let items: [String]
  var isExpanded: Bool
}

class ViewController: UIViewController {
  
  let headerId = "headerId"
  let cellId = "cellId"
  
  var datas = [TwoDimentionalData]()
  
  @IBOutlet weak var tableView: UITableView! {
    didSet {
      tableView.delegate = self
      tableView.dataSource = self
      tableView.register(CollapsibleTableViewCell.self, forCellReuseIdentifier: cellId)
      tableView.register(CopplasibleTableViewHeader.self, forHeaderFooterViewReuseIdentifier: headerId)
    }
  }
  override func viewDidLoad() {
    super.viewDidLoad()
    datas = [TwoDimentionalData(items: ["Item1-1", "Item1-2"], isExpanded: true),
             TwoDimentionalData(items: ["Item2-1", "Item2-2", "Item2-3"], isExpanded: true),
             TwoDimentionalData(items: ["Item3-1", "Item3-2", "Item3-3", "Item3-4"], isExpanded: true),
             TwoDimentionalData(items: ["Item4-1"], isExpanded: true)
    ]
  }
}

extension ViewController: UITableViewDelegate {
  func numberOfSections(in tableView: UITableView) -> Int {
    return datas.count
  }
  
  func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return datas[section].isExpanded ? datas[section].items.count : 0
  }
}

extension ViewController: UITableViewDataSource {
  func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: cellId, for: indexPath)
    cell.backgroundColor = .green
    return cell
  }
  
  func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
    return datas[indexPath.section].isExpanded ? 50 : 0
  }
  
  func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
    let header = tableView.dequeueReusableHeaderFooterView(withIdentifier: headerId) as? CopplasibleTableViewHeader
    header?.expandButton.tag = section
    header?.collapsibleTableViewHeaderDelegate = self
    header?.setLayout()
    header?.contentView.backgroundColor = .yellow
    return header
  }
  
  func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
    return 40
  }
  
}

extension ViewController: CollapsibleTableViewHeaderDelegate {
  func didTapExpandButton(tag: Int) {
    let isExpended = datas[tag].isExpanded
    datas[tag].isExpanded = !isExpended
    var indexPaths = [IndexPath]()
    let items = datas[tag].items
    
    for row in items.indices {
      let indexPath = IndexPath(row: row, section: tag)
      indexPaths.append(indexPath)
    }
    
    tableView.performBatchUpdates {
      if isExpended {
        tableView.deleteRows(at: indexPaths, with: .fade)
      } else {
        tableView.insertRows(at: indexPaths, with: .fade)
      }
    }
  }
}

protocol CollapsibleTableViewHeaderDelegate: AnyObject {
  func didTapExpandButton(tag: Int)
}

class CopplasibleTableViewHeader: UITableViewHeaderFooterView {

  weak var collapsibleTableViewHeaderDelegate: CollapsibleTableViewHeaderDelegate?
  
  lazy var expandButton: UIButton = {
    let button = UIButton()
    button.setImage(UIImage(systemName: "chevron.down"), for: .normal)
    button.addTarget(self, action: #selector(handleExpandButton), for: .touchUpInside)
    return button
  }()
  
  @objc private func handleExpandButton(button: UIButton) {
    print("handleExpandButton")
    let tag = button.tag
    collapsibleTableViewHeaderDelegate?.didTapExpandButton(tag: tag)
    
  }
  
  override func prepareForReuse() {
    super.prepareForReuse()
   
  }
  
  func setLayout() {
    addSubview(expandButton)
    expandButton.translatesAutoresizingMaskIntoConstraints = false
    expandButton.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true
    expandButton.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
  }
}

Solution

  • I've solved this by using reloadRows method instead of insert/delete rows method.

    First, change numberofRowsInSection to the below code to prevent a crash.

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return datas[section].items.count
      }
    

    And inside CollapsibleTableViewHeaderDelegate,

    extension ViewController: CollapsibleTableViewHeaderDelegate {
      func didTapExpandButton(tag: Int) {
        let isExpended = datas[tag].isExpanded
        datas[tag].isExpanded = !isExpended
        var indexPaths = [IndexPath]()
        let items = datas[tag].items
        
        for row in items.indices {
          let indexPath = IndexPath(row: row, section: tag)
          indexPaths.append(indexPath)
        }
        
         tableView.performBatchUpdates {
               tableView.reloadRows(at: indexPaths, with: .fade)
           }
      }
    }
    

    In this way, I get no more overlapping cells but still get fade animation.