Проблемы с переупорядочением Collection View Cell с пользовательскими размерами

Я хочу изменить порядок ячеек в представлении коллекции с настраиваемым размером для каждой ячейки.
В каждой ячейке 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()
}

Вид выглядит следующим образом: просмотр коллекции

Проблема заключается в том, что во время переупорядочения ячейки сохраняют размеры исходной ячейки в indexPath, поэтому во время переупорядочения вид выглядит следующим образом: переупорядочить На данный момент я исправил проблему перезагрузки данных в конце переупорядочения, чтобы пересчитать правильные размеры. Как я могу обладать правильными размерами для ячеек также во время интерактивного перемещения и изменять порядок размеров нестандартных ячеек?

Ответ 1

Это провоцировало меня всю неделю, поэтому я сел сегодня вечером, чтобы попытаться найти решение. Я думаю, что вам нужен настраиваемый менеджер макетов для вашего вида коллекции, который может динамически корректировать макет для каждой ячейки при изменении порядка.

Следующий код, очевидно, создает что-то намного более грубое, чем ваш макет выше, но в основном достигает желаемого поведения: кардинальное перемещение к новой компоновке при переупорядочивании ячеек происходит "мгновенно" без каких-либо промежуточных корректировок.

Ключом к этому является функция didSet в переменной sourceData контроллера вида. Когда значение этого массива изменяется (путем нажатия кнопки сортировки - мое грубое приближение к вашему распознавателю жестов), это автоматически вызывает пересчет требуемых размеров ячеек, что также заставляет макет очищать себя и пересчитывать, а просмотр коллекции для перезагрузки данные.

Если у вас есть какие-либо вопросы по любому из этих вопросов, дайте мне знать. Надеюсь, поможет!

UPDATE: Хорошо, я понимаю, что вы пытаетесь сделать сейчас, и я думаю, что добавленный обновленный код доставит вас туда. Вместо использования встроенных методов взаимодействия я считаю, что проще использовать способ, которым я использовал диспетчер компоновки для использования делегирования: когда распознающий жесты выбирает ячейку, мы создаем подзаголовок на основе этого слова, которое перемещается с помощью жест. В то же время в фоновом режиме мы удаляем слово из источника данных и обновляем макет. Когда пользователь выбирает место для размещения слова, мы отменяем этот процесс, сообщая делегату вставить слово в источник данных и обновить макет. Если пользователь перетаскивает слово вне представления коллекции или в недействительное местоположение, слово просто возвращается туда, где оно начиналось (используйте хитрое устройство хранения исходного индекса в качестве метки метки).

Надеюсь, что это поможет!

[Текст предоставлен Wikipedia]

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