使用自定义维度重新排序集合视图单元格时出现问题

Posted

技术标签:

【中文标题】使用自定义维度重新排序集合视图单元格时出现问题【英文标题】:Problems reordering Collection View Cell with custom dimensions 【发布时间】:2016-09-25 20:12:47 【问题描述】:

我想对集合视图中的单元格重新排序,并为每个单元格自定义大小。 在集合视图的每个单元格中都有一个带有单词的标签。 我用这段代码设置了每个单元格的尺寸:

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize 

    let word = textArray[indexPath.row]

    let font = UIFont.systemFont(ofSize: 17)
    let fontAttributes = [NSFontAttributeName: font]
    var size = (word as NSString).size(attributes: fontAttributes)
    size.width = size.width + 2
    return size

我使用以下代码重新排序集合视图:

override func viewDidLoad() 
    super.viewDidLoad()

    self.installsStandardGestureForInteractiveMovement = false
    let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(gesture:)))
    self.collectionView?.addGestureRecognizer(panGesture)



func handlePanGesture(gesture: UIPanGestureRecognizer) 
    switch gesture.state 
    case UIGestureRecognizerState.began :
        guard let selectedIndexPath = self.collectionView?.indexPathForItem(at: gesture.location(in: self.collectionView)) else 
            break
        
        collectionView?.beginInteractiveMovementForItem(at: selectedIndexPath)
        print("Interactive movement began")

    case UIGestureRecognizerState.changed :
        collectionView?.updateInteractiveMovementTargetPosition(gesture.location(in: gesture.view!))
        print("Interactive movement changed")

    case UIGestureRecognizerState.ended :
        collectionView?.endInteractiveMovement()
        print("Interactive movement ended")

    default:
        collectionView?.cancelInteractiveMovement()
        print("Interactive movement canceled")
    


override func collectionView(_ collectionView: UICollectionView, moveItemAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) 

    // Swap values if sorce and destination
    let change = textArray[sourceIndexPath.row]


    textArray.remove(at: sourceIndexPath.row)
    textArray.insert(change, at: destinationIndexPath.row)

    // Reload data to recalculate dimensions for the cells
    collectionView.reloadData()

视图如下所示:

问题是在重新排序期间,单元格在 indexPath 处保持原始单元格的尺寸,因此在重新排序期间,视图如下所示: 目前我已经解决了在重新排序结束时重新加载数据的问题,以重新计算正确的尺寸。 如何在交互式移动和重新排序自定义大小的单元格期间也保持单元格的正确尺寸?

【问题讨论】:

对不起,如果我在语言上犯了一些错误,我是意大利人 【参考方案1】:

这整个星期都困扰着我,所以我今晚坐下来尝试寻找解决方案。我认为您需要的是为您的集合视图自定义布局管理器,它可以随着顺序的变化动态调整每个单元格的布局。

下面的代码显然比你上面的布局更粗糙,但从根本上实现了你想要的行为:当单元格重新排序时,关键是移动到新的布局,“即时”发生,不需要任何临时调整。

这一切的关键是视图控制器的 sourceData 变量中的 didSet 函数。当这个数组的值改变时(通过按下排序按钮 - 我对你的手势识别器的粗略近似),这会自动触发所需单元格尺寸的重新计算,然后还会触发布局以清除自身并重新计算并重新加载集合视图数据。

如果您对此有任何疑问,请告诉我。希望能帮助到你!

更新:好的,我了解您现在想要做什么,我认为附加的更新代码可以让您到达那里。而不是使用内置的交互方法,我认为考虑到我已经实现了自定义布局管理器来使用委托的方式更容易:当平移手势识别器选择一个单元格时,我们创建一个基于该单词的子视图,该单词随手势。同时在后台我们从数据源中移除单词并刷新布局。当用户选择一个位置来放置单词时,我们反转该过程,告诉代理将单词插入数据源并刷新布局。如果用户将单词拖到集合视图之外或到无效位置,则单词会简单地放回它开始的位置(使用将原始索引存储为标签标签的狡猾技术)。

希望能帮到你!

[文字由***提供]

import UIKit

