点击展开 UITableViewCell(自定义) - 自动调整单元格 - 一些问题

Expand UITableViewCell (custom) on tap - autosize cell - some issues

在我的项目(UIKit,程序化UI)中,我有一个带有部分的 UITableView。单元格使用自定义 class。加载时所有单元格仅显示 3 行信息(2 个标签)。点击,将显示所有内容。因此,我将我的自定义单元格 class 设置为有两个容器,一个用于 3 行预览,一个用于完整内容。当用户通过在自定义单元格 class 上调用方法 (toggleFullView) 点击单元格时,需要时这些容器 added/removed 来自单元格的内容视图。此方法从 didSelectRowAt:

中的视图控制器调用
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        let annotation = annotationsController.getAnnotationFor(indexPath)

        //Expandable cell
        guard let cell = tableView.cellForRow(at: indexPath) as? AnnotationCell else { return }
        cell.toggleFullView()
        tableView.reloadRows(at: [indexPath], with: .none)
//        tableView.reloadData()
    }

基本可以,但是有一些问题:

  1. 我必须双击单元格才能展开,然后双击才能再次折叠。第一次点击将执行 tableView.reloadRows(at: [indexPath], with: .none) 的行动画,第二次点击将执行扩展。如果我将 reloadRows 替换为 tableView.reloadData(),则只需轻按一下即可展开和折叠!但这显然会禁用任何动画,它只是卡入到位。如何让它一键工作?

  2. 当单元格展开时,其他一些随机单元格也会展开。我想这与可重复使用的电池有关,但我无法解决这个问题。请参阅附件视频 (https://www.youtube.com/watch?v=rOkuqMnArEU)。

  3. 我想要展开的单元格在我点击另一个单元格展开时折叠,我怎么看?

我的自定义单元格class:

import UIKit

class AnnotationCell: UITableViewCell, SelfConfiguringAnnotationCell {
    //MARK: - Properties
    private let titleLabelPreview = ProjectTitleLabel(withTextAlignment: .left, andFont: UIFont.preferredFont(forTextStyle: .headline))
    private let titleLabelDetails = ProjectTitleLabel(withTextAlignment: .left, andFont: UIFont.preferredFont(forTextStyle: .headline))
    private let detailsLabelShort = ProjectTitleLabel(withTextAlignment: .left, andFont: UIFont.preferredFont(forTextStyle: .subheadline), numberOfLines: 2)
    private let detailsLabelLong = ProjectTitleLabel(withTextAlignment: .left, andFont: UIFont.preferredFont(forTextStyle: .subheadline), numberOfLines: 0)
    private let mapImageLabel = ProjectTitleLabel(withTextAlignment: .center, andFont: UIFont.preferredFont(forTextStyle: .footnote), andColor: .tertiarySystemGroupedBackground)
    private let lastEditedLabel = ProjectTitleLabel(withTextAlignment: .center, andFont: UIFont.preferredFont(forTextStyle: .footnote), andColor: .tertiarySystemGroupedBackground)
    private let checkmarkImageView = UIImageView()
    
    private var checkmarkView = UIView()
    private var previewDetailsView = UIStackView()
    private var fullDetailsView = UIStackView()
    
    private var showFullDetails = false
    
    //MARK: - Init
    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        
        configureContents()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func layoutIfNeeded() {
        super.layoutIfNeeded()
        
        let padding: CGFloat = 5
        
        if contentView.subviews.contains(previewDetailsView) {
            //Constrain the preview view
            NSLayoutConstraint.activate([
                previewDetailsView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: padding),
                previewDetailsView.leadingAnchor.constraint(equalTo: checkmarkView.trailingAnchor, constant: padding),
                previewDetailsView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -2 * padding),
                previewDetailsView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -padding)
            ])

        } else {
            //Constrain the full view
            NSLayoutConstraint.activate([
                fullDetailsView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: padding),
                fullDetailsView.leadingAnchor.constraint(equalTo: checkmarkView.trailingAnchor, constant: padding),
                fullDetailsView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -2 * padding),
                fullDetailsView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -padding)
            ])
        }

    }
    
    //MARK: - Actions
    ///Expand and collapse the cell
    func toggleFullView() {
        showFullDetails.toggle()
        
        if showFullDetails {
            //show the full version
            if contentView.subviews.contains(previewDetailsView) {
                previewDetailsView.removeFromSuperview()
            }
            if !contentView.subviews.contains(fullDetailsView) {
                contentView.addSubview(fullDetailsView)
            }
        } else {
            //show the preview version
            if contentView.subviews.contains(fullDetailsView) {
                fullDetailsView.removeFromSuperview()
            }
            if !contentView.subviews.contains(previewDetailsView) {
                contentView.addSubview(previewDetailsView)
            }
        }
        UIView.animate(withDuration: 1.2) {
            self.layoutIfNeeded()
        }
    }
    
    //MARK: - Layout
    private func configureContents() {
        backgroundColor = .clear
        separatorInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
        selectionStyle = .none
        
        detailsLabelShort.adjustsFontSizeToFitWidth = false
        detailsLabelLong.adjustsFontSizeToFitWidth = false
        
        checkmarkView.translatesAutoresizingMaskIntoConstraints = false
        checkmarkView.addSubview(checkmarkImageView)
        
        checkmarkImageView.tintColor = .systemOrange
        checkmarkImageView.translatesAutoresizingMaskIntoConstraints = false
        
        previewDetailsView = UIStackView(arrangedSubviews: [titleLabelPreview, detailsLabelShort])
        previewDetailsView.axis = .vertical
        previewDetailsView.translatesAutoresizingMaskIntoConstraints = false
        previewDetailsView.addBackground(.blue)
        
        fullDetailsView = UIStackView(arrangedSubviews: [titleLabelDetails, detailsLabelLong, mapImageLabel, lastEditedLabel])
        fullDetailsView.axis = .vertical
        fullDetailsView.translatesAutoresizingMaskIntoConstraints = false
        fullDetailsView.addBackground(.green)
        
        //By default only add the preview View
        contentView.addSubviews(checkmarkView, previewDetailsView)

        let padding: CGFloat = 5
        
        NSLayoutConstraint.activate([
            //Constrain the checkmark image view to the top left with a fixed height and width
            checkmarkImageView.widthAnchor.constraint(equalToConstant: 24),
            checkmarkImageView.heightAnchor.constraint(equalTo: checkmarkImageView.widthAnchor),
            checkmarkImageView.centerYAnchor.constraint(equalTo: checkmarkView.centerYAnchor),
            checkmarkImageView.centerXAnchor.constraint(equalTo: checkmarkView.centerXAnchor),

            checkmarkView.widthAnchor.constraint(equalToConstant: 30),
            checkmarkView.heightAnchor.constraint(equalTo: checkmarkView.widthAnchor),
            checkmarkView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: padding),
            checkmarkView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: padding)
                        
        ])

        self.layoutIfNeeded()
    }
    
    //MARK: - Configure cell with data
    func configure(with annotation: AnnotationsController.Annotation) {
        titleLabelPreview.text = annotation.title
        titleLabelDetails.text = annotation.title
        detailsLabelShort.text = annotation.details
        detailsLabelLong.text = annotation.details
        checkmarkImageView.image = annotation.complete ? ProjectImages.Annotation.checkmark : nil
        lastEditedLabel.text = annotation.lastEdited.customMediumToString
        mapImageLabel.text = annotation.mapImage?.title ?? "No map image attached"
    }
}

