Drag and Reorder - Uicollectionview with Sections

Drag and reorder - UICollectionview with sections

Details

  • Xcode 10.2 (10E125), Swift 5

Links

Coding for iOS 11: How to drag & drop into collections & tables

Full Sample

ViewController

import UIKit

enum CellModel {
case simple(text: String)
case availableToDrop
}

class ViewController: UIViewController {

private lazy var cellIdentifier = "cellIdentifier"
private lazy var supplementaryViewIdentifier = "supplementaryViewIdentifier"
private lazy var sections = 10
private lazy var itemsInSection = 2
private lazy var numberOfElementsInRow = 3

private lazy var data: [[CellModel]] = {
var count = 0
return (0 ..< sections).map { _ in
return (0 ..< itemsInSection).map { _ -> CellModel in
count += 1
return .simple(text: "cell \(count)")
}
}
}()

override func viewDidLoad() {
super.viewDidLoad()

let collectionViewFlowLayout = UICollectionViewFlowLayout()
collectionViewFlowLayout.minimumLineSpacing = 5
collectionViewFlowLayout.minimumInteritemSpacing = 5
let _numberOfElementsInRow = CGFloat(numberOfElementsInRow)
let allWidthBetwenCells = _numberOfElementsInRow == 0 ? 0 : collectionViewFlowLayout.minimumInteritemSpacing*(_numberOfElementsInRow-1)
let width = (view.frame.width - allWidthBetwenCells)/_numberOfElementsInRow
collectionViewFlowLayout.itemSize = CGSize(width: width, height: width)
collectionViewFlowLayout.headerReferenceSize = CGSize(width: 0, height: 40)

let collectionView = UICollectionView(frame: .zero, collectionViewLayout: collectionViewFlowLayout)
collectionView.backgroundColor = .white
view.addSubview(collectionView)
collectionView.translatesAutoresizingMaskIntoConstraints = false
collectionView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true
collectionView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true
collectionView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor).isActive = true
collectionView.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor).isActive = true

collectionView.register(CollectionViewCell.self, forCellWithReuseIdentifier: cellIdentifier)
collectionView.register(SupplementaryView.self,
forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader,
withReuseIdentifier: supplementaryViewIdentifier)

collectionView.dragInteractionEnabled = true
collectionView.reorderingCadence = .fast
collectionView.dropDelegate = self
collectionView.dragDelegate = self

collectionView.delegate = self
collectionView.dataSource = self
}
}

extension ViewController: UICollectionViewDelegate { }
extension ViewController: UICollectionViewDataSource {

func numberOfSections(in collectionView: UICollectionView) -> Int { return data.count }
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return data[section].count
}

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
switch data[indexPath.section][indexPath.item] {
case .simple(let text):
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellIdentifier, for: indexPath) as! CollectionViewCell
cell.label?.text = text
cell.backgroundColor = .gray
return cell
case .availableToDrop:
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellIdentifier, for: indexPath) as! CollectionViewCell
cell.backgroundColor = UIColor.green.withAlphaComponent(0.3)
return cell
}
}

func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: supplementaryViewIdentifier, for: indexPath as IndexPath) as! SupplementaryView
return headerView
}
}

extension ViewController: UICollectionViewDragDelegate {

func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
let itemProvider = NSItemProvider(object: "\(indexPath)" as NSString)
let dragItem = UIDragItem(itemProvider: itemProvider)
dragItem.localObject = data[indexPath.section][indexPath.row]
return [dragItem]
}

func collectionView(_ collectionView: UICollectionView, itemsForAddingTo session: UIDragSession, at indexPath: IndexPath, point: CGPoint) -> [UIDragItem] {
let itemProvider = NSItemProvider(object: "\(indexPath)" as NSString)
let dragItem = UIDragItem(itemProvider: itemProvider)
dragItem.localObject = data[indexPath.section][indexPath.row]
return [dragItem]
}


func collectionView(_ collectionView: UICollectionView, dragSessionWillBegin session: UIDragSession) {
var itemsToInsert = [IndexPath]()
(0 ..< data.count).forEach {
itemsToInsert.append(IndexPath(item: data[$0].count, section: $0))
data[$0].append(.availableToDrop)
}
collectionView.insertItems(at: itemsToInsert)
}

func collectionView(_ collectionView: UICollectionView, dragSessionDidEnd session: UIDragSession) {
var removeItems = [IndexPath]()
for section in 0..<data.count {
for item in 0..<data[section].count {
switch data[section][item] {
case .availableToDrop: removeItems.append(IndexPath(item: item, section: section))
case .simple: break
}
}
}
removeItems.forEach { data[$0.section].remove(at: $0.item) }
collectionView.deleteItems(at: removeItems)
}
}

