如何通过 indexPath 在水平 collectionView 中动态添加视图/图层

Posted

技术标签:

【中文标题】如何通过 indexPath 在水平 collectionView 中动态添加视图/图层【英文标题】:How to add views/layers dynamically in a horizontal collectionView by indexPath 【发布时间】:2018-06-06 16:54:58 【问题描述】:

我正在使用水平 collectionView 制作各种时间线,其中对于每个时间段(一个月),我都有一个单元格,其中包含不同颜色的 UIViews。有些月份添加了 30 个视图,有些是 31,有些是 28。我无法动态地将视图添加到每个单元格,这样它们就不会被复制或添加到错误的单元格中。

为此,我创建了此时间线的简化版本,其中每个月仅动态添加 1 个图层/视图 - 然后我将尝试解决添加可变数量的视图/图层的问题。

我把这个项目作为我所说的最简单的例子:

https://github.com/AlexMarshall12/testTimeline-ios

这是 ViewController.swift 中的代码:

import UIKit

class ViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource 

    var filledCells: [Int] = []

    @IBOutlet weak var collectionView: UICollectionView!

    override func viewDidLoad() 
        super.viewDidLoad()
        self.filledCells = [1,28]
        collectionView.delegate = self
        collectionView.dataSource = self
        // Do any additional setup after loading the view, typically from a nib.
    

    override func didReceiveMemoryWarning() 
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    


    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int 
        return 28
    

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell 
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "myCell", for: indexPath) as! MyCollectionViewCell
        print(indexPath.item)
        cell.myLabel.text = String(indexPath.item)
        if filledCells.contains(indexPath.item) 
            let tickLayer = CAShapeLayer()
            tickLayer.path = UIBezierPath(roundedRect: CGRect(x: 0, y: 0, width: cell.layer.bounds.width, height: cell.layer.bounds.height), cornerRadius: 5).cgPath
            tickLayer.fillColor = UIColor(red:0.99, green:0.13, blue:0.25, alpha:0.5).cgColor
            cell.layer.addSublayer(tickLayer)
         else 

        //updated 

            let tickLayer = CAShapeLayer()
            tickLayer.path = UIBezierPath(roundedRect: CGRect(x: 0, y: 0, width: cell.layer.bounds.width, height: cell.layer.bounds.height), cornerRadius: 5).cgPath
            tickLayer.fillColor = UIColor(red:0.1, green:0.13, blue:0.98, alpha:0.5).cgColor
            cell.layer.addSublayer(tickLayer)
        
        return cell
    


这个想法是,对于每个 indexPath 项目(每个单元格?),它会查看它是否包含在 self.filledCells 数组中:1 或 28 恰好是外边缘,因为 numberOfItemsInSection 返回了 28 个单元格和1节。所以我想要发生的是每个单元格都是浅蓝色的,除了第 1 个和第 28 个 - 浅红色。

但是,您可以在此处看到 https://imgur.com/a/KTLn7Cb。当我来回滚动时,单元格被多次填充,有多种蓝色、红色和紫色阴影,当然不是 1 和 28。

我认为有两个问题。

    即使我没有滚动到最边缘的单元格,indexPath.item 也会以某种方式返回 1 或 28。为什么是这样? 当我重新访问已渲染的单元格时,它会重新渲染它们。我不确定这是为什么。我想知道重写 prepareForReuse() 是否有帮助,但我听说这通常是个坏主意,所以我不确定它是否是我要找的。

对实现这一目标有什么建议吗?

【问题讨论】:

你能更新 GitHub 项目吗?里面什么都没有 【参考方案1】:

您忽略了单元被重复使用的事实。当您更改单元格的填充颜色并滚动关闭时,滚动的单元格会重新使用该单元格,并且您只是将其填充颜色设置为红色,并且您没有将其关闭。为每个单元格显式设置填充颜色,无论是红色、白色还是透明色,不要只为选定的单元格设置它。

【讨论】:

