6

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

Nilesh R Patel
  • 697
  • 7
  • 17
Emre Önder
  • 2,408
  • 2
  • 23
  • 73
  • Why this question gets down vote? can you please explain? – Emre Önder Feb 01 '19 at 08:30
  • Hello @Emre, I just read your question, but i am unable to identify what issue in animation, Can you just make small video or gif and put the link over here... so that i can get exact issue. One more thing... if you have any other sample(may be app or design) which describes what is your expected behaviour then that will be better to understand your issue. btw, I am not a person to downvote your question. – Mehul Thakkar Feb 01 '19 at 10:45
  • Hello, I edited my question and tried to explain it more clearly on screenshot explanation. Another city header goes above It's cell and then moves up. However, It should stay same because I only close Some Data not Another City. – Emre Önder Feb 01 '19 at 10:49
  • let say A, B and C are 3 section, now if you close the section B, then it is obvious that contentOffset of tableview will remain same. So, section C will come in upward direction. Also, if (contentOffset.y + screensize.height) will be greater than contentSize, then in that case, contentOffset's y position will also get updated(will go down in this case). – Mehul Thakkar Feb 01 '19 at 10:54
  • I think this is normal behaviour, if my understanding with your question is proper one. – Mehul Thakkar Feb 01 '19 at 10:55
  • Problem is section B y position goes 100 to 120 then goes to 80. I think proper animation should be 100 to 80. – Emre Önder Feb 01 '19 at 11:00
  • this must be happening if you are at last cell. If you are having limited cities, then add fake cities and make it enough scrollable then check that is it happening for if last cell is visible OR in all cases. As per my knowledge, this should only happen in case of last cell – Mehul Thakkar Feb 01 '19 at 11:10
  • But I’m opening NOT the last cell – Emre Önder Feb 01 '19 at 11:28
  • @EmreÖnder Try this. https://stackoverflow.com/questions/16071503/how-to-tell-when-uitableview-has-completed-reloaddata – Shubham Feb 02 '19 at 08:18
  • I’ll check it but as I searched, reloaddata has no animation so that there will be no problem – Emre Önder Feb 02 '19 at 08:21
  • still searching for solution? or found one – Mahesh Agrawal Feb 04 '19 at 09:41
  • Still searching – Emre Önder Feb 04 '19 at 09:42
  • do you want to do something like this? confirm if yes. enable flash on browser if does not play. https://screencast.com/t/KJkb3zoEb9z7 – Mahesh Agrawal Feb 04 '19 at 09:44
  • i used completely different approach then you and less code as well and working superb for me. – Mahesh Agrawal Feb 04 '19 at 09:50
  • What did you use? – Emre Önder Feb 04 '19 at 09:58
  • i used RATreeView. its a wrapper created using tableview at the end. – Mahesh Agrawal Feb 04 '19 at 10:05
  • Thank you for your comment but I need to solve it in this way :( and find what causes this problem? – Emre Önder Feb 04 '19 at 10:07

2 Answers2

7

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()
}
Emre Önder
  • 2,408
  • 2
  • 23
  • 73
0

Had the same issue, you can use this as a fix :

UIView.performWithoutAnimation {
    self.tableView.reloadSections(IndexSet(integer: section), with: .fade)
}

Of course, you lose the animation but also the flicker.

Guillaume Ramey
  • 205
  • 2
  • 5