Trouble moving CollectionViewCell to the end - ios

I have a UICollectionView in a UIViewController. I've configured a gesture recognizer to move cells. It works fine for moving a cell to any index except the end. Most aggravatingly, the app doesn't crash when I attempt to move a cell to the end--it just hangs. I can back out of ReorderViewControllerand go back to it. The view reloads normally.
I call this method from viewDidLoad to configure the gesture recognizer:
func configureGestureRecognizer() {
// configure longPressGestureRecognizer
longPressGesture = UILongPressGestureRecognizer(target: self, action: #selector(ReorderViewController.handleLongPressGesture))
longPressGesture.minimumPressDuration = 0.5
longPressGesture.delegate = self
self.collectionView.addGestureRecognizer(longPressGesture)
}
When the UILongPressGestureRecognizer is triggered, its handler is called:
func handleLongPressGesture(gesture: UILongPressGestureRecognizer) {
guard let selectedIndexPath = self.collectionView.indexPathForItem(at: gesture.location(in: self.collectionView)) else {
return
}
let selectedCell = collectionView.cellForItem(at: selectedIndexPath)
switch gesture.state {
case .began:
print("began")
editMode = true
collectionView.beginInteractiveMovementForItem(at: selectedIndexPath)
selectedCell?.isSelected = true
case .changed:
editMode = true
selectedCell?.isSelected = true
print("changed")
collectionView.updateInteractiveMovementTargetPosition(gesture.location(in: self.collectionView))
case .ended:
print("ended")
editMode = false
selectedCell?.isSelected = false
collectionView.endInteractiveMovement()
default:
print("default")
editMode = false
selectedCell?.isSelected = false
collectionView.cancelInteractiveMovement()
}
}
I can move cells with the gesture without any trouble so long as I'm not moving one to the end. Most annoyingly, the app doesn't crash--it just hangs. I can press the "Back" button on the NavBar and go to the prior ViewController without crashing and return to ReorderViewController.
Here's my code for moving cells:
func collectionView(_ collectionView: UICollectionView, moveItemAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) {
let stuffToReorder = currentRoutine?.myOrderedSet.mutableCopy() as! NSMutableOrderedSet
stuffToReorder.exchangeObject(at: sourceIndexPath.row, withObjectAt: destinationIndexPath.row)
currentRoutine?.myOrderedSet = stuffToReorder as NSOrderedSet
appDelegate.saveContext()
}
Any thoughts re: where my mistake is are greatly appreciated.

I think I've cracked it. My hunch about CoreData being the issue was a red herring (which is just as well as I don't have much experience of it!). The hang up was caused by the guard statement at the start of your handler method. Specifically, your method checks that there is a valid index path related to the gesture location; if the gesture moves out of the collection view, I think everything gets confused and therefore you get the hang (rather than a crash) as the function keeps exiting at that point. Moving things around a bit, however, seems to solve the problem:
func handleLongPressGesture(gesture: UILongPressGestureRecognizer) {
guard let _ = collectionVC.collectionView else { return }
switch gesture.state {
case .began:
guard let selectedIndexPath = collectionVC.collectionView!.indexPathForItem(at: gesture.location(in: collectionVC.collectionView)) else { return }
selectedCell = collectionVC.collectionView!.cellForItem(at: selectedIndexPath)
print("began")
lastGoodLocation = gesture.location(in: collectionVC.collectionView!)
collectionVC.collectionView!.beginInteractiveMovementForItem(at: selectedIndexPath)
selectedCell.isSelected = true
case .changed:
selectedCell?.isSelected = true
if collectionVC.collectionView!.frame.contains(gesture.location(in: view)) {
print(gesture.location(in: view))
print(collectionVC.collectionView!.frame)
print("INSIDE COLLECTION VIEW!")
collectionVC.collectionView!.updateInteractiveMovementTargetPosition(gesture.location(in: collectionVC.collectionView!))
lastGoodLocation = gesture.location(in: collectionVC.collectionView!)
}
else
{
print("OUTSIDE COLLECTION VIEW!")
collectionVC.collectionView!.updateInteractiveMovementTargetPosition(lastGoodLocation) // Not sure this is needed
}
print("changed")
case .ended:
print("ended")
selectedCell?.isSelected = false
collectionVC.collectionView!.endInteractiveMovement()
default:
print("default")
selectedCell?.isSelected = false
collectionVC.collectionView!.cancelInteractiveMovement()
}
}
Implementing things this way, I moved the guard statement for selectedCell into the .began case of your switch, as this is the only place that it is initialised. I therefore had to declare selectedCell as a class property so that it could be referenced within the other cases later on. I also introduced a CGPoint variable, lastGoodLocation, which stores the last location for which a valid index path is available - this way, if the gesture ends outside the collection view, the cell is sent to that index path.
Anyway, this is a bit rough but certainly seems to prevent the hang. Hope that helps!

