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
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
How to Pause and Resume Nstimer.Scheduledtimerwithtimeinterval in Swift
Confusion Due to Swift Lacking Implicit Conversion of Cgfloat
How to Remove Spaces from a String in Swift
How to Line Break Long Large Title in iOS 11
Swift - Seeding Arc4Random_Uniform? or Alternative
Rotate an Object in Its Direction of Motion
Method' Is Ambiguous for Type Lookup in This Context, Error in Alamofire
When to Use [Self] VS [Weak Self] in Swift Blocks
Avaudioengine Crashes When Plug Headphones in or Out
How to Include Assets/Resources in a Swift Package Manager Library
How to Hide Labels in iOS-Charts
How to Open Settings App Programmatically
What's the Equivalent to String.Localizedstringwithformat(_:_:) for Swiftui's Localizedstringkey
Uicollectionview Compositionallayout Not Calling Uiscrolldelegate
Keyboard Overlaying Action Sheet in iOS 13.1 on Cncontactviewcontroller
How to Clear Alamofireimage Setimagewithurl Cache
Why Can the Keyword "Weak" Only Be Applied to Class and Class-Bound Protocol Types