I got a UICollectionview with XX images/cells and i want to make them swap places when dragging and dropping. I'm using the build-in reorder function but its not fulfilling my needs. Here is the code:
longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(handleLongGesture))
longPressGesture.minimumPressDuration = 0.2
longPressGesture.delaysTouchesBegan = true
cameraRollCollectionView.addGestureRecognizer(longPressGesture)
func handleLongGesture(gesture: UILongPressGestureRecognizer) {
switch(gesture.state) {
case UIGestureRecognizerState.began:
guard let selectedIndexPath = cameraRollCollectionView.indexPathForItem(at: gesture.location(in: cameraRollCollectionView)) else {
break
}
cameraRollCollectionView.beginInteractiveMovementForItem(at: selectedIndexPath)
case UIGestureRecognizerState.changed:
cameraRollCollectionView.updateInteractiveMovementTargetPosition(gesture.location(in: gesture.view!))
case UIGestureRecognizerState.ended:
cameraRollCollectionView.endInteractiveMovement()
default:
cameraRollCollectionView.cancelInteractiveMovement()
}
}
I basically just want the animation to only swap the moving cell and destination cell, not reordering the "neighbours" like the attached image.
I have it working in "the behind" with this code:
func collectionView(_ collectionView: UICollectionView,
moveItemAt sourceIndexPath: IndexPath,
to destinationIndexPath: IndexPath) {
let temp = selectedUIImages[sourceIndexPath.row]
selectedUIImages[sourceIndexPath.row] = selectedUIImages[destinationIndexPath.row]
selectedUIImages[destinationIndexPath.row] = temp
}
You could create a copy of the collectionViewCell's contentView when you recognize a long press upon a cell. Then you can make that copy move around with the touch (touchesMoved) while all the cells are still and when the user drops it (touchesEnded) you can calculate the indexPath for that location. When you have the indexPath, you can change the positions like you're already doing.
Related
I implement a simple drag and drop sample.
import UIKit
class ViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
private var collectionView: UICollectionView?
var colors: [UIColor] = [
.link,
.systemGreen,
.systemBlue,
.red,
.systemOrange,
.black,
.systemPurple,
.systemYellow,
.systemPink,
.link,
.systemGreen,
.systemBlue,
.red,
.systemOrange,
.black,
.systemPurple,
.systemYellow,
.systemPink
]
override func viewDidLoad() {
super.viewDidLoad()
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .vertical
layout.itemSize = CGSize(width: view.frame.size.width/3.2, height: view.frame.size.width/3.2)
layout.sectionInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
//collectionView?.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "cell")
let customCollectionViewCellNib = CustomCollectionViewCell.getUINib()
collectionView?.register(customCollectionViewCellNib, forCellWithReuseIdentifier: "cell")
collectionView?.delegate = self
collectionView?.dataSource = self
collectionView?.backgroundColor = .white
view.addSubview(collectionView!)
let gesture = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPressGesture))
collectionView?.addGestureRecognizer(gesture)
}
#objc func handleLongPressGesture(_ gesture: UILongPressGestureRecognizer) {
guard let collectionView = collectionView else {
return
}
switch gesture.state {
case .began:
guard let targetIndexPath = collectionView.indexPathForItem(at: gesture.location(in: self.collectionView)) else {
return
}
collectionView.beginInteractiveMovementForItem(at: targetIndexPath)
case .changed:
collectionView.updateInteractiveMovementTargetPosition(gesture.location(in: collectionView))
case .ended:
collectionView.endInteractiveMovement()
default:
collectionView.cancelInteractiveMovement()
}
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
collectionView?.frame = view.bounds
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return colors.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
cell.backgroundColor = colors[indexPath.row]
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: view.frame.size.width/3.2, height: view.frame.size.width/3.2)
}
func collectionView(_ collectionView: UICollectionView, canMoveItemAt indexPath: IndexPath) -> Bool {
return true
}
func collectionView(_ collectionView: UICollectionView, moveItemAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
let item = colors.remove(at: sourceIndexPath.row)
colors.insert(item, at: destinationIndexPath.row)
}
}
However, I notice that, if my UICollectionViewCell is created with XIB, it will randomly exhibit flickering behaviour, during drag and drop.
The CustomCollectionViewCell is a pretty straightforward code.
CustomCollectionViewCell.swift
import UIKit
extension UIView {
static func instanceFromNib() -> Self {
return getUINib().instantiate(withOwner: self, options: nil)[0] as! Self
}
static func getUINib() -> UINib {
return UINib(nibName: String(describing: self), bundle: nil)
}
}
class CustomCollectionViewCell: UICollectionViewCell {
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
}
Flickering
By using the following code
let customCollectionViewCellNib = CustomCollectionViewCell.getUINib()
collectionView?.register(customCollectionViewCellNib, forCellWithReuseIdentifier: "cell")
It will have the following random flickering behaviour - https://youtu.be/CbcUAHlRJKI
No flickering
However, if the following code is used instead
collectionView?.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "cell")
Things work fine. There are no flickering behaviour - https://youtu.be/QkV2HlIrXK8
May I know why it is so? How can I avoid the flickering behaviour, when my custom UICollectionView is created from XIB?
Please note that, the flickering behaviour doesn't happen all the time. It happens randomly. It is easier to reproduce the problem using real iPhone device, than simulator.
Here's the complete sample code - https://github.com/yccheok/xib-view-cell-cause-flickering
While we are rearranging cells in UICollectionView (gesture is active), it handles all of the cell movements for us (without having us to worry about changing dataSource while the rearrange is in flight).
At the end of this rearrange gesture, UICollectionView rightfully expects that we will reflect the change in our dataSource as well which you are doing correctly here.
func collectionView(_ collectionView: UICollectionView, moveItemAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
let item = colors.remove(at: sourceIndexPath.row)
colors.insert(item, at: destinationIndexPath.row)
}
Since UICollectionView expects a dataSource update from our side, it performs following steps -
Call our collectionView(_:, moveItemAt:, to:) implementation to provide us a chance to reflect the changes in dataSource.
Call our collectionView(_:, cellForItemAt:) implementation for the destinationIndexPath value from call #1, to re-create a new cell at that indexPath from scratch.
Okay, but why would it perform step 2 even if this is the correct cell to be at that indexPath?
It's because UICollectionView doesn't know for sure whether you actually made those dataSource changes or not. What happens if you don't make those changes? - now your dataSource & UI are out of sync.
In order to make sure that your dataSource changes are correctly reflected in the UI, it has to do this step.
Now when the cell is being re-created, you sometimes see the flicker. Let the UI reload the first time, put a breakpoint in the cellForItemAt: implementation at the first line and rearrange a cell. Right after rearrange completes, your program will pause at that breakpoint and you can see following on the screen.
Why does it not happen with UICollectionViewCell class (not XIB)?
It does (as noted by others) - it's less frequent. Using the above steps by putting a breakpoint, you can catch it in that state.
How to solve this?
Get a reference to the cell that's currently being dragged.
Return this instance from cellForItemAt: implementation.
var currentlyBeingDraggedCell: UICollectionViewCell?
var willRecreateCellAtDraggedIndexPath: Bool = false
#objc func handleLongPressGesture(_ gesture: UILongPressGestureRecognizer) {
guard let cv = collectionView else { return }
let location = gesture.location(in: cv)
switch gesture.state {
case .began:
guard let targetIndexPath = cv.indexPathForItem(at: location) else { return }
currentlyBeingDraggedCell = cv.cellForItem(at: targetIndexPath)
cv.beginInteractiveMovementForItem(at: targetIndexPath)
case .changed:
cv.updateInteractiveMovementTargetPosition(location)
case .ended:
willRecreateCellAtDraggedIndexPath = true
cv.endInteractiveMovement()
default:
cv.cancelInteractiveMovement()
}
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if willRecreateCellAtDraggedIndexPath,
let currentlyBeingDraggedCell = currentlyBeingDraggedCell {
self.willRecreateCellAtDraggedIndexPath = false
self.currentlyBeingDraggedCell = nil
return currentlyBeingDraggedCell
}
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
cell.contentView.backgroundColor = colors[indexPath.item]
return cell
}
Will this solve the problem 100%?
NO. UICollectionView will still remove the cell from it's view hierarchy and ask us for a new cell - we are just providing it with an existing cell instance (that we know is going to be correct according to our own implementation).
You can still catch it in the state where it disappears from UI before appearing again. However this time there's almost no work to be done, so it will be significantly faster and you will see the flickering less often.
BONUS
iOS 15 seems to be working on similar problems via UICollectionView.reconfigureItems APIs. See an explanation in following Twitter thread.
Whether these improvements will land in rearrange or not, we will have to see.
Other Observations
Your UICollectionViewCell subclass' XIB looks like following
However it should look like following (1st one is missing contentView wrapper, you get this by default when you drag a Collection View Cell to the XIB from the View library OR create a UICollectionViewCell subclass with XIB).
And your implementation uses -
cell.backgroundColor = colors[indexPath.row]
You should use contentView to do all the UI customization, also note the indexPath.item(vs row) that better fits with cellForItemAt: terminology (There are no differences in these values though). cellForRowAt: & indexPath.row are more suited for UITableView instances.
cell.contentView.backgroundColor = colors[indexPath.item]
UPDATE
Should I use this workaround for my app in production?
NO.
As noted by OP in the comments below -
The proposed workaround has 2 shortcomings.
(1) Missing cell
(2) Wrong content cell.
This is clearly visible in https://www.youtube.com/watch?v=uDRgo0Jczuw Even if you perform explicit currentlyBeingDraggedCell.backgroundColor = colors[indexPath.item] within if block, wrong content cell issue is still there.
The flickering is caused by the cell being recreated at its new position. You can try holding to the cell.
(only the relevant code is shown)
// keeps a reference to the cell being dragged
private weak var draggedCell: UICollectionViewCell?
// the flag is set when the dragging completes
private var didInteractiveMovementEnd = false
#objc func handleLongPressGesture(_ gesture: UILongPressGestureRecognizer) {
switch gesture.state {
case .began:
// keep cell reference
draggedCell = collectionView.cellForItem(at: targetIndexPath)
collectionView.beginInteractiveMovementForItem(at: targetIndexPath)
case .ended:
// reuse the cell in `cellForItem`
didInteractiveMovementEnd = true
collectionView.performBatchUpdates {
collectionView.endInteractiveMovement()
} completion: { completed in
self.draggedCell = nil
self.didInteractiveMovementEnd = false
}
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
// reuse the dragged cell
if didInteractiveMovementEnd, let draggedCell = draggedCell {
return draggedCell
}
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
...
}
I have a collectionView showing cells. When I drag/touch/slide my finger on an item, if the touch ends on the item, the cell is selected (segues to the details screen).
Is there any way to limit cell selection (didSelectItemAt indexPath) to a simple tap? i.e it shouldn't select the cell if finger is dragged on an item and the touch ends on it.
Is this the default behavior?
I feel like it might be the cause of a cryptic issue with my custom navigation.
Thanks
Do add Following in your cellForItem
let tap = UITapGestureRecognizer(target: self, action: #selector(cellTapped(tapGestureRecognizer:)))
tap.numberOfTapsRequired = 1
cell.addGestureRecognizer(tap)
And add following function
#IBAction func cellTapped(tapGestureRecognizer: UITapGestureRecognizer)
{
//Do your required work.
}
You can use UITapGestureRecognizer, cause it will only respond on Tap gesture:
#objc func tapAction(_ sender: UITapGestureRecognizer) {
// TODO: - Action you need
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: <CellReuseId>, for: indexPath)
let tap = UITapGestureRecognizer(target: self, action: #selector(tapAction(_:)))
cell.contentView.addGestureRecognizer(tap)
return cell
}
But in this way didSelectItemAt will not work.
I am using drag and drop functionality in collection view.its working fine but when I replace the values of array when user trap the cell into another position.I have reload the collection view.But when we drag and drop cell 0 and 1 collection view not reload all cell it reload only one cell.
I don't know what is the problem in my code.
Below is my code.
longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(self.handleLongGesture(gesture:)))
collection_view.addGestureRecognizer(longPressGesture)
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
{
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell_images", for: indexPath) as! AddImagesCollectionViewCell
cell.layer.borderWidth=1
if Details.Images[indexPath.item].sourceType == sourceType.URL
{
cell.img_images.downloadedFrom(link: Details.Images[indexPath.item].imageURL!, contentMode: .scaleAspectFill)
}
else
{
cell.img_images.image = Details.Images[indexPath.item].image
}
Details.Images[indexPath.row].sequence="\(indexPath.row + 1)"
return cell
}
func collectionView(_ collectionView: UICollectionView, canMoveItemAt indexPath: IndexPath) -> Bool
{
if indexPath.row == Details.Images.count
{
return false
}
return true
}
func collectionView(_ collectionView: UICollectionView, moveItemAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath)
{
print("Starting Index: \(sourceIndexPath.item)")
print("Ending Index: \(destinationIndexPath.item)")
let tempArray = Details.Images[sourceIndexPath.item]
Details.Images.remove(at: sourceIndexPath.item)
Details.Images.insert(tempArray, at: destinationIndexPath.item)
collectionView.reloadData()
}
#objc func handleLongGesture(gesture: UILongPressGestureRecognizer)
{
switch(gesture.state)
{
case .began:
guard let selectedIndexPath = collection_view.indexPathForItem(at: gesture.location(in: collection_view))
else
{
break
}
if !(selectedIndexPath.row == Details.Images.count)
{
collection_view.beginInteractiveMovementForItem(at: selectedIndexPath)
}
case .changed:
guard let selectedIndexPath = collection_view.indexPathForItem(at: gesture.location(in: collection_view))
else
{
break
}
if !(selectedIndexPath.row == Details.Images.count)
{
collection_view.updateInteractiveMovementTargetPosition(gesture.location(in: gesture.view!))
}
case .ended:
collection_view.endInteractiveMovement()
default:
collection_view.cancelInteractiveMovement()
}
}
I have same issue. Don't know why reloadData() isn't working after endInteractiveMovement() or cancelInteractiveMovement(), but I have a workaround for this.
Instead of using reloadData() use reloadSections(), like:
self.collectionView.cancelInteractiveMovement()
// Not working: self.collectionView.reloadData()
UIView.performWithoutAnimation {
self.collectionView.reloadSections(IndexSet(integer: 0))
}
Also if you use reloadData() in DispatchQueue.main.asyncAfter() it will work.
There is no bugs in your code, collectionView will reload only one cell (one you have draged).If you want to reload all collection view you need to call reloadData() after endInteractiveMovement().
I had this problem with reordering via drag-and-drop - the collection view won't reload all the cells whose positions change because of the move operation. I fixed it by calling moveItemAtIndexPath:toIndexPath: in my implementation of performDropWithCoordinator. See this question.
I am creating a screen in iOS Xcode 8.3.2 Swift 3.1 that has a horizontally scrolling UICollectionView on it with a single row of images. The problem is, I had thought the "automatic re-ordering" capability was part of the UICollectionView, when it actually is only implemented in the UICollectionViewController. As I just want a portion of the screen to scroll, I don't believe I can use the collection view controller -- the collection view is always full screen in that case, right? With the controller, I discovered installsStandardGestureForInteractiveMovement, which automatically implements a gesture recognizer.
I would like this same capability in my collection view, which is on a UIViewController. However, I have no idea which gestures I need to capture to implement cell re-ordering. Is any of the drag re-ordering "magic" built into the collection view itself? Is there a way I can use the controller, instead, and not have the collection view full screen?
I have implemented the following method to do the actual move of the data, but I'm not sure which gesture recognizer(s) to setup.
func collectionView(_ collectionView: UICollectionView,
moveItemAt sourceIndexPath: IndexPath,
to destinationIndexPath: IndexPath) {
let cellToMove = promoPages.remove(at: (sourceIndexPath as NSIndexPath).row)
promoPages.insert(cellToMove, at: (destinationIndexPath as NSIndexPath).row)
}
After reading this guide, and converting it to Swift3:
For UIViewController class:
override func viewDidLoad() {
super.viewDidLoad()
let g = UILongPressGestureRecognizer(target: self,
action: #selector(handleLongGesture(_:)))
clcImages?.addGestureRecognizer(g)
}
func handleLongGesture(_ gesture: UILongPressGestureRecognizer)
{
switch(gesture.state) {
case UIGestureRecognizerState.began:
guard let selectedIndexPath = clcImages?.indexPathForItem(at: gesture.location(in: clcImages)) else {
break
}
clcImages?.beginInteractiveMovementForItem(at: selectedIndexPath)
case UIGestureRecognizerState.changed:
clcImages?.updateInteractiveMovementTargetPosition(gesture.location(in: gesture.view!))
case UIGestureRecognizerState.ended:
clcImages?.endInteractiveMovement()
default:
clcImages?.cancelInteractiveMovement()
}
}
I have a CollectionView issue, I have a video showing the problem detailed below. When I click one cell it moves in a weird manner.
Here is my code:
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
selectedFilter = indexPath.row
if filters[indexPath.row] != "Todo" {
filteredNews = news.filter { $0.category == filters[indexPath.row] }
} else {
filteredNews = news
}
tableView.reloadData()
collectionView.reloadData()
}
My Cell is moving, (Just the last cell, don't know why).
I think it might be related to collectionView.reloadData() But I need to do that for updating the green bar you can see on this Video when I select a Cell.
How can I make it not move? Someone had had a similar problem?
I noticed you reloaded a tableView during collectionView didSelectItemAt. If that tableView is a superView of your collectionView that will be the exact reason why you are having this abnormal behaviour.
If it were not, I can offer 3 solutions:
This library have a view controller subclass that can create the effect you want to show.
Manually create a UIView/UIImageView that is not inside the collectionView but update it's position during the collectionView's didSelectItemAt delegate method to but visually over the cell instead - this would require some calculation, but your collectionView will not need to reload.
You can attempt to only reload the two affected cells using the collectionView's reloadItem(at: IndexPath) method.
Know that when you reload a table/collection view, it will not change the current visible cell. However any content in each cell will be affected.
Finally I Solve it! I removed collectionView.reloadData() and added my code to change colors inside didSelectItemAt changing current selected item and old selected item (I created a Variable to see which one was the old selected item).
If someone interested, here is my code:
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let oldSelectedFilter = selectedFilter
if selectedFilter != indexPath.row {
let oldIndexPath = IndexPath(item: oldSelectedFilter, section: 0)
selectedFilter = indexPath.row
if filters[indexPath.row] != "Todo" {
filteredNews = news.filter { $0.category == filters[indexPath.row] }
} else {
filteredNews = news
}
if let cell = collectionView.cellForItem(at: indexPath) as? FiltersCollectionViewCell {
cell.selectedView.backgroundColor = MainColor
}
if let cell = collectionView.cellForItem(at: oldIndexPath) as? FiltersCollectionViewCell {
cell.selectedView.backgroundColor = UIColor(red:0.31, green:0.33, blue:0.35, alpha:1.0)
}
tableView.reloadData()
}
}