好的,搞定了,一个完全展开的表格视图单元格。关键是使自定义单元格中的布局无效 class 并在 tableView 上调用 beginUpdates() 和 endUpdates()!

在我的 viewController:

    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        //Expandable cell
        guard let cell = tableView.cellForRow(at: indexPath) as? AnnotationCell else { return }
        cell.toggleFullView()
        tableView.beginUpdates()
        tableView.endUpdates()
    }

我的自定义单元格 class 使用 toggleFullView() 方法:

class AnnotationCell: UITableViewCell, SelfConfiguringAnnotationCell {
    //MARK: - Properties
    private let titleLabelPreview = ProjectTitleLabel(withTextAlignment: .left, andFont: UIFont.preferredFont(forTextStyle: .headline))
    private let titleLabelDetails = ProjectTitleLabel(withTextAlignment: .left, andFont: UIFont.preferredFont(forTextStyle: .headline))
    private let detailsLabelShort = ProjectTitleLabel(withTextAlignment: .left, andFont: UIFont.preferredFont(forTextStyle: .subheadline), numberOfLines: 2)
    private let detailsLabelLong = ProjectTitleLabel(withTextAlignment: .left, andFont: UIFont.preferredFont(forTextStyle: .subheadline), numberOfLines: 0)
    private let mapImageLabel = ProjectTitleLabel(withTextAlignment: .center, andFont: UIFont.preferredFont(forTextStyle: .footnote), andColor: .tertiarySystemGroupedBackground)
    private let lastEditedLabel = ProjectTitleLabel(withTextAlignment: .center, andFont: UIFont.preferredFont(forTextStyle: .footnote), andColor: .tertiarySystemGroupedBackground)
    private let checkmarkImageView = UIImageView()
    
