【问题标题】:Expand UITableViewCell adding UICollectionView as a subview展开 UITableViewCell 添加 UICollectionView 作为子视图
【发布时间】:2021-06-26 18:36:50
【问题描述】:

我查看了其他几个 Stack Overflow 问题,但我一直无法找到一个可以回答我的具体问题的问题。很多答案都说要使表格视图部分可扩展,但我希望该行可扩展并能够在扩展时向该行添加集合视图。而且,最好是在没有任何外部库的情况下完成。

我有一个 UITableView 部分,其中包含可以点击以展开或折叠的行。折叠时,单元格在左侧的垂直 UIStackView 中显示两行文本,在右侧显示图像和向下箭头。下图显示了折叠视图,我可以使用 AutoLayout 约束使其工作。

当展开时,应该在单元格内添加一个 UICollectionView,并且该行应该动画展开以显示集合视图。下图大致显示了它应该是什么样子。底部的灰色区域是集合视图,它是一个水平的集合视图,具有动态高度。

但我不确定如何将集合视图添加到 UITableViewCell 以及如何为其设置动画。我考虑过在代码中创建集合视图,向左右添加约束以将其固定到边缘行的(所以它将是全宽)。然后我要在集合视图的顶部添加一个约束,将其固定到表格视图单元格的底部,然后在动画块中,我要更改约束,以便集合视图的底部位于表格视图单元格的底部和集合视图的顶部被约束到图像视图的底部,并将压缩阻力优先级设置为 1000 以确保集合视图不被压缩,但我最终得到了无法满足的约束和我的约束最终被删除。我最终放弃了这个想法,因为我觉得它太复杂了,应该有更直接的方法来做到这一点。

我考虑过的另一种方法是在同一个 XIB 文件中有两个单独的视图 - 一个用于正常状态,一个用于展开状态。但是我不确定如何为状态之间的行中的所有更改设置动画。

有没有人知道如何在展开时将集合视图(或任何视图,我猜)添加到表格视图单元格的底部以及如何为其添加动画?做这样的事情的最佳实践是什么?感谢您的帮助。

【问题讨论】:

  • “底部的灰色区域是集合视图。” -- 你的意思是固定高度、水平滚动的集合视图吗?或者你的意思是一个垂直滚动的集合视图?或者你的意思是你想尝试实现一个动态变化的高度集合视图?
  • 感谢您的回复。它是一个水平滚动的集合视图,为用户提供了一些可供选择的选项。这些项目都应该是相同的高度,但高度应该是动态的。我的意思是,有时他们都会在其中有一个特定高度的图片,或者有时只会有一个具有最大高度的文本。

标签: ios objective-c uitableview uicollectionview


【解决方案1】:

我建议...

  • 向您的单元格添加一个子视图作为集合视图“容器”
  • 设置其.clipsToBounds = true
  • .constant = 0给它一个高度限制
  • .constant = 0给collectionView一个高度约束
  • 当您知道所需的collectionViewCell 高度时,设置collectionView 的约束.constant,以便加载单元格
  • 为“容器”视图的高度约束设置动画以展开/折叠

从“容器”视图本身开始......一旦你让它正确展开/折叠,然后添加到集合视图中。


编辑这是一个非常基本的动画展开/折叠表格视图单元示例。

数据结构 - 键和值字符串、“高度”属性(可能在您知道该行的集合视图的高度时设置)和“扩展”属性标志。

struct DemoStruct {
    var key: String = ""
    var value: String = ""
    var cvHeight: CGFloat = 0
    var expanded: Bool = false
}

单元格类 - 两个标签,展开/折叠按钮,标签下方的“容器”视图:

class SampleTVCell: UITableViewCell {
    
    // always visible elements
    let keyLabel = UILabel()
    let valLabel = UILabel()
    let btn = UIButton()
    
    // this will either hold the collectionView
    //  or be replaced by the collectionView
    let containerView = UIView()
    
    // will be set by the data
    var containerHeight: NSLayoutConstraint!
    
    // constraints for expanded/collapsed states
    var expandedConstraint: NSLayoutConstraint!
    var collapsedConstraint: NSLayoutConstraint!
    
