Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Rearranging different-sized items of UICollectionView with UICollectionViewFlowLayout

Let's say I have a UICollectionView with a UICollectionViewFlowLayout, and my items are different sizes. So I've implemented collectionView(_:layout:sizeForItemAt:).

Now let's say I permit the user to rearrange items (collectionView(_:canMoveItemAt:)).

Here's the problem. As a cell is being dragged and other cells are moving out of its way, collectionView(_:layout:sizeForItemAt:) is called repeatedly. But it's evidently called for the wrong index paths: a cell is sized with the index path for the place it has been visually moved to. Therefore it adopts the wrong size during the drag as it shuttles into a different position.

enter image description here

Once the drag is over and collectionView(_:moveItemAt:to:) is called, and I update the data model and reload the data, all the cells assume their correct size. The problem occurs only during the drag.

We clearly are not being given enough information in collectionView(_:layout:sizeForItemAt:) to know what answer to return while the drag is going on. Or maybe I should say, we're being asked for the size for the wrong index path.

My question is: what on earth are people doing about this?

like image 570
matt Avatar asked Jul 24 '18 15:07

matt


1 Answers

The trick is to implement

override func collectionView(_ collectionView: UICollectionView, 
    targetIndexPathForMoveFromItemAt orig: IndexPath, 
    toProposedIndexPath prop: IndexPath) -> IndexPath {

During a drag, that method is called repeatedly, but there comes a moment where a cell crosses another and cells are shoved out of the way to compensate. At that moment, orig and prop have different values. So at that moment you need to revise all your sizes in accordance with how the cells have moved.

To do that, you need to simulate in your rearrangement of sizes what the interface is doing as the cells move around. The runtime gives you no help with this!

Here's a simple example. Presume that the user can move a cell only within the same section. And presume that our data model looks like this, with each Item remembering its own size once collectionView(_:layout:sizeForItemAt:) has initially calculated it:

struct Item {
    var size : CGSize
    // other stuff
}
struct Section {
    var itemData : [Item]
    // other stuff
}
var sections : [Section]!

Here's how sizeForItemAt: memoizes the calculated sizes into the model:

func collectionView(_ collectionView: UICollectionView, 
layout collectionViewLayout: UICollectionViewLayout, 
sizeForItemAt indexPath: IndexPath) -> CGSize {
    let memosize = self.sections[indexPath.section].itemData[indexPath.row].size
    if memosize != .zero {
        return memosize
    }
    // no memoized size; calculate it now
    // ... not shown ...
    self.sections[indexPath.section].itemData[indexPath.row].size = sz // memoize
    return sz
}

Then as we hear that the user has dragged in a way that makes the cells shift, we read in all the size values for this section, perform the same remove-and-insert that the interface has done, and put the rearranged size values back into the model:

override func collectionView(_ collectionView: UICollectionView, 
targetIndexPathForMoveFromItemAt orig: IndexPath, toProposedIndexPath 
prop: IndexPath) -> IndexPath {
    if orig.section != prop.section {
        return orig
    }
    if orig.item == prop.item {
        return prop
    }
    // they are different, we're crossing a boundary - shift size values!
    var sizes = self.sections[orig.section].rowData.map{$0.size}
    let size = sizes.remove(at: orig.item)
    sizes.insert(size, at:prop.item)
    for (ix,size) in sizes.enumerated() {
        self.sections[orig.section].rowData[ix].size = size
    }
    return prop
}

The result is that collectionView(_:layout:sizeForItemAt:) now gives the right result during the drag.

The extra piece of the puzzle is that when the drag starts you need to save off all the original sizes, and when the drag ends you need to restore them all, so that when the drag ends the result will be correct as well.

like image 155
matt Avatar answered Oct 01 '22 03:10

matt