class ViewController: UIViewController, bespokeCollectionViewControllerDelegate 

     let sourceText : String = "So Midas, king of Lydia, swelled at first with pride when he found he could transform everything he touched to gold; but when he beheld his food grow rigid and his drink harden into golden ice then he understood that this gift was a bane and in his loathing for gold, cursed his prayer"

    var sourceData : [String]! 
        didSet 
            refresh()
        
    
    var sortedCVController : UICollectionViewController!
    var sortedLayout : bespokeCollectionViewLayout!
    var sortButton : UIButton!
    var sortDirection : Int = 0

    override func viewDidLoad() 
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.

        sortedLayout = bespokeCollectionViewLayout(contentWidth: view.frame.width - 200)
        sourceData = 
            let components = sourceText.components(separatedBy: " ")
            return components
        ()

        sortedCVController = bespokeCollectionViewController(sourceData: sourceData, collectionViewLayout: sortedLayout, frame: CGRect(origin: CGPoint(x: 100, y: 100), size: CGSize(width: view.frame.width - 200, height: view.frame.height - 200)))
        (sortedCVController as! bespokeCollectionViewController).delegate = self
        sortedCVController.collectionView!.frame = CGRect(origin: CGPoint(x: 100, y: 100), size: CGSize(width: view.frame.width - 200, height: view.frame.height - 200))

        sortButton = 
            let sB : UIButton = UIButton(frame: CGRect(origin: CGPoint(x: 25, y: 100), size: CGSize(width: 50, height: 50)))
            sB.setTitle("Sort", for: .normal)
            sB.setTitleColor(UIColor.black, for: .normal)
            sB.addTarget(self, action: #selector(sort), for: .touchUpInside)
            sB.layer.borderColor = UIColor.black.cgColor
            sB.layer.borderWidth = 1.0
            return sB
        ()

        view.addSubview(sortedCVController.collectionView!)
        view.addSubview(sortButton)
    

    func refresh() -> Void 
        let dimensions : [CGSize] = 
            var d : [CGSize] = [CGSize]()
            let font = UIFont.systemFont(ofSize: 17)
            let fontAttributes = [NSFontAttributeName : font]
            for item in sourceData 
                let stringSize = ((item + " ") as NSString).size(attributes: fontAttributes)
                d.append(CGSize(width: stringSize.width, height: stringSize.height))
            
            return d
        ()

        if self.sortedLayout != nil 
            sortedLayout.dimensions = dimensions
            if let _ = sortedCVController 
                (sortedCVController as! bespokeCollectionViewController).sourceData = sourceData
            
            self.sortedLayout.cache.removeAll()
            self.sortedLayout.prepare()
            if let _ = self.sortedCVController 

                self.sortedCVController.collectionView?.reloadData()
            
        
    


    func sort() -> Void 
        sourceData = sortDirection > 0 ? sourceData.sorted(by:  $0 > $1 ) : sourceData.sorted(by:  $0 < $1 )
        sortDirection = sortDirection + 1 > 1 ? 0 : 1
    

    func didMoveWord(atIndex: Int) 
        sourceData.remove(at: atIndex)
    

    func didPlaceWord(word: String, atIndex: Int) 
        print(atIndex)
        if atIndex >= sourceData.count 
            sourceData.append(word)
        
        else
        
            sourceData.insert(word, at: atIndex)
        

    

    func pleaseRefresh() 
        refresh()
    



protocol bespokeCollectionViewControllerDelegate 
    func didMoveWord(atIndex: Int) -> Void
    func didPlaceWord(word: String, atIndex: Int) -> Void
    func pleaseRefresh() -> Void


class bespokeCollectionViewController : UICollectionViewController 

    var sourceData : [String]
    var movingLabel : UILabel!
    var initialOffset : CGPoint!
    var delegate : bespokeCollectionViewControllerDelegate!

    init(sourceData: [String], collectionViewLayout: bespokeCollectionViewLayout, frame: CGRect) 
        self.sourceData = sourceData
        super.init(collectionViewLayout: collectionViewLayout)

        self.collectionView = UICollectionView(frame: frame, collectionViewLayout: collectionViewLayout)
        self.collectionView?.backgroundColor = UIColor.white
        self.collectionView?.layer.borderColor = UIColor.black.cgColor
        self.collectionView?.layer.borderWidth = 1.0

        self.installsStandardGestureForInteractiveMovement = false

        let pangesture = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(gesture:)))
        self.collectionView?.addGestureRecognizer(pangesture)
    

    required init?(coder aDecoder: NSCoder) 
        fatalError("init(coder:) has not been implemented")
    

    func handlePanGesture(gesture: UIPanGestureRecognizer) 
        guard let _ = delegate else  return 

        switch gesture.state 
        case UIGestureRecognizerState.began:
            guard let selectedIndexPath = self.collectionView?.indexPathForItem(at: gesture.location(in: self.collectionView)) else  break 
            guard let selectedCell : UICollectionViewCell = self.collectionView?.cellForItem(at: selectedIndexPath) else  break 
            initialOffset = gesture.location(in: selectedCell)

            let index : Int = 
                var i : Int = 0
                for sectionCount in 0..<selectedIndexPath.section 
                    i += (self.collectionView?.numberOfItems(inSection: sectionCount))!
                
                i += selectedIndexPath.row
                return i
            ()


            movingLabel = 
                let mL : UILabel = UILabel()
                mL.font = UIFont.systemFont(ofSize: 17)
                mL.frame = selectedCell.frame
                mL.textColor = UIColor.black
                mL.text = sourceData[index]
                mL.layer.borderColor = UIColor.black.cgColor
                mL.layer.borderWidth = 1.0
                mL.backgroundColor = UIColor.white
                mL.tag = index
                return mL
            ()

            self.collectionView?.addSubview(movingLabel)

            delegate.didMoveWord(atIndex: index)
        case UIGestureRecognizerState.changed:
            if let _ = movingLabel 
                movingLabel.frame.origin = CGPoint(x: gesture.location(in: self.collectionView).x - initialOffset.x, y: gesture.location(in: self.collectionView).y - initialOffset.y)
            

        case UIGestureRecognizerState.ended:
            print("Interactive movement ended")
            if let selectedIndexPath = self.collectionView?.indexPathForItem(at: gesture.location(in: self.collectionView)) 
                 guard let _ = movingLabel else  return 

                let index : Int = 
                    var i : Int = 0
                    for sectionCount in 0..<selectedIndexPath.section 
                        i += (self.collectionView?.numberOfItems(inSection: sectionCount))!
                    
                    i += selectedIndexPath.row
                    return i
                ()

                delegate.didPlaceWord(word: movingLabel.text!, atIndex: index)
                UIView.animate(withDuration: 0.25, animations: 
                    self.movingLabel.alpha = 0
                    self.movingLabel.removeFromSuperview()
                    , completion:  _ in
                        self.movingLabel = nil )
            
            else
            
                if let _ = movingLabel 
                    delegate.didPlaceWord(word: movingLabel.text!, atIndex: movingLabel.tag)
                    UIView.animate(withDuration: 0.25, animations: 
                        self.movingLabel.alpha = 0
                        self.movingLabel.removeFromSuperview()
                    , completion:  _ in
                        self.movingLabel = nil )
                
            

        default:
            collectionView?.cancelInteractiveMovement()
            print("Interactive movement canceled")
        
    

    override func numberOfSections(in collectionView: UICollectionView) -> Int 
        guard !(self.collectionViewLayout as! bespokeCollectionViewLayout).cache.isEmpty else  return 0 

        return (self.collectionViewLayout as! bespokeCollectionViewLayout).cache.last!.indexPath.section + 1
    

    override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int 
        guard !(self.collectionViewLayout as! bespokeCollectionViewLayout).cache.isEmpty else  return 0 

        var n : Int = 0
        for element in (self.collectionViewLayout as! bespokeCollectionViewLayout).cache 
            if element.indexPath.section == section 
                if element.indexPath.row > n 
                    n = element.indexPath.row
                
            
        
        print("Section \(section) has \(n) elements")
        return n + 1
    

    override func collectionView(_ collectionView: UICollectionView, moveItemAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) 
        let change = sourceData[sourceIndexPath.row]

        sourceData.remove(at: sourceIndexPath.row)
        sourceData.insert(change, at: destinationIndexPath.row)
    

    override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell 
        collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "cell")

        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)

        // Clean
        for subview in cell.subviews 
            subview.removeFromSuperview()
        

        let label : UILabel = 
            let l : UILabel = UILabel()
            l.font = UIFont.systemFont(ofSize: 17)
            l.frame = CGRect(origin: CGPoint.zero, size: cell.frame.size)
            l.textColor = UIColor.black

            let index : Int = 
                var i : Int = 0
                for sectionCount in 0..<indexPath.section 
                    i += (self.collectionView?.numberOfItems(inSection: sectionCount))!
                
                i += indexPath.row
                return i
            ()

            l.text = sourceData[index]
            return l
        ()

        cell.addSubview(label)

        return cell
    




