无法结合 UIImageView 旋转动画和 tableView 部分重新加载

Can't combine UIImageView rotation animation and tableView section reload

我有 4 个部分,每个部分有 2 个嵌套行。我通过点击每个部分打开行。

这是我的初始数据的样子。它有 titlesubtitleoptions(这是嵌套行应该显示的内容):

   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
    }
}
  1. 这是现在的样子:CLICK

  2. 这是我想要的(任何 iPhone 本机应用程序):CLICK

我尝试添加和删除行而不是重新加载整个部分,但总是以错误告终:

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

    }
    
}