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
}
}
-
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 returnsections.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 newUITableViewController
and assign its custom class toExpandSectionTableViewController
: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) } } } }