extension ViewController: UICollectionViewDropDelegate {
func collectionView(_ collectionView: UICollectionView, performDropWith coordinator: UICollectionViewDropCoordinator) {
let destinationIndexPath: IndexPath
if let indexPath = coordinator.destinationIndexPath {
destinationIndexPath = indexPath
} else {
let section = collectionView.numberOfSections - 1
let row = collectionView.numberOfItems(inSection: section)
destinationIndexPath = IndexPath(row: row, section: section)
}

switch coordinator.proposal.operation {
case .move:
reorderItems(coordinator: coordinator, destinationIndexPath:destinationIndexPath, collectionView: collectionView)
case .copy:
copyItems(coordinator: coordinator, destinationIndexPath: destinationIndexPath, collectionView: collectionView)
default: return
}
}

func collectionView(_ collectionView: UICollectionView, canHandle session: UIDropSession) -> Bool { return true }
func collectionView(_ collectionView: UICollectionView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UICollectionViewDropProposal {
if collectionView.hasActiveDrag, let destinationIndexPath = destinationIndexPath {
switch data[destinationIndexPath.section][destinationIndexPath.row] {
case .simple:
return UICollectionViewDropProposal(operation: .move, intent: .insertAtDestinationIndexPath)
case .availableToDrop:
return UICollectionViewDropProposal(operation: .move, intent: .insertIntoDestinationIndexPath)
}
} else { return UICollectionViewDropProposal(operation: .forbidden) }
}

private func reorderItems(coordinator: UICollectionViewDropCoordinator, destinationIndexPath: IndexPath, collectionView: UICollectionView) {
let items = coordinator.items
if items.count == 1, let item = items.first,
let sourceIndexPath = item.sourceIndexPath,
let localObject = item.dragItem.localObject as? CellModel {

collectionView.performBatchUpdates ({
data[sourceIndexPath.section].remove(at: sourceIndexPath.item)
data[destinationIndexPath.section].insert(localObject, at: destinationIndexPath.item)
collectionView.deleteItems(at: [sourceIndexPath])
collectionView.insertItems(at: [destinationIndexPath])
})
}
}

private func copyItems(coordinator: UICollectionViewDropCoordinator, destinationIndexPath: IndexPath, collectionView: UICollectionView) {
collectionView.performBatchUpdates({
var indexPaths = [IndexPath]()
for (index, item) in coordinator.items.enumerated() {
if let localObject = item.dragItem.localObject as? CellModel {
let indexPath = IndexPath(row: destinationIndexPath.row + index, section: destinationIndexPath.section)
data[indexPath.section].insert(localObject, at: indexPath.row)
indexPaths.append(indexPath)
}
}
collectionView.insertItems(at: indexPaths)
})
}
}

Cells

import UIKit

class CollectionViewCell: UICollectionViewCell {
weak var label: UILabel?

override init(frame: CGRect) {
super.init(frame: frame)
clipsToBounds = true
let label = UILabel(frame: .zero)
label.contentMode = .scaleAspectFill
addSubview(label)
label.translatesAutoresizingMaskIntoConstraints = false
label.topAnchor.constraint(equalTo: topAnchor).isActive = true
label.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
label.leftAnchor.constraint(equalTo: leftAnchor).isActive = true
label.rightAnchor.constraint(equalTo: rightAnchor).isActive = true
label.textAlignment = .center
label.textColor = .white
self.label = label
layer.borderWidth = 1
layer.borderColor = UIColor.white.cgColor
backgroundColor = .white
}

required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}

override func prepareForReuse() {
super.prepareForReuse()
label?.text = nil
backgroundColor = .white
}
}

import UIKit

class SupplementaryView: UICollectionReusableView {

weak var label: UILabel?
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = UIColor.blue.withAlphaComponent(0.7)
}

required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}

Result

Sample Image

UICollectionView with SwiftUI + Drag and drop reordering possible?

Just use UICollectionViewDragDelegate and UICollectionViewDropDelegate to drag and drop cell views inside UICollectionView. It works perfectly. Here is the sample code...

