Implementing re-ordering gestures for UICollectionView - ios

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

Related

collectionView :didSelectItemAtIndexPath called with a delay

I have a collection view which loads a list of products and there is no fancy functionality in it. It just loads data from API. The problem is that when i try to tap on one of the items in the collection view
collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath)
is not getting called. But when i long press on a cell it gets called.
I am pretty sure that there are no gestures applied on the cell.
Can anyone give me some guidelines to tackle the problem.
The code base that i have is as follows
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let cellTapped = (self.storeSearchListingCollectionViewOutlet.cellForItem(at: indexPath)) as! StoreSearchProdListingCollectionViewCell
UStoreShareClass.storeSharedInstance.longTappedProductID = String(cellTapped.tag)
if((UStoreShareClass.storeSharedInstance.longTappedProductID) != nil)
{
self.performSegue(withIdentifier: "searchToProductDetailSegue", sender: self)
}
else
{
SetDefaultWrappers().showAlert(info:SERVER_DOWN_ERROR_ALERT, viewController: self)
}
}
View Hierarchy is as follows
Thanks in Advance...!!!
This is intentional. On the Storyboard go to the Collection View and uncheck "Delay Touch Down" in the Attribute inspector.
May have two reasons:
1) May be first reason is:
Unfortunately your code is not getting main thread to work on UI. So you have to put your UI related part in MAIN thread like this.
DispatchQueue.main.async {
// PUT YOUR CODE HERE
if((UStoreShareClass.storeSharedInstance.longTappedProductID) != nil)
{
self.performSegue(withIdentifier: "searchToProductDetailSegue", sender: self)
}
else
{
SetDefaultWrappers().showAlert(info:SERVER_DOWN_ERROR_ALERT, viewController: self)
}
}
2) In second reason:
Have you made collectionView bouncing turned off?
In Storyboard, make checked "Bounce on scroll" checkbox of collectionView, if unchecked.
For future readers of this old question - probably me in the future ;)
I had a UITapGestureRecognizer which was swallowing my short taps (in the UICollectionView's grandparent) and hence only the old taps were making it through! I recon you have the same problem. You just need to add tapRecognizer.cancelsTouchesInView = false!
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(tapDidTouch(sender:)))
tapRecognizer.cancelsTouchesInView = false
scrollView.addGestureRecognizer(tapRecognizer)
First of all , put breakpoint in if condition and check if it gets inside the didselect block. Then,
Try adding your "performsegue" code in main thread like below
if((UStoreShareClass.storeSharedInstance.longTappedProductID) != nil)
{
DispatchQueue.main.async
{
self.performSegue(withIdentifier: "searchToProductDetailSegue", sender: self)
}
}
else
{
SetDefaultWrappers().showAlert(info:SERVER_DOWN_ERROR_ALERT, viewController: self)
}

UICollectionView not calling moveItemAt:To: Data Source method in response to endInteractiveMovement()

I'm having a difficult time getting interactive reordering to work in a UICollectionView embedded in a subclassed UIViewController. There is no custom layout object.
The view controller is set as the delegate and datasource for the collection view, and the collection view is given a long press gesture recognizer as outlined here and here. I've also implemented the datasource methods canMoveItemAt: and moveItemAt:To:
I have another collection view in the view controller which shouldn't reorder, and the VC is the delegate/datasource of both. Thus:
func collectionView(_ collectionView: UICollectionView, canMoveItemAt indexPath: IndexPath) -> Bool {
if collectionView == cardCollectionView {
return true
}
else {
return false
}
}
I can confirm that this datasource method is being called appropriately.
The handler of the long press gesture triggers the appropriate steps, and the cells begin their rearranging behavior during the press. EXCEPT for the response to UIGestureRecognizerState.ended:, which triggers cardCollectionView.endInteractiveMovement(). This should lead to the moveItemAt:To datasource method, but it is never called.
#objc func handleLongGesture(gesture: UILongPressGestureRecognizer) {
//print(cardCollectionView.delegate)
switch(gesture.state) {
case UIGestureRecognizerState.began:
guard let selectedIndexPath = self.cardCollectionView.indexPathForItem(at: gesture.location(in: self.cardCollectionView)) else {
break
}
cardCollectionView.beginInteractiveMovementForItem(at: selectedIndexPath)
case UIGestureRecognizerState.changed:
cardCollectionView.updateInteractiveMovementTargetPosition(gesture.location(in: gesture.view!))
case UIGestureRecognizerState.ended:
print("ending")
cardCollectionView.endInteractiveMovement()
default:
print("canceling")
cardCollectionView.cancelInteractiveMovement()
}
}
func collectionView(_ collectionView: UICollectionView, moveItemAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
print("Starting Index: \(sourceIndexPath.item)")
}
Rather, when the long press ends, the cells just all return to their initial location, and I never get a chance to effect the reordering of the data. What do I need to do to complete this reordering process?
Also, when the long press is initiated, the cells bounce around in the reordering animation, but I don't see the "lifted" cell under the press. Is that a separate issue, or are they related?
(Code is in Swift 4.0)
Having a UICollectionViewDropDelegate assigned to a UICollectionView interferes with interactive reordering (iOS 9 style) via the long press gesture handler method.
After tearing down my UIViewController and rebuilding it a line at a time, I isolated the place where interactive reordering failed. The culprit was the one in viewDidLoad where (for other reasons) I assigned the ViewController to be the dropDelegate for the CollectionView:
self.cardCollectionView.dropDelegate = self
The workaround was to simply implement reordering via the new iOS 11 drag and drop API's (WWDC 2017 session 223, around 32:00)

