Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Content Inset change when UITableView Reload Sections

I have an expandable UITableView. When user tap on a header, the related cell will be shown with an animation (RowAnimation.Fade) and then UITableView scrolls to that header (expanded header). When the user taps again to that header, It collapses.

What I want to achieve: I need to have an expandable UITableView with header and cells. When user tap header, cells need to be opened with RowAnimation.Fade and then scroll to that header.

Bonus: Also If I can get the arrow animate when user taps on the header will be great but I think this cause another bug, cuz we run so much animation on the same thread (Main thread)

My problem is that when a user taps to the header, tableView content inset changes and whole headers goes on minus Y position. So a weird animation occurs. (For example, headers, looks center of a cell) However, after the animation finish, everything looks correct.

func toggleSection(header: DistrictTableViewHeader, section: Int) {
self.selectedHeaderIndex = section
self.cities[section].isCollapsed = !self.cities[section].isCollapsed
let contentOffset = self.tableView.contentOffset
self.tableView.reloadSections(IndexSet(integer: section), with: UITableView.RowAnimation.fade)
self.tableView.scrollToRow(at: IndexPath(row: NSNotFound, section: section) /* you can pass NSNotFound to scroll to the top of the section even if that section has 0 rows */, at: UITableView.ScrollPosition.top, animated: true)
}

In addition: I set the height of headers and cells like in below.

func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
    return 1
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
    return 60
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
    return 140
}
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
    let header: DistrictTableViewHeader = tableView.dequeueReusableHeaderFooterView(withIdentifier: headerId) as! DistrictTableViewHeader
    //let header = DistrictTableViewHeader()

    header.customInit(title: self.cities[section].name, section: section, delegate: self,isColapsed: self.cities[section].isCollapsed,isSelectedHeader: section == selectedHeaderIndex ? true : false)
    return header
}

My custom headerView Class:

protocol ExpandableHeaderViewDelegate {
func toggleSection(header: DistrictTableViewHeader, section: Int)
}

class DistrictTableViewHeader: UITableViewHeaderFooterView {
var delegate: ExpandableHeaderViewDelegate?
var section: Int!

let nameLabel: UILabel = {
   let l = UILabel()
    l.textColor = Color.DistrictsPage.headerTextColor
    return l
}()

private let arrowImage: UIImageView = {
  let i = UIImageView()
    let image = UIImage(named: "ileri")?.withRenderingMode(UIImage.RenderingMode.alwaysTemplate)
    i.image = image
    i.contentMode = .scaleAspectFit
    return i
}()
var willAnimate: Bool = false
var isColapsed: Bool!{
    didSet{
        expandCollapseHeader()
    }
}

private func expandCollapseHeader(){
    if(willAnimate){
        if(!self.isColapsed){
            let degrees : Double = 90 //the value in degrees
            self.nameLabel.textColor = Color.Common.garantiLightGreen
            self.arrowImage.tintColor = Color.Common.garantiLightGreen
            self.arrowImage.transform = CGAffineTransform.init(rotationAngle: CGFloat(degrees * .pi/180))
            self.contentView.backgroundColor = UIColor(red:0.97, green:0.97, blue:0.97, alpha:1.0)
        }else{
            let degrees : Double = 0 //the value in degrees
            self.nameLabel.textColor = Color.DistrictsPage.headerTextColor
            self.arrowImage.tintColor = UIColor.black
            self.arrowImage.transform = CGAffineTransform.init(rotationAngle: CGFloat(degrees * .pi/180))
            self.contentView.backgroundColor = UIColor.white
        }
    }else{
        if(!isColapsed){
            let degrees : Double = 90 //the value in degrees
            self.nameLabel.textColor = Color.Common.garantiLightGreen
            self.arrowImage.tintColor = Color.Common.garantiLightGreen
            self.arrowImage.transform = CGAffineTransform.init(rotationAngle: CGFloat(degrees * .pi/180))
            self.contentView.backgroundColor = UIColor(red:0.97, green:0.97, blue:0.97, alpha:1.0)
        }else{
            let degrees : Double = 0 //the value in degrees
            self.nameLabel.textColor = Color.DistrictsPage.headerTextColor
            self.arrowImage.tintColor = UIColor.black
            self.arrowImage.transform = CGAffineTransform.init(rotationAngle: CGFloat(degrees * .pi/180))
            self.contentView.backgroundColor = UIColor.white
        }
        layoutSubviews()
    }
}

override init(reuseIdentifier: String?) {

    super.init(reuseIdentifier: reuseIdentifier)
    self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(selectHeaderAction)))
    nameLabel.translatesAutoresizingMaskIntoConstraints = false
    nameLabel.font = UIFont.systemFont(ofSize: 22)
    nameLabel.textColor = Color.DistrictsPage.headerTextColor
    contentView.addSubview(nameLabel)
    nameLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor).isActive = true
    nameLabel.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 15).isActive = true

    arrowImage.tintColor =  UIColor(red:0.32, green:0.36, blue:0.36, alpha:1.0)
    arrowImage.translatesAutoresizingMaskIntoConstraints = false
    contentView.addSubview(arrowImage)
    arrowImage.centerYAnchor.constraint(equalTo: contentView.centerYAnchor).isActive = true
    arrowImage.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -20).isActive = true
    arrowImage.widthAnchor.constraint(equalToConstant: 20).isActive = true

}

required init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
}
func rotate(_ toValue: CGFloat) {
    self.transform = CGAffineTransform.init(rotationAngle: toValue)
}
@objc func selectHeaderAction(gestureRecognizer: UITapGestureRecognizer) {
    let cell = gestureRecognizer.view as! DistrictTableViewHeader
    delegate?.toggleSection(header: self, section: cell.section)
}


func customInit(title: String, section: Int, delegate: ExpandableHeaderViewDelegate,isColapsed: Bool, isSelectedHeader: Bool) {
    self.nameLabel.text = title
    self.nameLabel.accessibilityIdentifier = title
    self.section = section
    self.delegate = delegate
    self.willAnimate = isSelectedHeader
    self.isColapsed = isColapsed
}

override func layoutSubviews() {
    super.layoutSubviews()
    self.contentView.backgroundColor = UIColor.white

}
}

In the below picture, bug is seen clearly. When "Some Data" and "Another City" is open and u tap on "Some Data". Animation bug occurs. "Another city" goes above Its' cell and then goes up. What should be done is "Another city" should stay in Its' place and then move up when "Some Data" cell is closing. enter image description here Example Project: https://github.com/emreond/tableViewLayoutIssue

like image 944
Emre Önder Avatar asked Jan 28 '19 06:01

Emre Önder


1 Answers

After days of search and tries, I found that changing UITableViewStyle to the group do the trick. Therefore, I changed initializing UITableView to

let tableView = UITableView.init(frame: CGRect.zero, style: .grouped)

In addition for scrolling, I needed to add CATransaction to catch the completion.

CATransaction.begin()
DispatchQueue.main.async {
self.tableView.beginUpdates()
CATransaction.setCompletionBlock {
    // Code to be executed upon completion
        self.tableView.scrollToRow(at: IndexPath(row: NSNotFound, section: section) /* you can pass NSNotFound to scroll to the top of the section even if that section has 0 rows */, at: UITableView.ScrollPosition.top, animated: true)
}
    self.tableView.reloadSections(IndexSet.init(integer: section), with: UITableView.RowAnimation.fade)
    self.tableView.endUpdates()
}
CATransaction.commit()
}
like image 198
Emre Önder Avatar answered Oct 31 '22 20:10

Emre Önder