Related

RxSwift: How to add gesture to UILabel?

I have a label with isUserInteractionEnabled set to true. Now, I need to add UITapGestureRecognizer for the label. Is there a way to add in Rx way.
I have looked at the RxSwift library here. Which they didn't provide any extension for adding gesture. The UILabel+Rx file has only text and attributedText.
Is there any workaround to add gesture to label?
A UILabel is not configured with a tap gesture recognizer out of the box, that's why RxCocoa does not provide the means to listen to a gesture directly on the label. You will have to add the gesture recognizer yourself. Then you can use Rx to observe events from the recognizer, like so:
let disposeBag = DisposeBag()
let label = UILabel()
label.text = "Hello World!"
let tapGesture = UITapGestureRecognizer()
label.addGestureRecognizer(tapGesture)
tapGesture.rx.event.bind(onNext: { recognizer in
print("touches: \(recognizer.numberOfTouches)") //or whatever you like
}).disposed(by: disposeBag)
Swift 5 (using RxGesture library).
Best and simplest option imho.
label
.rx
.tapGesture()
.when(.recognized) // This is important!
.subscribe(onNext: { [weak self] _ in
guard let self = self else { return }
self.doWhatYouNeedToDo()
})
.disposed(by: disposeBag)
Take care! If you don't use .when(.recognized) the tap gesture will fire as soon as your label is initialised!
Swift 4 with RxCocoa + RxSwift + RxGesture
let disposeBag = DisposeBag()
let myView = UIView()
myView.rx
.longPressGesture(numberOfTouchesRequired: 1,
numberOfTapsRequired: 0,
minimumPressDuration: 0.01,
allowableMovement: 1.0)
.when(.began, .changed, .ended)
.subscribe(onNext: { pan in
let view = pan.view
let location = pan.location(in: view)
switch pan.state {
case .began:
print("began")
case .changed:
print("changed \(location)")
case .ended:
print("ended")
default:
break
}
}).disposed(by bag)
or
myView.rx
.gesture(.tap(), .pan(), .swipe([.up, .down]))
.subscribe({ onNext: gesture in
switch gesture {
case .tap: // Do something
case .pan: // Do something
case .swipeUp: // Do something
default: break
}
}).disposed(by: bag)
or event clever, to return an event. i.e string
var buttons: Observable<[UIButton]>!
let stringEvents = buttons
.flatMapLatest({ Observable.merge($0.map({ button in
return button.rx.tapGesture().when(.recognized)
.map({ _ in return "tap" })
}) )
})
Those extensions are technically part of the RxCocoa libary which is currently packaged with RxSwift.
You should be able to add the UITapGestureRecognizer to the view then just use the rx.event (rx_event if older) on that gesture object.
If you have to do this in the context of the UILabel, then you might need to wrap it inside the UILabel+Rx too, but if you have simpler requirements just using the rx.event on the gesture should be a good workaround.
You can subscribe label to the tap gesture
label
.rx
.tapGesture()
.subscribe(onNext: { _ in
print("tap")
}).disposed(by: disposeBag)
As Write George Quentin. All work.
view.rx
.longPressGesture(configuration: { gestureRecognizer, delegate in
gestureRecognizer.numberOfTouchesRequired = 1
gestureRecognizer.numberOfTapsRequired = 0
gestureRecognizer.minimumPressDuration = 0.01
gestureRecognizer.allowableMovement = 1.0
})
.when(.began, .changed, .ended)
.subscribe(onNext: { pan in
let view = pan.view
let location = pan.location(in: view)
switch pan.state {
case .began:
print(":DEBUG:began")
case .changed:
print(":DEBUG:changed \(location)")
case .ended:
print(":DEBUG:end \(location)")
nextStep()
default:
break
}
})
.disposed(by: stepBag)
I simple use this extension to get the tap as Driver in UI layer.
public extension Reactive where Base: RxGestureView {
func justTap() -> Driver<Void> {
return tapGesture()
.when(.recognized)
.map{ _ in }
.asDriver { _ in
return Driver.empty()
}
}
}
When I need the tap event I call this
view.rx.justTap()