struct ContentView: View {
var numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
var body: some View {
GeometryReader { proxy in
GridView(self.numbers, proxy: proxy) { number in
Image("image\(number)")
.resizable()
.scaledToFill()
}
}
}
}
struct GridView<CellView: View>: UIViewRepresentable {
let cellView: (Int) -> CellView
let proxy: GeometryProxy
var numbers: [Int]
init(_ numbers: [Int], proxy: GeometryProxy, @ViewBuilder cellView: @escaping (Int) -> CellView) {
self.proxy = proxy
self.cellView = cellView
self.numbers = numbers
}
func makeUIView(context: Context) -> UICollectionView {
let layout = UICollectionViewFlowLayout()
layout.minimumLineSpacing = 0
layout.minimumInteritemSpacing = 0

let collectionView = UICollectionView(frame: UIScreen.main.bounds, collectionViewLayout: layout)
collectionView.backgroundColor = .white
collectionView.register(GridCellView.self, forCellWithReuseIdentifier: "CELL")

collectionView.dragDelegate = context.coordinator //to drag cell view
collectionView.dropDelegate = context.coordinator //to drop cell view

collectionView.dragInteractionEnabled = true
collectionView.dataSource = context.coordinator
collectionView.delegate = context.coordinator
collectionView.contentInset = UIEdgeInsets(top: 4, left: 4, bottom: 4, right: 4)
return collectionView
}
func updateUIView(_ uiView: UICollectionView, context: Context) { }
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UICollectionViewDelegateFlowLayout, UICollectionViewDataSource, UICollectionViewDragDelegate, UICollectionViewDropDelegate {
var parent: GridView
init(_ parent: GridView) {
self.parent = parent
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return parent.numbers.count
}

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CELL", for: indexPath) as! GridCellView
cell.backgroundColor = .clear
cell.cellView.rootView = AnyView(parent.cellView(parent.numbers[indexPath.row]).fixedSize())
return cell
}

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: ((parent.proxy.frame(in: .global).width - 8) / 3), height: ((parent.proxy.frame(in: .global).width - 8) / 3))
}

//Provides the initial set of items (if any) to drag.
func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
let item = self.parent.numbers[indexPath.row]
let itemProvider = NSItemProvider(object: String(item) as NSString)
let dragItem = UIDragItem(itemProvider: itemProvider)
dragItem.localObject = item
return [dragItem]
}

//Tells your delegate that the position of the dragged data over the collection view changed.
func collectionView(_ collectionView: UICollectionView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UICollectionViewDropProposal {
if collectionView.hasActiveDrag {
return UICollectionViewDropProposal(operation: .move, intent: .insertAtDestinationIndexPath)
}
return UICollectionViewDropProposal(operation: .forbidden)
}

//Tells your delegate to incorporate the drop data into the collection view.
func collectionView(_ collectionView: UICollectionView, performDropWith coordinator: UICollectionViewDropCoordinator) {
var destinationIndexPath: IndexPath
if let indexPath = coordinator.destinationIndexPath {
destinationIndexPath = indexPath
} else {
let row = collectionView.numberOfItems(inSection: 0)
destinationIndexPath = IndexPath(item: row - 1, section: 0)
}
if coordinator.proposal.operation == .move {
self.reorderItems(coordinator: coordinator, destinationIndexPath: destinationIndexPath, collectionView: collectionView)
}
}
private func reorderItems(coordinator: UICollectionViewDropCoordinator, destinationIndexPath: IndexPath, collectionView: UICollectionView) {
if let item = coordinator.items.first, let sourceIndexPath = item.sourceIndexPath {
collectionView.performBatchUpdates({
self.parent.numbers.remove(at: sourceIndexPath.item)
self.parent.numbers.insert(item.dragItem.localObject as! Int, at: destinationIndexPath.item)
collectionView.deleteItems(at: [sourceIndexPath])
collectionView.insertItems(at: [destinationIndexPath])
}, completion: nil)
coordinator.drop(item.dragItem, toItemAt: destinationIndexPath)
}
}
}
}
class GridCellView: UICollectionViewCell {
public var cellView = UIHostingController(rootView: AnyView(EmptyView()))
public override init(frame: CGRect) {
super.init(frame: frame)
configure()
}
public required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
configure()
}
private func configure() {
contentView.addSubview(cellView.view)
cellView.view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
cellView.view.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 5),
cellView.view.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -5),
cellView.view.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 5),
cellView.view.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -5),
])
cellView.view.layer.masksToBounds = true
}
}

You can see the final result here https://media.giphy.com/media/UPuWLauQepwi5Q77PA/giphy.gif
Thanks. X_X

Swift. Correctly reordering multiple items at once in a collectionView using drag and drop

The problem was how to calculate correct indexPaths in the collection view as pages are being manually deleted and inserted before performBatchUpdates was called. I was able to solve the problem by inspecting the model (array of items in the collectionView) and getting the new indexPath of the current item within each loop. Was easier than manually trying to calculate how each deletion and insertion affected the system provided indexPath of subsequent drag items

Rearranging different-sized items of UICollectionView with UICollectionViewFlowLayout

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.



Related Topics



Leave a reply



Submit