简体   繁体   中英

Can't combine UIImageView rotation animation and tableView section reload

I have 4 sections, each section have 2 nested rows. I open the rows by tapping on each section.

Here is how my initial data looks like. It has title , subtitle and options (which is what nested rows should display):

   private var sections = [
        SortingSection(title: "По имени", subtitle: "Российский рубль", options: ["По возрастанию (А→Я)", "По убыванию (Я→А)"]),
        SortingSection(title: "По короткому имени", subtitle: "RUB", options: ["По возрастанию (А→Я)", "По убыванию (Я→А)"]),
        SortingSection(title: "По значению", subtitle: "86,22", options: ["По возрастанию (1→2)", "По убыванию (2→1)"]),
        SortingSection(title: "Своя", subtitle: "в любом порядке", options: ["Включить"])
   ]

When I tap on a section I want it accessory ( chevron.right , made as UIImageView ) be rotated in sync with expanding of nested rows and when I click again the same behaviour for closing.

I have a variable called isOpened (bool, false by default), which I change from false to true and back each tap in didSelectRowAt . Based on that a show all nested cells and rotate the UIImageView :

    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    if indexPath.row == 0 {
        sections[indexPath.section].isOpened.toggle()
        
        guard let cell = tableView.cellForRow(at: indexPath) as? MainSortTableViewCell else { return }
        
        UIView.animate(withDuration: 0.3) {
            if self.sections[indexPath.section].isOpened {
                cell.chevronImage.transform = CGAffineTransform(rotationAngle: .pi/2)
            } else {
                cell.chevronImage.transform = .identity
            }
        } completion: { _ in
            tableView.reloadSections([indexPath.section], with: .none)
        }
     }

As you can see above I reload tableView section to show\hide nested rows in a completion block after animation. I can't use reloadSections in an if\else statement because then chevron animation gets skipped .

Also my numberOrRowsInSection method:

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    let section = sections[section]
    if section.isOpened {
        return section.options.count + 1
    } else {
        return 1
    }
}
  1. Here is how it looks now: CLICK

  2. Here is what I want (any iPhone native apps): CLICK

I tried to add and delete rows instead of reloading the whole section, but always end up with error:

UIView.animate(withDuration: 0.3) {
    if self.sections[indexPath.section].isOpened {
       cell.chevronImage.transform = CGAffineTransform(rotationAngle: .pi/2)
                
         for i in 0..<self.sections[indexPath.section].options.count {
                    tableView.insertRows(at: [IndexPath(row: 1+i, section: indexPath.section)], with: .none)
         }
      } else {
         cell.chevronImage.transform = .identity
                
       for i in 0..<self.sections[indexPath.section].options.count {
        tableView.deleteRows(at: [IndexPath(row: i-1, section: indexPath.section)], with: .none)
                }
            }
        }

How can I change my code to solve the task and animate chevron at the same time nested rows expand or close?

As you've seen, if you want to animate an element in a cell you cannot do so at the same time as reloading the cell.

So, to get the effect you want, one approach will be to split your data into "section pairs."

So, instead of this:

在此处输入图像描述

you'll have this:

在此处输入图像描述

When tapping on a "header" section, you can animate the image view rotation for that cell while reloading the next section .

It takes a little more management of the data -- but, really, not that much.

For example, if the data structure is:

struct SortingSection {
    var title: String = ""
    var subtitle: String = ""
    var options: [String] = []
    var isOpened: Bool = false
}

in numberOfSections we can return sections.count * 2

Then, in numberOfRowsInSection , we'll get the "virtualSection" number to get the index into our data array - something like this:

override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    let virtualSection: Int = section / 2
    let secItem = sections[virtualSection]
    if section % 2 == 0 {
        return 1
    }
    if secItem.isOpened {
        return secItem.options.count
    }
    return 0
}

similarly, in cellForRowAt :

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let virtualSection: Int = indexPath.section / 2
    let secItem = sections[virtualSection]
    if indexPath.section % 2 == 0 {
        // return a "header row cell"
    }
    // return a "option row cell"
}

and finally, in didSelectRowAt :

override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {

    let virtualSection: Int = indexPath.section / 2
    // if it's a "header row"
    if indexPath.section % 2 == 0 {
        sections[virtualSection].isOpened.toggle()
        guard let c = tableView.cellForRow(at: indexPath) as? ExpandCell else { return }
        UIView.animate(withDuration: 0.3) {
            if self.sections[virtualSection].isOpened {
                c.chevronImageView.transform = CGAffineTransform(rotationAngle: .pi/2)
            } else {
                c.chevronImageView.transform = .identity
            }
            // reload the NEXT section
            tableView.reloadSections([indexPath.section + 1], with: .automatic)
        }
    }

}

Here's a complete implementation to try out. Everything is done via code (no @IBOutlet connections), so create a new UITableViewController and assign its custom class to ExpandSectionTableViewController :