Enlarge UICollectionViewCell when long press and dragged

I want to scale the collectionview when longpress and dragged and when user end drag then cell should come in regular size.
I am creating demo using below steps which works fine but enlarge is not working as I expected.
Collection View Example
Here is the gesture code I used :
func handleLongGesture(gesture: UILongPressGestureRecognizer) {
switch(gesture.state) {
case UIGestureRecognizerState.Began:
guard let selectedIndexPath = self.collectionView.indexPathForItemAtPoint(gesture.locationInView(self.collectionView)) else {
break
}
collectionView.beginInteractiveMovementForItemAtIndexPath(selectedIndexPath)
case UIGestureRecognizerState.Changed:
collectionView.updateInteractiveMovementTargetPosition(gesture.locationInView(gesture.view!))
case UIGestureRecognizerState.Ended:
collectionView.endInteractiveMovement()
default:
collectionView.cancelInteractiveMovement()
}
}
Try this on a collection view layout sub-class:
- (UICollectionViewLayoutAttributes*) layoutAttributesForInteractivelyMovingItemAtIndexPath:(NSIndexPath *)indexPath withTargetPosition:(CGPoint)position
{
UICollectionViewLayoutAttributes *attributes = [super layoutAttributesForInteractivelyMovingItemAtIndexPath:indexPath withTargetPosition:position];
attributes.zIndex = NSIntegerMax;
attributes.transform3D = CATransform3DScale(attributes.transform3D, 1.2f, 1.2f, 1.0);
return attributes;
}

Long press to slide show images