    private var checkmarkView = UIView()
    private var previewDetailsView = UIStackView()
    private var fullDetailsView = UIStackView()
    
    let padding: CGFloat = 5
    
    var showFullDetails = false
    
    //MARK: - Init
    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        
        configureContents()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
        
    //MARK: - Actions
    ///Expand and collapse the cell
    func toggleFullView() {
        //Show the full contents
        print("ShowFullDetails = \(showFullDetails.description.uppercased())")
        if showFullDetails {
            print("Show full contents")
            if contentView.subviews.contains(previewDetailsView) {
                previewDetailsView.removeFromSuperview()
            }
            if !contentView.subviews.contains(fullDetailsView) {
                contentView.addSubview(fullDetailsView)
            }
            NSLayoutConstraint.activate([
                fullDetailsView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: padding),
                fullDetailsView.leadingAnchor.constraint(equalTo: checkmarkView.trailingAnchor, constant: padding),
                fullDetailsView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -2 * padding),
                fullDetailsView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -padding)
            ])
        //Show preview contents
        } else {
            print("Show preview contents")
            if contentView.subviews.contains(fullDetailsView) {
                fullDetailsView.removeFromSuperview()
            }
            if !contentView.subviews.contains(previewDetailsView) {
                contentView.addSubview(previewDetailsView)
            }
            NSLayoutConstraint.activate([
                previewDetailsView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: padding),
                previewDetailsView.leadingAnchor.constraint(equalTo: checkmarkView.trailingAnchor, constant: padding),
                previewDetailsView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -2 * padding),
                previewDetailsView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
            ])
        }
        
        showFullDetails.toggle()

        //Invalidate current layout &
        self.setNeedsLayout()
    }
    
    override func prepareForReuse() {
        //Make sure reused cells start in the preview mode!
//        showFullDetails = false
    }
    
    override func layoutIfNeeded() {
        super.layoutIfNeeded()
        
        NSLayoutConstraint.activate([
            //Constrain the checkmark image view to the top left with a fixed height and width
            checkmarkImageView.widthAnchor.constraint(equalToConstant: 24),
            checkmarkImageView.heightAnchor.constraint(equalTo: checkmarkImageView.widthAnchor),
            checkmarkImageView.centerYAnchor.constraint(equalTo: checkmarkView.centerYAnchor),
            checkmarkImageView.centerXAnchor.constraint(equalTo: checkmarkView.centerXAnchor),
            
            checkmarkView.widthAnchor.constraint(equalToConstant: 30),
            checkmarkView.heightAnchor.constraint(equalTo: checkmarkView.widthAnchor),
            checkmarkView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: padding),
            checkmarkView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: padding)
        ])
    }
    
    //MARK: - Layout
    private func configureContents() {
        //Setup Views
        backgroundColor = .clear
        separatorInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
        selectionStyle = .none
        
        detailsLabelShort.adjustsFontSizeToFitWidth = false
        detailsLabelLong.adjustsFontSizeToFitWidth = false
        
        checkmarkView.translatesAutoresizingMaskIntoConstraints = false
        checkmarkView.addSubview(checkmarkImageView)
        
        checkmarkImageView.tintColor = .systemOrange
        checkmarkImageView.translatesAutoresizingMaskIntoConstraints = false
        
        previewDetailsView = UIStackView(arrangedSubviews: [titleLabelPreview, detailsLabelShort])
        previewDetailsView.axis = .vertical
        previewDetailsView.translatesAutoresizingMaskIntoConstraints = false
        previewDetailsView.addBackground(.blue)
        
        fullDetailsView = UIStackView(arrangedSubviews: [titleLabelDetails, detailsLabelLong, mapImageLabel, lastEditedLabel])
        fullDetailsView.axis = .vertical
        fullDetailsView.translatesAutoresizingMaskIntoConstraints = false
        fullDetailsView.addBackground(.green)
        
        //By default only show the preview View
        contentView.addSubviews(checkmarkView)
        
        //Setup preview/DetailView
        toggleFullView()
    }
    
    //MARK: - Configure cell with data
    func configure(with annotation: AnnotationsController.Annotation) {
        titleLabelPreview.text = annotation.title
        titleLabelDetails.text = annotation.title
        detailsLabelShort.text = annotation.details
        detailsLabelLong.text = annotation.details
        checkmarkImageView.image = annotation.complete ? ProjectImages.Annotation.checkmark : nil
        lastEditedLabel.text = annotation.lastEdited.customMediumToString
        mapImageLabel.text = annotation.mapImage?.title ?? "No map image attached"
    }
}

HTH!