struct SortingSection {
    var title: String = ""
    var subtitle: String = ""
    var options: [String] = []
    var isOpened: Bool = false
}
class ExpandCell: UITableViewCell {
    let titleLabel = UILabel()
    let subtitleLabel = UILabel()
    let chevronImageView = UIImageView()
    
    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    func commonInit() {
        
        titleLabel.translatesAutoresizingMaskIntoConstraints = false
        contentView.addSubview(titleLabel)
        
        subtitleLabel.translatesAutoresizingMaskIntoConstraints = false
        contentView.addSubview(subtitleLabel)
        
        chevronImageView.translatesAutoresizingMaskIntoConstraints = false
        contentView.addSubview(chevronImageView)
        
        let g = contentView.layoutMarginsGuide
        NSLayoutConstraint.activate([
            titleLabel.topAnchor.constraint(equalTo: g.topAnchor),
            titleLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor),
            
            subtitleLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 4.0),
            subtitleLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor),
            
            chevronImageView.trailingAnchor.constraint(equalTo: g.trailingAnchor),
            chevronImageView.widthAnchor.constraint(equalToConstant: 40.0),
            chevronImageView.heightAnchor.constraint(equalTo: chevronImageView.widthAnchor),
            chevronImageView.centerYAnchor.constraint(equalTo: g.centerYAnchor),
            
            subtitleLabel.bottomAnchor.constraint(equalTo: g.bottomAnchor),
            
        ])
    
        subtitleLabel.font = .systemFont(ofSize: 12.0, weight: .regular)
        subtitleLabel.textColor = .gray
        
        chevronImageView.contentMode = .center
        let cfg = UIImage.SymbolConfiguration(pointSize: 24.0, weight: .regular)
        if let img = UIImage(systemName: "chevron.right", withConfiguration: cfg) {
            chevronImageView.image = img
        }
        
    }
}
class SubCell: UITableViewCell {
    
    let titleLabel = UILabel()
    
    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    func commonInit() {
        
        titleLabel.translatesAutoresizingMaskIntoConstraints = false
        contentView.addSubview(titleLabel)
        
        let g = contentView.layoutMarginsGuide
        NSLayoutConstraint.activate([
            titleLabel.topAnchor.constraint(equalTo: g.topAnchor),
            titleLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
            titleLabel.bottomAnchor.constraint(equalTo: g.bottomAnchor),
        ])
        
        titleLabel.font = .italicSystemFont(ofSize: 15.0)
        
    }
}

class ExpandSectionTableViewController: UITableViewController {
    
    var sections: [SortingSection] = []
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let optCounts: [Int] = [
            2, 3, 2, 5, 4, 2, 2, 3, 3, 4, 2, 1, 2, 3, 4, 3, 2
        ]
        for (i, val) in optCounts.enumerated() {
            var opts: [String] = []
            for n in 1...val {
                opts.append("Section \(i) - Option \(n)")
            }
            sections.append(SortingSection(title: "Title \(i)", subtitle: "Subtitle \(i)", options: opts, isOpened: false))
        }
        
        tableView.register(ExpandCell.self, forCellReuseIdentifier: "expCell")
        tableView.register(SubCell.self, forCellReuseIdentifier: "subCell")
    }
    override func numberOfSections(in tableView: UITableView) -> Int {
        return sections.count * 2
    }
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        
        let virtualSection: Int = section / 2
        let secItem = sections[virtualSection]
        if section % 2 == 0 {
            return 1
        }
        if secItem.isOpened {
            return secItem.options.count
        }
        return 0

    }
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

        let virtualSection: Int = indexPath.section / 2
        let secItem = sections[virtualSection]
        if indexPath.section % 2 == 0 {
            let c = tableView.dequeueReusableCell(withIdentifier: "expCell", for: indexPath) as! ExpandCell
            c.titleLabel.text = secItem.title
            c.subtitleLabel.text = secItem.subtitle
            c.chevronImageView.transform = secItem.isOpened ? CGAffineTransform(rotationAngle: .pi/2) : .identity
            c.selectionStyle = .none
            return c
        }
        let c = tableView.dequeueReusableCell(withIdentifier: "subCell", for: indexPath) as! SubCell
        c.titleLabel.text = secItem.options[indexPath.row]
        return c

    }
    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {

        let virtualSection: Int = indexPath.section / 2
        // if it's a "header row"
        if indexPath.section % 2 == 0 {
            sections[virtualSection].isOpened.toggle()
            guard let c = tableView.cellForRow(at: indexPath) as? ExpandCell else { return }
            UIView.animate(withDuration: 0.3) {
                if self.sections[virtualSection].isOpened {
                    c.chevronImageView.transform = CGAffineTransform(rotationAngle: .pi/2)
                } else {
                    c.chevronImageView.transform = .identity
                }
                // reload the NEXT section
                tableView.reloadSections([indexPath.section + 1], with: .automatic)
            }
        }

    }
    
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM