Home > Software engineering >  Can't combine UIImageView rotation animation and tableView section reload
Can't combine UIImageView rotation animation and tableView section reload

Time:04-12

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: enter image description here

    you'll have this:

    enter image description here

    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)
                }
            }
    
        }
        
    }
    
  • Related