I have a CollectionView, the cell in CollectionView has size equal to the screen (CollectionView has paging enable mode).
I want to press long on the screen, then the CollectionView will scroll to the next cell.
For example:
I need 1 second to make the CollectionView scroll to the next cell,
and I press for 2,5 seconds.
The begining time: I am starting long press on the screen and the collection view is now on the first cell.
After the first second: It will scroll to the second cell.
After the second second: It will scroll to the third cell.
The last half second: It still stand on the third cell (because half second is not enough time to make the collection view scroll to the next cell).
I have added the UILongPressGestureRecognizer to the cell and I have tried like this:
func handleLongPress(longGesture: UILongPressGestureRecognizer) {
if longGesture.state == .Ended {
let p = longGesture.locationInView(self.collectionView)
let indexPath = self.collectionView.indexPathForItemAtPoint(p)
if let indexPath = indexPath {
let row = indexPath.row + 1
let section = indexPath.section
if row < self.photoData.count {
self.collectionView.selectItemAtIndexPath(NSIndexPath(forRow: row, inSection: section), animated: true, scrollPosition: .Right)
}
print(indexPath.row)
} else {
print("Could not find index path")
}
}
}
But I always have to END the long gesture to make the collection view scroll.
What you seem to want is something that kicks off a timer that fires every 1 second while the finger is down. I'd probably make a function:
func scrollCell() {
if (longPressActive) {
//scroll code
let dispatchTime: dispatch_time_t = dispatch_time(DISPATCH_TIME_NOW, Int64(0.1 * Double(NSEC_PER_SEC)))
dispatch_after(dispatchTime, dispatch_get_main_queue(), {
scrollCell() // function calls itself after a second
})
}
}
That you could control in your handleLongPress code:
func handleLongPress(longGesture: UILongPressGestureRecognizer) {
if longGesture.state == .Began {
longPressActive = true
scrollCell()
} else if longGesture.state == .Ended || longGesture.state == .Canceled {
longPressActive = false
}
}
So, when the long press gesture first fires, it sets a bool (longPressActive), and then calls the scroll function. When the scroll function completes, it calls itself again. If the gesture ever finalizes, it will clear the longPressActive bool, so if the timer fires, the bool will be false and it won't scroll.
More ideally I'd probably not use a long press gesture recognizer and just track the touches myself, as I could reference the touch and check its state instead of use a boolean. Also, there is probably a fun bug involved with the dispatch when it goes into the background.
Here is the way I have tried:
First, I add these properties to my Controller:
var counter = 0
var timer = NSTimer()
var currentIndexPath: NSIndexPath?
Then I count up the counter whenever longGesture.state == .Began
func handleLongPress(longGesture: UILongPressGestureRecognizer) {
if longGesture.state == .Began {
let point = longGesture.locationInView(self.collectionView)
currentIndexPath = self.collectionView.indexPathForItemAtPoint(point)
self.counter = 0
self.timer = NSTimer.scheduledTimerWithTimeInterval(1, target: self, selector: #selector(ODIProfileAlbumMode4TableViewCell.incrementCounter), userInfo: nil, repeats: true)
} else if longGesture.state == .Ended {
self.timer.invalidate()
}
}
func incrementCounter() {
self.counter += 1
print(self.counter)
if let indexPath = currentIndexPath {
let section = indexPath.section
if self.counter < self.photoData.count {
self.collectionView.scrollToItemAtIndexPath(NSIndexPath(forRow: self.counter, inSection: section), atScrollPosition: .Right, animated: true)
} else {
self.counter = 0
self.collectionView.scrollToItemAtIndexPath(NSIndexPath(forRow: 0, inSection: section), atScrollPosition: .Right, animated: true)
}
} else {
print("Could not find index path")
}
}
It works perfectly now. :)

Temporarily Hiding a cell in UICollectionView Swift iOS