class bespokeCollectionViewLayout : UICollectionViewLayout 

    var cache : [UICollectionViewLayoutAttributes] = [UICollectionViewLayoutAttributes]()
    let contentWidth: CGFloat
    var dimensions : [CGSize]!

    init(contentWidth: CGFloat) 
        self.contentWidth = contentWidth

        super.init()
    

    required init?(coder aDecoder: NSCoder) 
        fatalError("init(coder:) has not been implemented")
    

    override func prepare() -> Void 
        guard self.dimensions != nil else  return 
        if cache.isEmpty 
            var xOffset : CGFloat = 0
            var yOffset : CGFloat = 0

            var rowCount = 0
            var wordCount : Int = 0

            while wordCount < dimensions.count 
                let nextRowCount : Int = 
                    var totalWidth : CGFloat = 0
                    var numberOfWordsInRow : Int = 0

                    while totalWidth < contentWidth && wordCount < dimensions.count 
                        if totalWidth + dimensions[wordCount].width >= contentWidth 
                            break
                        
                        else
                        
                            totalWidth += dimensions[wordCount].width
                            wordCount += 1
                            numberOfWordsInRow += 1
                        

                    
                    return numberOfWordsInRow
                ()

                var columnCount : Int = 0
                for count in (wordCount - nextRowCount)..<wordCount 
                    let index : IndexPath = IndexPath(row: columnCount, section: rowCount)
                    let newAttribute : UICollectionViewLayoutAttributes = UICollectionViewLayoutAttributes(forCellWith: index)
                    let cellFrame : CGRect = CGRect(origin: CGPoint(x: xOffset, y: yOffset), size: dimensions[count])
                    newAttribute.frame = cellFrame
                    cache.append(newAttribute)

                    xOffset += dimensions[count].width
                    columnCount += 1
                

                xOffset = 0
                yOffset += dimensions[0].height

                rowCount += 1

            
        
    

    override var collectionViewContentSize: CGSize 
        guard !cache.isEmpty else  return CGSize(width: 100, height: 100) 
        return CGSize(width: self.contentWidth, height: cache.last!.frame.maxY)
    

    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? 
        var layoutAttributes = [UICollectionViewLayoutAttributes]()
        if cache.isEmpty 
            self.prepare()
        
        for attributes in cache 
            if attributes.frame.intersects(rect) 
                layoutAttributes.append(attributes)
            
        
        return layoutAttributes
    

