ios - 使用自定义维度重新排序 Collection View Cell 时出现问题

标签 ios swift uicollectionview uicollectionviewcell

我想对 Collection View 中的单元格重新排序,并为每个单元格自定义大小。
在 Collection View 的每个单元格中都有一个带有单词的标签。
我使用以下代码设置每个单元格的尺寸:

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
}

我用这段代码重新排序 Collection View :

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

View 如下所示: collection view

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

最佳答案

这整个星期都困扰着我,所以我今晚坐下来尝试找到解决方案。我认为您需要的是用于 Collection View 的自定义布局管理器,它可以在顺序更改时动态调整每个单元格的布局。

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

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

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

更新:好的,我明白你现在想做什么,我认为附加的更新代码可以帮助你实现目标。与其使用内置的交互方法,我认为我已经实现了自定义布局管理器以使用委托(delegate)的方式更容易:当平移手势识别器选择一个单元格时,我们基于该单词创建一个 subview ,该单词随手势。同时在后台我们从数据源中移除单词并刷新布局。当用户选择放置单词的位置时,我们反转该过程,告诉代理将单词插入数据源并刷新布局。如果用户将单词拖到 Collection View 之外或拖到无效位置,则该单词会简单地放回到它开始的位置(使用巧妙的技术将原始索引存储为标签的标签)。

希望对您有所帮助!

[文字由维基百科提供]

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

关于ios - 使用自定义维度重新排序 Collection View Cell 时出现问题,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/39691358/

相关文章:

ios - SplitView 的 displayModeButtonItem 在 iPad (iOS8) 上不显示标题

ios - 在 ipad 上运行应用程序一次又一次地要求输入系统用户名和密码

ios - UIDocumentInteractionController 很棒,但是语言设置怎么样?

iphone - Swift - 应用程序在 IOS 8.4 中启动时崩溃,错误为 _NSURLSessionTaskPriorityDefault

ios - Swift Firebase 长时间延迟加载记录

ios - swift,为 ios 7 和 8 编写代码

ios - traitCollection 更改时更改单元格布局

iphone - UICollectionView布局

android - 通过 PhoneGap 中的短信共享手机地理位置坐标(适用于 Android/IOS)

ios - Collection View 树表示