I've been trying this for hours with no luck. I have a UICollectionView collectionView. The collection view is basically a list with the last cell always being a cell with a big plus sign to add another item. I've enabled reordering with the following. What I'd like for it to do is when I start the interactive movement, the plus sign cell goes away, and then when the user is done editing, it appears again. This is a basic version of the code I have:
func handleLongGesture(gesture: UILongPressGestureRecognizer) {
switch(gesture.state) {
case UIGestureRecognizerState.Began:
...
self.collectionView.beginInteractiveMovementForItemAtIndexPath(selectedIndexPath)
removeAddCell()
case UIGestureRecognizerState.Changed:
case UIGestureRecognizerState.Ended:
...
collectionView.endInteractiveMovement()
replaceAddCell()
default:
collectionView.cancelInteractiveMovement()
}
}
func removeAddCell(){
print("Reloading data - removing add cell")
data_source.popLast()
self.collectionView.reloadData()
}
func replaceAddCell(){
print("Reloading data - replacing add cell")
data_source.append("ADD BUTTON")
self.collectionView.reloadData()
}
It's very rough pseudocode, but I can't even get the simplest version of this to work. With the code I have, it gives me the dreaded "Fatal error: unexpectedly found nil while unwrapping an Optional values" on the line where I reference the UICollectionViewCell after removing the items from the data source.
If anyone who has done something like this could share their approach I'd really appreciate it! Thank you!
-Bryce
You can do something like this:
func longPressed(sender: UILongPressGestureRecognizer) {
let indexPath = NSIndexPath(forItem: items.count - 1, inSection: 0)
let cell = collectionView.cellForItemAtIndexPath(indexPath) as! YourCollectionViewCell
switch sender.state {
case .Began:
UIView.animateWithDuration(0.3, animations: {
cell.contentView.alpha = 0
})
case .Ended:
UIView.animateWithDuration(0.3, animations: {
cell.contentView.alpha = 1
})
default: break
}
}
this way it gradually disappears instead of abruptly.
I've done something like this. The data source for the collection view tracks a BOOL to determine whether or not to show the Add Item Cell. And call insertItemsAtIndexPaths: and deleteItemsAtIndexPaths: to animate the Add Item Cell appearing and disappearing. I actually use a Edit button to toggle the modes. But you can adapt this code to use your gesture recognizer.
basic code:
self.editing = !self.editing; // toggle editing mode, BOOL that collection view data source uses
NSIndexPath *indexPath = [self indexPathForAddItemCell];
if (!self.editing) { // editing mode over, show add item cell
if (indexPath) {
[self.collectionView insertItemsAtIndexPaths:#[indexPath]];
}
}
else { // editing mode started, delete add item cell
if (indexPath) {
[self.collectionView deleteItemsAtIndexPaths:#[indexPath]];
}
}

UITableView Drag & Drop Outside Table = Crash

The Good
My drag & drop function almost works wonderfully. I longPress a cell and it smoothly allows me to move the pressed cell to a new location between two other cells. The table adjusts and the changes save to core data. Great!
The Bad
My problem is that if I drag the cell below the bottom cell in the table, even if I don't let go (un-press) of the cell... the app crashes. If I do the drag slowly, really it crashes as the cell crosses the y-center of the last cell... so I do think it's a problem related to the snapshot getting a location. Less important, but possibly related, is that if I long press below the last cell with a value in it, it also crashes.
The drag/drop runs off a switch statement that runs one of three sets of code based on the status:
One case when the press begins
One case when the cell is being dragged
One case when when the user lets go of the cell
My code is adapted from this tutorial:
Drag & Drop Tutorial
My code:
func longPressGestureRecognized(gestureRecognizer: UIGestureRecognizer) {
let longPress = gestureRecognizer as! UILongPressGestureRecognizer
let state = longPress.state
var locationInView = longPress.locationInView(tableView)
var indexPath = tableView.indexPathForRowAtPoint(locationInView)
struct My {
static var cellSnapshot : UIView? = nil
}
struct Path {
static var initialIndexPath : NSIndexPath? = nil
}
let currentCell = tableView.cellForRowAtIndexPath(indexPath!) as! CustomTableViewCell;
var dragCellName = currentCell.nameLabel!.text
var dragCellDesc = currentCell.descLabel.text
//Steps to take a cell snapshot. Function to be called in switch statement
func snapshotOfCell(inputView: UIView) -> UIView {
UIGraphicsBeginImageContextWithOptions(inputView.bounds.size, false, 0.0)
inputView.layer.renderInContext(UIGraphicsGetCurrentContext())
let image = UIGraphicsGetImageFromCurrentImageContext() as UIImage
UIGraphicsEndImageContext()
let cellSnapshot : UIView = UIImageView(image: image)
cellSnapshot.layer.masksToBounds = false
cellSnapshot.layer.cornerRadius = 0.0
cellSnapshot.layer.shadowOffset = CGSizeMake(-5.0, 0.0)
cellSnapshot.layer.shadowRadius = 5.0
cellSnapshot.layer.shadowOpacity = 0.4
return cellSnapshot
}
switch state {
case UIGestureRecognizerState.Began:
//Calls above function to take snapshot of held cell, animate pop out
//Run when a long-press gesture begins on a cell
if indexPath != nil && indexPath != nil {
Path.initialIndexPath = indexPath
let cell = tableView.cellForRowAtIndexPath(indexPath!) as UITableViewCell!
My.cellSnapshot = snapshotOfCell(cell)
var center = cell.center
My.cellSnapshot!.center = center
My.cellSnapshot!.alpha = 0.0
tableView.addSubview(My.cellSnapshot!)
UIView.animateWithDuration(0.25, animations: { () -> Void in
center.y = locationInView.y
My.cellSnapshot!.center = center
My.cellSnapshot!.transform = CGAffineTransformMakeScale(1.05, 1.05)
My.cellSnapshot!.alpha = 0.98
cell.alpha = 0.0
}, completion: { (finished) -> Void in
if finished {
cell.hidden = true
}
})
}
case UIGestureRecognizerState.Changed:
if My.cellSnapshot != nil && indexPath != nil {
//Runs when the user "lets go" of the cell
//Sets CG Y-Coordinate of snapshot cell to center of current location in table (snaps into place)
var center = My.cellSnapshot!.center
center.y = locationInView.y
My.cellSnapshot!.center = center
var appDel: AppDelegate = (UIApplication.sharedApplication().delegate as! AppDelegate)
var context: NSManagedObjectContext = appDel.managedObjectContext!
var fetchRequest = NSFetchRequest(entityName: currentListEntity)
let sortDescriptor = NSSortDescriptor(key: "displayOrder", ascending: true )
fetchRequest.sortDescriptors = [ sortDescriptor ]
//If the indexPath is not 0 AND is not the same as it began (didn't move)...
//Update array and table row order
if ((indexPath != nil) && (indexPath != Path.initialIndexPath)) {
swap(&taskList_Cntxt[indexPath!.row], &taskList_Cntxt[Path.initialIndexPath!.row])
tableView.moveRowAtIndexPath(Path.initialIndexPath!, toIndexPath: indexPath!)
toolBox.updateDisplayOrder()
context.save(nil)
Path.initialIndexPath = indexPath
}
}
default:
if My.cellSnapshot != nil && indexPath != nil {
//Runs continuously while a long press is recognized (I think)
//Animates cell movement
//Completion block:
//Removes snapshot of cell, cleans everything up
let cell = tableView.cellForRowAtIndexPath(Path.initialIndexPath!) as UITableViewCell!
cell.hidden = false
cell.alpha = 0.0
UIView.animateWithDuration(0.25, animations: { () -> Void in
My.cellSnapshot!.center = cell.center
My.cellSnapshot!.transform = CGAffineTransformIdentity
My.cellSnapshot!.alpha = 0.0
cell.alpha = 1.0
}, completion: { (finished) -> Void in
if finished {
Path.initialIndexPath = nil
My.cellSnapshot!.removeFromSuperview()
My.cellSnapshot = nil
}
})//End of competion block & end of animation
}//End of 'if nil'
}//End of switch
}//End of longPressGestureRecognized
Potential Culprit
My guess is that the issue is related to the cell being unable to get coordinates once it is below the last cell. It isn't really floating, it is constantly setting its location in relation to the other cells. I think the solution will be an if-statement that does something magical when there's no cell to reference for a location. But what!?! Adding a nil check to each case isn't working for some reason.
Clearly Stated Question
How do I avoid crashes and handle an event where my dragged cell is dragged below the last cell?
Screenshot of crash:
The Ugly
It seems that you simply need to do a preemptive check, to ensure your indexPath is not nil:
var indexPath = tableView.indexPathForRowAtPoint(locationInView)
if (indexPath != nil) {
//Move your code to this block
}
Hope that helps!
You don't state where in the code the crash occurs, which makes it harder to determine what is going on. Set a breakpoint on exceptions to determine which line is the culprit. To do that, use the '+' in the bottom-left corner of the breakpoint list in XCode.
The main issue I think is with the indexPath. There are a couple of issues:
You are using the indexPath even though it might be nil, in this line:
let currentCell = tableView.cellForRowAtIndexPath(indexPath!) as! CustomTableViewCell;
The indexPath can be invalid, even though it is not nil. Check for its section and row members to be different from NSNotFound.
Finally, I have been using a pre-made, open source, UITableView subclass that does all the moving for you, so you don't have to implement it yourself anymore. It also takes care of autoscrolling, which you have not even considered yet. Use it directly, or use it as inspiration for your code:
https://www.cocoacontrols.com/controls/fmmovetableview

Resources