    // down/up chevrons
    var expandImg: UIImage!
    var collapseImg: UIImage!
    
    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() -> Void {
        // make sure we get down/up button images
        guard let imgD = UIImage(systemName: "chevron.down"),
              let imgU = UIImage(systemName: "chevron.up")
        else {
            fatalError("Bad image loading!")
        }
        expandImg = imgD
        collapseImg = imgU

        // covers the bottom of the container view when cell is "collapsed"
        let coverView = UIView()
        coverView.backgroundColor = .white

        [keyLabel, valLabel, btn, containerView, coverView].forEach { v in
            v.translatesAutoresizingMaskIntoConstraints = false
            contentView.addSubview(v)
        }

        // set as desired
        keyLabel.font = .systemFont(ofSize: 20.0, weight: .bold)
        valLabel.font = .italicSystemFont(ofSize: 18.0)

        // use the built-in layout margins
        let g = contentView.layoutMarginsGuide
        
        // .constant will be set by data
        containerHeight = containerView.heightAnchor.constraint(equalToConstant: 0.0)

        // constrain bottom of valLabel to bottom of margins
        //  priority will be updated when expanded/collapsed
        collapsedConstraint = valLabel.bottomAnchor.constraint(equalTo: g.bottomAnchor)
        collapsedConstraint.priority = .defaultHigh
        
        // constrain bottom of containerView to bottom of margins
        //  priority will be updated when expanded/collapsed
        expandedConstraint = containerView.bottomAnchor.constraint(equalTo: g.bottomAnchor)
        expandedConstraint.priority = .defaultLow
        
        NSLayoutConstraint.activate([

            // button at upper-right
            btn.topAnchor.constraint(equalTo: g.topAnchor),
            btn.trailingAnchor.constraint(equalTo: g.trailingAnchor),
            btn.heightAnchor.constraint(equalToConstant: 32.0),
            btn.widthAnchor.constraint(equalTo: btn.heightAnchor, multiplier: 1.5),

            // keyLabel at upper-left
            keyLabel.topAnchor.constraint(equalTo: g.topAnchor),
            keyLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor),
            keyLabel.trailingAnchor.constraint(equalTo: btn.leadingAnchor, constant: -8.0),
            
            // valLabel aligned and below keyLabel
            valLabel.topAnchor.constraint(equalTo: keyLabel.bottomAnchor, constant: 4.0),
            valLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor),
            valLabel.trailingAnchor.constraint(equalTo: btn.leadingAnchor, constant: -8.0),
            
            // containerView below valLabel
            containerView.topAnchor.constraint(equalTo: valLabel.bottomAnchor, constant: 8.0),
            containerView.leadingAnchor.constraint(equalTo: g.leadingAnchor),
            containerView.trailingAnchor.constraint(equalTo: g.trailingAnchor),

            // activate our "control" constraints
            collapsedConstraint,
            expandedConstraint,
            containerHeight,

            // cover view sits at the very bottom of the contentView
            //  to cover the top part of the containerView
            //  when cell is "collapsed"
            coverView.leadingAnchor.constraint(equalTo: g.leadingAnchor),
            coverView.trailingAnchor.constraint(equalTo: g.trailingAnchor),
            coverView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
            coverView.heightAnchor.constraint(equalToConstant: contentView.layoutMargins.bottom),
            
        ])
        
        // we don't want the labels to stretch or compress vertically
        keyLabel.setContentCompressionResistancePriority(.required, for: .vertical)
        valLabel.setContentCompressionResistancePriority(.required, for: .vertical)
        keyLabel.setContentHuggingPriority(.required, for: .vertical)
        valLabel.setContentHuggingPriority(.required, for: .vertical)

        // action for button
        btn.addTarget(self, action: #selector(btnTap(_:)), for: .touchUpInside)
        
        // Important!
        contentView.clipsToBounds = true

        // so we can see the containerView
        //  until we replace it with or add a collectionView as a subview
        containerView.backgroundColor = .systemYellow
        
    }
    
    // closure so we can tell the controller we want the cell
    //  to collapse or expand
    var expand: ((Bool)->())?
    
    private var expanded: Bool = false {
        didSet {
            // set collapsed/expanded constraint priorities
            collapsedConstraint.priority = expanded ? .defaultLow : .defaultHigh
            expandedConstraint.priority = expanded ? .defaultHigh : .defaultLow
            // update button image
            btn.setImage(expanded ? collapseImg : expandImg, for: [])
        }
    }
    
    func fillData(_ d: DemoStruct) -> Void {
        keyLabel.text = d.key
        valLabel.text = d.value
        containerHeight.constant = d.cvHeight
        expanded = d.expanded
    }
    
    @objc func btnTap(_ sender: Any?) -> Void {
        expanded.toggle()
        // tell the controller our expanded state changed
        expand?(expanded)
    }
    
}

控制器类 - 生成 20 行数据,具有不同的“集合视图”高度。

class DemoTableViewController: UITableViewController {
    
    var theData: [DemoStruct] = []
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // create 20 rows of sample data
        theData = (1...20).map { DemoStruct(key: "Key \($0)", value: "Value \($0)", cvHeight: 40.0, expanded: false) }
        
        // vary the containerView heights
        //  this will end up being the collectionView heights
        let demoHeights: [CGFloat] = [
            40, 160, 80, 120,
        ]
        for i in 0..<theData.count {
            theData[i].cvHeight = demoHeights[i % demoHeights.count]
        }
        
        tableView.register(SampleTVCell.self, forCellReuseIdentifier: "stvc")
    }
    
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return theData.count
    }
    
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let c = tableView.dequeueReusableCell(withIdentifier: "stvc", for: indexPath) as! SampleTVCell
        
        let d = theData[indexPath.row]
        c.fillData(d)
        
        // set the closure
        c.expand = { isExpanded in
            // update data
            self.theData[indexPath.row].expanded = isExpanded
            // this tells the tableView to animate cell height changes
            tableView.performBatchUpdates(nil, completion: nil)
        }
        
        return c
    }
    
}

【讨论】:

  • 非常感谢您的回复。我会试试这个,让你知道它是怎么回事。
  • 抱歉延迟回复。我按照您建议的步骤进行操作,它确实允许在点击时更改高度,但我仍然无法让动画正常工作。我会继续努力。我还发布了一个关于重写我的整个界面的相关问题:stackoverflow.com/questions/66907297/…
  • @ashipma - 我用动画展开/折叠表格视图单元格的基本示例编辑了我的答案。
猜你喜欢
  • 2014-02-26
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-06-21
相关资源
最近更新 更多