UICollectionview reorder image cells

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.

How to add tap gesture to UICollectionView , while maintaining cell selection?

Task
Add a single tap gesture to UICollectionView, do not get in the way of cell selection.
I want some other taps on the no-cell part of the collectionView.
Code
Using XCode8, Swift 3.
override func viewDidLoad() {
...
collectionView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(tap)))
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
print(indexPath)
}
func tap(sender: UITapGestureRecognizer){
print("tapped")
}
Result
Yeah, it gets in the way now. When you tap on cell, it logs "tapped".
Analysis
I check the hitTest return value of the collectionView and the cell. Both returned the tapped cell, which means they form a responder chain of Cell -> CollectionView
no gestures on the cell
3 gestures on collectionView, no one seems to work with the cell select
UIScrollViewDelayedTouchesBeganGestureRecognizer
UIScrollViewPanGestureRecognizer
UITapGestureRecognizer
callStack, seems cell selection has a different stack trace with gesture's target-action pattern.
double tap gesture works along with cell selection.
Question
Couldn't find more trace. Any ideas on how cell selection is implemented or to achieve this task?
Whenever you want to add a gesture recognizer, but not steal the touches from the target view, you should set UIGestureRecognizer.cancelsTouchesInView for your gestureRecognizer instance to false.
Instead of trying to force didSelectItem you can just get the indexPath and/or cell this way:
func tap(sender: UITapGestureRecognizer){
if let indexPath = self.collectionView?.indexPathForItem(at: sender.location(in: self.collectionView)) {
let cell = self.collectionView?.cellForItem(at: indexPath)
print("you can do something with the cell or index path here")
} else {
print("collection view was tapped")
}
}
I get another way: when adding gestures, set delegate, implement below method
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
you can decide whether the gesture recognizer to receive the touch by your own logic, with position or the page's hidden property, based on your demand.

didSelectItemAtIndexPath Doesn't Work In Collection View Swift

I have been working on a new application and it displays Gifs in a Collection View. I am also using a custom collection view cell class for the cells in my collection view.
The method didSelectItemAtIndexPath doesn't work though ...
func collectionView(collectionView: UICollectionView, didSelectItemAtIndexPath indexPath: NSIndexPath) {
println("it worked")
// ^this did not print
}
How would I change it so I can get the indexPath of the clicked item using a gesture recognizer?
as #santhu said (https://stackoverflow.com/a/21970378/846780)
didSelectItemAtIndexPath is called when none of the subView of
collectionViewCell respond to that touch. As the textView respond to
those touches, so it won't forward those touches to its superView, so
collectionView won't get it.
So, you have a UILongPressGestureRecognizer and it avoid didSelectItemAtIndexPath call.
With UILongPressGestureRecognizer approach you need to handle handleLongPress delegate method. Basically you need to get gestureReconizer.locationInView and know the indexPath located at this point (gestureReconizer.locationInView).
func handleLongPress(gestureReconizer: UILongPressGestureRecognizer) {
if gestureReconizer.state != UIGestureRecognizerState.Ended {
return
}
let p = gestureReconizer.locationInView(self.collectionView)
let indexPath = self.collectionView.indexPathForItemAtPoint(p)
if let index = indexPath {
var cell = self.collectionView.cellForItemAtIndexPath(index)
// do stuff with your cell, for example print the indexPath
println(index.row)
} else {
println("Could not find index path")
}
}

Resources