这确实有帮助,但似乎每次我回滚到红色单元格时仍然会被覆盖 - 基本上是添加了另一层。此外,如果您添加两个不同的层,具体取决于它是否在 [1,28] 中,就像我在更新的代码中一样,它通常会同时绘制两个 - 紫色。我该怎么做才能防止这种情况发生? 每次调用 cellForItemAt 时,都必须从头开始设置单元格,并删除单元格中上次可能已放入的任何内容。因此,如果单元格应着色,请删除旧图层颜色并添加新颜色。如果单元格不应着色,请移除旧图层颜色并将单元格恢复为原始状态。【参考方案2】:

每次在表格视图单元格或集合视图单元格中添加视图或图层总是一个坏主意。当您重复使用单元格时,无需一次又一次地创建 tickLayer。将tickLayer设为全局变量,在MyCollectionViewCell的awakeFromNib函数中初始化,然后在cellForRow函数中改变其填充颜色。

【讨论】:

awakeFromNib 用于单元格本身?变量是单元类的一部分吗?我需要动态的视图 - 为了能够为时间线的目的创建可变数量的视图抱歉,我将在问题中澄清这一点。 awakeFromNib 是 UICollectionViewCell 中的一个方法。您需要在自定义单元格中覆盖它。在那里初始化 tickLayer 并在 cellForItemAtIndexPath 函数中改变它的颜色。 全局变量不是使用图层的正确方法。请注意,图层是视图(单元格)的一部分 - 每个单元格都应该有自己的图层副本,该图层将具有其子图层,同时还要记住单元格被重用。拥有一个图层的全局变量将简单地从先前处理的单元格中删除 tickLayer,并将其添加到当前单元格中。这可能会导致未定义的行为。【参考方案3】:

向单元格添加/删除视图不是一个好主意。 有两种方法可以完成此操作: ① 通过子视图创建不同的单元格; ② 创建一个通用单元格并通过隐藏和布局控制子视图的显示,并在您获得此单元格时使用新数据进行更新。

【讨论】:

【参考方案4】:

除了@Owen Hartnett 关于单元可重用性所说的内容之外,我想指出您每次都在重新创建层!这相当于创建子视图。

每次遇到cellForItemAt 函数中的单元格时,您还应该检查是否存在预添加层。类似的东西(不是语法上完美的 Swift,而是伪代码):

let bLayerFound = false;
for (layer: CALayer in cell.layer.sublayers)

   if (layer.name.equals("mylayer"))
   
     let shapeLayer = layer as! CAShapeLayer
     bLayerFound = true;
     //set layer properties based on your business logic, as you don't know what this cell is holding due to reuse
     shapeLayer.path = //path
     shapeLayer.fillColor = //color
   


if (bLayerFound == false)

   //create layer here, again
   let tickLayer = CAShapeLayer()

   //set layer properties based on your business logic, as you don't know what this cell is holding due to reuse
   tickLayer.path = //path
   tickLayer.fillColor = //color
   tickLayer.name = "mylayer"
   cell.layer.addSublayer(tickLayer)     

其他几点注意事项:

最好始终将 UI 添加到 cell.contentView,而不是 cell 本身。 一旦你让它运行起来,考虑把这段代码移到你的UICollectionViewCell子类中。如果没有子类,则可能是一个单独的函数,以免cellForItemAt 与讨厌的 if-else 逻辑混淆。

【讨论】:

以上是关于如何通过 indexPath 在水平 collectionView 中动态添加视图/图层的主要内容,如果未能解决你的问题,请参考以下文章

UICollectionView - 基于水平滚动更新数组 indexPath

如何获取位于 UICollectionView 中心的单元格的 indexPath

Xcode UICollectionView 如何通过 indexpath 禁用某些单元格

iOS:`UICollectionview`,如何通过它的`indexPath`获取单元格的`contentoffset`

如何获取自定义单元格并通过 indexpath 使用它的自定义项 - swift3?

iOS:`UICollectionview`,如何通过它的`indexPath`得到一个单元格的`contentoffset`