【讨论】:

感谢您的工作,我将尝试理解并实施这一点。我会及时通知你 @ale00 没问题,我喜欢它。我很欣赏它相对复杂,但如果你运行它并通过代码工作,它应该是有意义的。我发现在这种情况下,定制的集合视图布局可以提供更多控制。 我已经运行了代码并且它运行良好,然后我尝试使用手势识别器实现重新排序,但是如果我在实际行中移动单元格,一个单元格会消失,如果我将它移到外面应用程序崩溃。这是我现在的代码:gist.github.com/ale00/a34c575e1d4f6f16774c68a921759d3b,更改从第 88 行开始。也许我做错了。 (抱歉英语语言错误) @ale00 请参阅上面的更新代码。我认为这现在实现了您正在寻找的东西,所以如果您认为它对您有所帮助,请随时接受它! 很抱歉我的不活动。当我尝试在我的应用程序中实现它时,我注意到集合视图的尺寸是在开始时设置的并且永远不会改变,所以如果我改变设备的方向会有视觉问题

以上是关于使用自定义维度重新排序集合视图单元格时出现问题的主要内容,如果未能解决你的问题,请参考以下文章

重新加载行以更新 UITableView 中的单元格高度时出现问题

如何使用自定义按钮重新排序表格视图单元格,但不使用 ios 中的移动编辑图标

集合视图单元格快速滚动到中心时出现问题

设置集合视图单元格的动态宽度时出现问题

在自定义单元格中的标签中分配值时出现异常

使用自动布局自定义集合视图单元格的动态高度