无法结合 UIImageView 旋转动画和 tableView 部分重新加载
Can't combine UIImageView rotation animation and tableView section reload
我有 4 个部分,每个部分有 2 个嵌套行。我通过点击每个部分打开行。
这是我的初始数据的样子。它有 title
、subtitle
和 options
(这是嵌套行应该显示的内容):
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: ["Включить"])
]
当我点击一个部分时,我希望它的附件(chevron.right
,制作为 UIImageView
)随着嵌套行的扩展 同步 旋转和当我再次单击相同的关闭行为时。
我有一个名为 isOpened 的变量(bool,默认情况下为 false),我将其从 false 更改为 true 并在 didSelectRowAt
中每次点击后返回。基于此显示所有嵌套单元格并旋转 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)
}
}
正如您在上面看到的那样,我在动画后的完成块中将 tableView 部分重新加载到 show\hide 嵌套行。我不能在 if\else 语句中使用 reloadSections
因为 V 形动画 会被跳过 .
还有我的numberOrRowsInSection
方法:
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
let section = sections[section]
if section.isOpened {
return section.options.count + 1
} else {
return 1
}
}
我尝试添加和删除行而不是重新加载整个部分,但总是以错误告终:
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)
}
}
}
如何更改我的代码来解决任务并在嵌套行展开或关闭的同时为人字形设置动画?
如您所见,如果要为单元格中的元素设置动画,则不能在重新加载单元格的同时执行此操作。
因此,为了获得您想要的效果,一种方法是将您的数据拆分为“部分对”。
所以,而不是这个:
你会得到这个:
点击“header”部分时,您可以在重新加载下一部分[=时为该单元格设置图像视图旋转动画55=].
它需要对数据进行更多管理 -- 但实际上并没有那么多。
例如,如果数据结构是:
struct SortingSection {
var title: String = ""
var subtitle: String = ""
var options: [String] = []
var isOpened: Bool = false
}
在numberOfSections
中我们可以returnsections.count * 2
然后,在 numberOfRowsInSection
中,我们将获取“virtualSection”编号以获取数据数组中的索引 - 如下所示:
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
}
同样,在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"
}
最后,在 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)
}
}
}
这里有一个完整的实现来试用。一切都是通过代码完成的(没有 @IBOutlet
连接),因此创建一个新的 UITableViewController
并将其自定义 class 分配给 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)
}
}
}
}
我有 4 个部分,每个部分有 2 个嵌套行。我通过点击每个部分打开行。
这是我的初始数据的样子。它有 title
、subtitle
和 options
(这是嵌套行应该显示的内容):
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: ["Включить"])
]
当我点击一个部分时,我希望它的附件(chevron.right
,制作为 UIImageView
)随着嵌套行的扩展 同步 旋转和当我再次单击相同的关闭行为时。
我有一个名为 isOpened 的变量(bool,默认情况下为 false),我将其从 false 更改为 true 并在 didSelectRowAt
中每次点击后返回。基于此显示所有嵌套单元格并旋转 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)
}
}
正如您在上面看到的那样,我在动画后的完成块中将 tableView 部分重新加载到 show\hide 嵌套行。我不能在 if\else 语句中使用 reloadSections
因为 V 形动画 会被跳过 .
还有我的numberOrRowsInSection
方法:
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
let section = sections[section]
if section.isOpened {
return section.options.count + 1
} else {
return 1
}
}
我尝试添加和删除行而不是重新加载整个部分,但总是以错误告终:
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)
}
}
}
如何更改我的代码来解决任务并在嵌套行展开或关闭的同时为人字形设置动画?
如您所见,如果要为单元格中的元素设置动画,则不能在重新加载单元格的同时执行此操作。
因此,为了获得您想要的效果,一种方法是将您的数据拆分为“部分对”。
所以,而不是这个:
你会得到这个:
点击“header”部分时,您可以在重新加载下一部分[=时为该单元格设置图像视图旋转动画55=].
它需要对数据进行更多管理 -- 但实际上并没有那么多。
例如,如果数据结构是:
struct SortingSection {
var title: String = ""
var subtitle: String = ""
var options: [String] = []
var isOpened: Bool = false
}
在numberOfSections
中我们可以returnsections.count * 2
然后,在 numberOfRowsInSection
中,我们将获取“virtualSection”编号以获取数据数组中的索引 - 如下所示:
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
}
同样,在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"
}
最后,在 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)
}
}
}
这里有一个完整的实现来试用。一切都是通过代码完成的(没有 @IBOutlet
连接),因此创建一个新的 UITableViewController
并将其自定义 class 分配给 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)
}
}
}
}