Horizontal reordering of UICollectionView cells - ios

I've got a UICollectionView with horizontal scrolling. Each cell corresponds to a object (a currency code) in an array. What I would like is to be able to reorder cells via a drag and drop gesture.
I found a tutorial for UITableView and tried it but when I hold and drag a cell it only moves vertically and doesn't scroll when I move my finger to the edge of the screen. Here is a gif.
What I want to happen is for the cell to move horizontally, instead of vertically, and for the collection view to scroll when the edge of the screen is reached. How would I achieve this?
This is what I have now:
UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget: self action: #selector(longPressGestureRecognised:)];
[self.collectionView addGestureRecognizer: longPress];
-(IBAction) longPressGestureRecognised:(id)sender
{
UILongPressGestureRecognizer *longPress = (UILongPressGestureRecognizer *)sender;
UIGestureRecognizerState state = longPress.state;
CGPoint location = [longPress locationInView: self.collectionView];
NSIndexPath *indexPath = [self.collectionView indexPathForItemAtPoint: location];
static UIView *snapshot = nil;
static NSIndexPath *sourceIndexPath = nil;
switch (state) {
case UIGestureRecognizerStateBegan: {
if (indexPath) {
sourceIndexPath = indexPath;
UICollectionViewCell *cell = [self.collectionView cellForItemAtIndexPath: indexPath];
// Take a snapshot of the selected item using helper method.
snapshot = [self customSnapshotFromView: cell];
// Add the snapshot as subview, centered at cell's centre.
__block CGPoint centre = cell.center;
snapshot.center = centre;
snapshot.alpha = 0.0;
[self.collectionView addSubview: snapshot];
[UIView animateWithDuration: 0.25 animations:^{
// Offset for gesture location.
centre.y = location.y;
snapshot.center = centre;
snapshot.transform = CGAffineTransformMakeScale(1.05, 1.05);
snapshot.alpha = 0.98;
// Fade out.
cell.alpha = 0.0;
} completion: ^(BOOL finished) {
cell.hidden = YES;
}];
}
break;
}
case UIGestureRecognizerStateChanged: {
CGPoint centre = snapshot.center;
centre.y = location.y;
snapshot.center = centre;
// Is destination valid and is it different from source?
if (indexPath && ![indexPath isEqual: sourceIndexPath]) {
// Update data source.
[currencyArray exchangeObjectAtIndex: indexPath.item withObjectAtIndex:sourceIndexPath.item];
// Move the items.
[self.collectionView moveItemAtIndexPath: sourceIndexPath toIndexPath: indexPath];
// And update source so it is in sync with UI changes.
sourceIndexPath = indexPath;
}
break;
}
default: {
// Clean up.
UICollectionViewCell *cell = [self.collectionView cellForItemAtIndexPath: sourceIndexPath];
cell.hidden = NO;
cell.alpha = 0.0;
[UIView animateWithDuration: 0.25 animations: ^{
snapshot.center = cell.center;
snapshot.transform = CGAffineTransformIdentity;
snapshot.alpha = 0.0;
// Undo fade out.
cell.alpha = 1.0;
}completion: ^(BOOL finished) {
sourceIndexPath = nil;
[snapshot removeFromSuperview];
snapshot = nil;
}];
break;
}
}
}
-(UIView *) customSnapshotFromView:(UIView *)inputView
{
// Make an image from the input view.
UIGraphicsBeginImageContextWithOptions(inputView.bounds.size, NO, 0);
[inputView.layer renderInContext: UIGraphicsGetCurrentContext()];
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
// Create an image view.
UIView *snapshot = [[UIImageView alloc] initWithImage: image];
snapshot.layer.masksToBounds = NO;
snapshot.layer.cornerRadius = 0.0;
snapshot.layer.shadowOffset = CGSizeMake(-5.0, 0.0);
snapshot.layer.shadowRadius = 5.0;
snapshot.layer.shadowOpacity = 0.4;
return snapshot;
}
EDIT:
I've figured it out. Here is the code:
UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget: self action: #selector(handleLongGesture:)];
[self.collectionView addGestureRecognizer: longPress];
-(IBAction) handleLongGesture: (id)sender {
UILongPressGestureRecognizer *longPress = (UILongPressGestureRecognizer *)sender;
CGPoint location = [longPress locationInView: self.collectionView];
NSIndexPath *indexPath = [self.collectionView indexPathForItemAtPoint: location];
switch (longPress.state) {
case UIGestureRecognizerStateBegan:
[self.collectionView beginInteractiveMovementForItemAtIndexPath: indexPath];
NSLog(#"Gesture began");
break;
case UIGestureRecognizerStateChanged:
[self.collectionView updateInteractiveMovementTargetPosition: [longPress locationInView: longPress.view]];
NSLog(#"Gesture state changed");
break;
case UIGestureRecognizerStateEnded:
[self.collectionView endInteractiveMovement];
NSLog(#"Gesture state ended");
break;
default:
[self.collectionView cancelInteractiveMovement];
NSLog(#"Gesture cancelled");
break;
}
}

in UIGestureRecognizerStateChanged:
change this line of code
CGPoint centre = snapshot.center;
centre.x = location.x;
snapshot.center = centre;
centre.y returns y axis dragging
change it to centre.x for x axis dragging

Related

Move cells of UICollectionView in iOS?

I have a UICollectionView.I am trying to give it as SpringBoard functionality.I have am able to give the shake animation to each cell.But i want when icons are shaking then i should be able to move them as well.
For shaking the cells i have added the UILongPressGesture on each cell.When gesture end then i have added one custom animation on them & also added a delete button on top left corner.
Code for long press gesture:
declaration of variables
CGPoint p;
UILongPressGestureRecognizer *lpgr;
NSIndexPath *gesture_indexPath;
add gesture to collection view
lpgr
= [[UILongPressGestureRecognizer alloc]
initWithTarget:self action:#selector(handleLongPress:)];
lpgr.minimumPressDuration = .3; // To detect after how many seconds you want shake the cells
lpgr.delegate = self;
[self.collection_view addGestureRecognizer:lpgr];
lpgr.delaysTouchesBegan = YES;
Callback method
-(void)handleLongPress:(UILongPressGestureRecognizer *)gestureRecognizer
{
if (gestureRecognizer.state != UIGestureRecognizerStateEnded)
{
return;
}
p = [gestureRecognizer locationInView:self.collection_view];
NSIndexPath *indexPath = [self.collection_view indexPathForItemAtPoint:p];
if (indexPath == nil)
{
NSLog(#"couldn't find index path");
}
else
{
[[NSUserDefaults standardUserDefaults]setValue:#"yes" forKey:#"longPressed"];
[self.collection_view reloadData];
}
}
Cell for Item at inde path
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
NSLog(#"arr_album index row");
BlogAlbumCell *cell;
static NSString *identifier = #"UserBlogAlbum";
cell = [collectionView dequeueReusableCellWithReuseIdentifier:identifier forIndexPath:indexPath];
UserAlbum *user_allbum=[arr_userAlbums objectAtIndex:indexPath.row];
cell.label_blog_name.text=user_allbum.album_name;
cell.image_blog_image.image = [UIImage imageNamed:#"more.png"];
[cell.image_blog_image setImageWithURL:[NSURL URLWithString:[IMAGE_BASE_URL stringByAppendingString:user_allbum.album_image]]];
if([[[NSUserDefaults standardUserDefaults]valueForKey:#"longPressed"] isEqualToString:#"yes"])
{
CABasicAnimation* anim = [CABasicAnimation animationWithKeyPath:#"transform.rotation"];
[anim setToValue:[NSNumber numberWithFloat:0.0f]];
[anim setFromValue:[NSNumber numberWithDouble:M_PI/50]];
[anim setDuration:0.1];
[anim setRepeatCount:NSUIntegerMax];
[anim setAutoreverses:YES];
cell.layer.shouldRasterize = YES;
[cell.layer addAnimation:anim forKey:#"SpringboardShake"];
CGFloat delButtonSize = 20;
UIButton *delButton = [[UIButton alloc] initWithFrame:CGRectMake(0, 0, delButtonSize, delButtonSize)];
delButton.center = CGPointMake(9, 10);
delButton.backgroundColor = [UIColor clearColor];
[delButton setImage: [UIImage imageNamed:#"cross_30.png"] forState:UIControlStateNormal];
[cell addSubview:delButton];
[delButton addTarget:self action:#selector(deleteRecipe:) forControlEvents:UIControlEventTouchUpInside];
}
else if ([[[NSUserDefaults standardUserDefaults]valueForKey:#"singleTap"] isEqualToString:#"yes"])
{
for(UIView *subview in [cell subviews])
{
if([subview isKindOfClass:[UIButton class]])
{
[subview removeFromSuperview];
}
else
{
// Do nothing - not a UIButton or subclass instance
}
}
[cell.layer removeAllAnimations];
// _deleteButton.hidden = YES;
// [_deleteButton removeFromSuperview];
}
return cell;
}
It works fine till here.
For moving the cell i made a sample app in which i added UICollectionViewController & override this method
-(void)collectionView:(UICollectionView *)collectionView moveItemAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath
{
NSLog(#"Move at index path called");
}
This also works fine.It also uses long press gesture & when gesture detetced then i am able to move the cells.But now the issue is at one either i can move cell or animate them.If i add my custom gesture then i am not able to move the images.Please tell me how can i remove this issue?
Take a look at this project called DragDropCollectionView. It implements the dragging and dropping as well as the animation.
Edit: This problem can be broken down into 2 smaller subproblems:
How to animate the cells
How to reorder the cells using the drag and drop.
You should combine these 2 solutions into a subclas of UICollectionView to obtain your main solution.
How to animate the cells
To get the wiggle animation you need add 2 different animation effects:
Move the cells vertically i.e up and down bounce
Rotate the cells
Lastly, add a random interval time for each cell so it does appear that the cells don't animate uniformly
Here is the code:
#interface DragDropCollectionView ()
#property (assign, nonatomic) BOOL isWiggling;
#end
#implementation DragDropCollectionView
//Start and Stop methods for wiggle
- (void) startWiggle {
for (UICollectionViewCell *cell in self.visibleCells) {
[self addWiggleAnimationToCell:cell];
}
self.isWiggling = true;
}
- (void)stopWiggle {
for (UICollectionViewCell *cell in self.visibleCells) {
[cell.layer removeAllAnimations];
}
self.isWiggling = false;
}
//- (UICollectionViewCell *)dequ
- (UICollectionViewCell *)dequeueReusableCellWithReuseIdentifier:(NSString *)identifier forIndexPath:(nonnull NSIndexPath *)indexPath{
UICollectionViewCell *cell = [super dequeueReusableCellWithReuseIdentifier:identifier forIndexPath:indexPath];
if (self.isWiggling) {
[self addWiggleAnimationToCell:cell];
} else {
[cell.layer removeAllAnimations];
}
return [[UICollectionViewCell alloc] init];
}
//Animations
- (void)addWiggleAnimationToCell:(UICollectionViewCell *)cell {
[CATransaction begin];
[CATransaction setDisableActions:false];
[cell.layer addAnimation:[self rotationAnimation] forKey:#"rotation"];
[cell.layer addAnimation:[self bounceAnimation] forKey:#"bounce"];
[CATransaction commit];
}
- (CAKeyframeAnimation *)rotationAnimation {
CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:#"transform.rotation.z"];
CGFloat angle = 0.04;
NSTimeInterval duration = 0.1;
double variance = 0.025;
animation.values = #[#(angle), #(-1 * angle)];
animation.autoreverses = YES;
animation.duration = [self randomizeInterval:duration withVariance: variance];
animation.repeatCount = INFINITY;
return animation;
}
- (CAKeyframeAnimation *)bounceAnimation {
CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:#"transform.translation.y"];
CGFloat bounce = 3.0;
NSTimeInterval duration = 0.12;
double variance = 0.025;
animation.values = #[#(bounce), #(-1 * bounce)];
animation.autoreverses = YES;
animation.duration = [self randomizeInterval:duration withVariance: variance];
animation.repeatCount = INFINITY;
return animation;
}
- (NSTimeInterval)randomizeInterval:(NSTimeInterval)interval withVariance:(double)variance {
double randomDecimal = (arc4random() % 1000 - 500.0) / 500.0;
return interval + variance * randomDecimal;
}
How to reorder the cells using drag and drop
So the idea is this: You’re not moving the actual cell around, but rather moving a UIImageView with a UIImage of the contents of the cell.
The algorithm goes more or less like this. I’ve broken it down into 3 sections, gestureRecognizerBegan, Changed and Ended
gestureRecognizerBegan:
When the gestureRecognizer begins, determine if the long press was indeed on a cell (and not on empty space)
Get a UIImage of the cell (See my method “getRasterizedImageOfCell”)
Hide the cell (i.e alpha = 0), create a UIImageView with the exact frame of the cell so the user does not realize you’ve actually hidden the cell and you’re actually using the imageview.
gestureRecognizerChanged:
Update the center of the UIImageView so that it moves with your finger.
If the user has stopped moving his finge i.e. he is hovering over the cell he wants to replace, you now need to swap the cells. (Look at my function “shouldSwapCells”, this method returns a bool of whether the cells should swap or not)
Move the cell that you were dragging to the new indexPath. (Look at my method “swapDraggedCell”). UICollectionView has a built-in method called "moveItemAtIndexPath: toIndexPath”, I’m not sure if UITableView has the same thing
gestureRecognizerEnd:
“Drop” the UIImageView back onto the cell
change the cell alpha from 0.0 to 1.0 and remove the UIImageView from the view.
Here is the code:
#interface DragDropCollectionView ()
#property (strong, nonatomic) NSIndexPath *draggedCellIndexPath;
#property (strong, nonatomic) UIImageView *draggingImageView;
#property (assign, nonatomic) CGPoint touchOffsetFromCenterOfCell;
#property (strong, nonatomic) UILongPressGestureRecognizer *longPressRecognizer;
#end
#implementation DragDropCollectionView
- (void)handleLongPress:(UILongPressGestureRecognizer *)longPressRecognizer {
CGPoint touchLocation = [longPressRecognizer locationInView:self];
switch (longPressRecognizer.state) {
case UIGestureRecognizerStateBegan: {
self.draggedCellIndexPath = [self indexPathForItemAtPoint:touchLocation];
if (self.draggedCellIndexPath != nil) {
UICollectionViewCell *draggedCell = [self cellForItemAtIndexPath:self.draggedCellIndexPath];
self.draggingImageView = [[UIImageView alloc] initWithImage:[self rasterizedImageCopyOfCell:draggedCell]];
self.draggingImageView.center = draggedCell.center;
[self addSubview:self.draggingImageView];
draggedCell.alpha = 0.0;
self.touchOffsetFromCenterOfCell = CGPointMake(draggedCell.center.x - touchLocation.x, draggedCell.center.y - touchLocation.y);
[UIView animateWithDuration:0.4 animations:^{
self.draggingImageView.transform = CGAffineTransformMakeScale(1.3, 1.3);
self.draggingImageView.alpha = 0.8;
}];
}
break;
}
case UIGestureRecognizerStateChanged: {
if (self.draggedCellIndexPath != nil) {
self.draggingImageView.center = CGPointMake(touchLocation.x + self.touchOffsetFromCenterOfCell.x, touchLocation.y + self.touchOffsetFromCenterOfCell.y);
}
float pingInterval = 0.3;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(pingInterval * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSIndexPath *newIndexPath = [self indexPathToSwapCellWithAtPreviousTouchLocation:touchLocation];
if (newIndexPath) {
[self swapDraggedCellWithCellAtIndexPath:newIndexPath];
}
});
break;
}
case UIGestureRecognizerStateEnded: {
if (self.draggedCellIndexPath != nil ) {
UICollectionViewCell *draggedCell = [self cellForItemAtIndexPath:self.draggedCellIndexPath];
[UIView animateWithDuration:0.4 animations:^{
self.draggingImageView.transform = CGAffineTransformIdentity;
self.draggingImageView.alpha = 1.0;
if (draggedCell != nil) {
self.draggingImageView.center = draggedCell.center;
}
} completion:^(BOOL finished) {
[self.draggingImageView removeFromSuperview];
self.draggingImageView = nil;
if (draggedCell != nil) {
draggedCell.alpha = 1.0;
self.draggedCellIndexPath = nil;
}
}];
}
}
default:
break;
}
}
- (UIImage *)rasterizedImageCopyOfCell:(UICollectionViewCell *)cell {
UIGraphicsBeginImageContextWithOptions(cell.bounds.size, false, 0.0);
[cell.layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
return image;
}
- (NSIndexPath *)indexPathToSwapCellWithAtPreviousTouchLocation:(CGPoint)previousTouchLocation {
CGPoint currentTouchLocation = [self.longPressRecognizer locationInView:self];
if (!isnan(currentTouchLocation.x) && !isnan(currentTouchLocation.y)) {
if ([self distanceBetweenPoints:currentTouchLocation secondPoint:previousTouchLocation] < 20.0) {
NSIndexPath *newIndexPath = [self indexPathForItemAtPoint:currentTouchLocation];
return newIndexPath;
}
}
return nil;
}
- (CGFloat)distanceBetweenPoints:(CGPoint)firstPoint secondPoint:(CGPoint)secondPoint {
CGFloat xDistance = firstPoint.x - secondPoint.x;
CGFloat yDistance = firstPoint.y - secondPoint.y;
return sqrtf(xDistance * xDistance + yDistance * yDistance);
}
- (void)swapDraggedCellWithCellAtIndexPath:(NSIndexPath *)newIndexPath {
[self moveItemAtIndexPath:self.draggedCellIndexPath toIndexPath:newIndexPath];
UICollectionViewCell *draggedCell = [self cellForItemAtIndexPath:newIndexPath];
draggedCell.alpha = 0.0;
self.draggedCellIndexPath = newIndexPath;
}
Hope this helps :)

UITableView returning wrong indexPath when user's finger goes outside the screen

I'm developing a reorder cells class to allow my users to reorder the cells inside a table.
The process is very simple, I switch cell's position each time I ove one of them.
The process run pretty well until I go down my finger outside the iPhone screen. In that case, the UIGestureRecognizer or the UITableView give me a wrong indexPath. It refers to the first position of my tableView.
This is my code:
Initialize gesture recognition
...
UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc]
initWithTarget:self action:#selector(longPressGestureRecognized:)];
[_tableView addGestureRecognizer:longPress];
...
Method to control the gesture states
- (IBAction)longPressGestureRecognized:(id)sender {
UILongPressGestureRecognizer *longPress = (UILongPressGestureRecognizer *)sender;
UIGestureRecognizerState state = longPress.state;
CGPoint location = [longPress locationInView:_tableView];
NSIndexPath *indexPath = [_tableView indexPathForRowAtPoint:location];
static UIView *snapshot = nil; ///< A snapshot of the row user is moving.
static NSIndexPath *sourceIndexPath = nil; ///< Initial index path, where gesture begins.
NSLog(#"State: %ld", (long)state);
switch (state) {
case UIGestureRecognizerStateBegan: {
NSLog(#"UIGestureRecognizerStateBegan");
if (indexPath) {
sourceIndexPath = indexPath;
Task *sourceTask = [_elements objectAtIndex:sourceIndexPath.row];
NSLog(#"superTask: state[%ld], %#, indexpath.row: %#, actualPosition: %#", (long)state, sourceTask.subtask, #(indexPath.row), sourceTask.position);
if (_collapseSubElements){
if ([sourceTask.subtask isEqualToString:#"0"]){
[self collapseAllTaskWithSubtasks];
sourceIndexPath = indexPath = [NSIndexPath indexPathForRow:[_elements indexOfObjectIdenticalTo:sourceTask] inSection:0];
}
UITableViewCell *cell = [_tableView cellForRowAtIndexPath:indexPath];
// Take a snapshot of the selected row using helper method.
snapshot = [self customSnapshoFromView:cell];
// Add the snapshot as subview, centered at cell's center...
__block CGPoint center = cell.center;
snapshot.center = center;
snapshot.alpha = 0.0;
[_tableView addSubview:snapshot];
[UIView animateWithDuration:0.25 animations:^{
// Offset for gesture location.
center.y = location.y;
snapshot.center = center;
snapshot.transform = CGAffineTransformMakeScale(1.05, 1.05);
snapshot.alpha = 0.98;
cell.alpha = 0.0;
} completion:^(BOOL finished) {
cell.hidden = YES;
}];
}
}
break;
}
case UIGestureRecognizerStateChanged: {
CGPoint center = snapshot.center;
center.y = location.y;
snapshot.center = center;
Task *sourceTask = [_elements objectAtIndex:sourceIndexPath.row];
Task *targetTask = [_elements objectAtIndex:indexPath.row];
NSLog(#"UIGestureRecognizerStateChanged: %ld - %ld - %#", (long)indexPath.row, (long)sourceIndexPath.row, sourceTask.position);
// Is destination valid and is it different from source?
if (indexPath && ![indexPath isEqual:sourceIndexPath]) {
if (indexPath.row - sourceIndexPath.row <= 1){
UITableViewCell *cell = [_tableView cellForRowAtIndexPath:indexPath];
UIButton *subtasksButton = (UIButton *)[cell viewWithTag:108];
// Solamente se pueden mover tareas dentro del mismo nivel
if ([sourceTask isBrotherOfTask:targetTask]){
if (_collapseSubElements){
if (subtasksButton.selected){
//[self expandCollapseSubtasks:subtasksButton];
_collapseCellsBlock(subtasksButton);
}
// ... update element position
NSNumber *sourceTaskPosition = sourceTask.position;
NSNumber *targetTaskPosition = targetTask.position;
_temporalValues = [NSMutableDictionary dictionaryWithObjectsAndKeys:
sourceTaskPosition, #"position", nil];
[targetTask updateWithValues: _temporalValues];
_temporalValues = [NSMutableDictionary dictionaryWithObjectsAndKeys:
targetTaskPosition, #"position", nil];
[sourceTask updateWithValues: _temporalValues];
// ... update data source.
[_elements exchangeObjectAtIndex:indexPath.row withObjectAtIndex:sourceIndexPath.row];
// ... move the rows.
[_tableView moveRowAtIndexPath:sourceIndexPath toIndexPath:indexPath];
// ... and update source so it is in sync with UI changes.
sourceIndexPath = indexPath;
} else {
}
}
}
}
break;
}
case UIGestureRecognizerStateEnded:
{
NSLog(#"UIGestureRecognizerStateEnded");
Task *task = [_elements objectAtIndex:indexPath.row];
//if (indexPath && ![indexPath isEqual:sourceIndexPath]) {
NSLog(#"httpClient indexpath.row: %#, editedposition: %#", #(indexPath.row), task.position);
/*[_httpClient createUpdateRequestForObject:task withPath:#"task/" withRegeneration:NO];
[_httpClient update:nil];*/
_completionBlock(task);
}
default: {
// Clean up.
UITableViewCell *cell = [_tableView cellForRowAtIndexPath:sourceIndexPath];
cell.hidden = NO;
cell.alpha = 0.0;
[UIView animateWithDuration:0.25 animations:^{
snapshot.center = cell.center;
snapshot.transform = CGAffineTransformIdentity;
snapshot.alpha = 0.0;
cell.alpha = 1.0;
} completion:^(BOOL finished) {
sourceIndexPath = nil;
[snapshot removeFromSuperview];
snapshot = nil;
}];
break;
}
}
}
Method to create a snapshot from the selected cell
- (UIView *)customSnapshoFromView:(UIView *)inputView {
// Make an image from the input view.
UIGraphicsBeginImageContextWithOptions(inputView.bounds.size, NO, 0);
[inputView.layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
// Create an image view.
UIView *snapshot = [[UIImageView alloc] initWithImage:image];
snapshot.layer.masksToBounds = NO;
snapshot.layer.cornerRadius = 0.0;
snapshot.layer.shadowOffset = CGSizeMake(-5.0, 0.0);
snapshot.layer.shadowRadius = 5.0;
snapshot.layer.shadowOpacity = 0.4;
return snapshot;
}
Cell movements
When I move cell number 8 everything is OK, but when I move cell 6 to the bottom, the interface maintain the cell in the right position but the indexPath returned is 0
I think the reason is contentsize from _tableView is smaller than the location you point.
You can check this:
if (_tableView.contentSize.height<location.y) indexPath = [NSIndexPath indexPathForRow:[_tableView numberOfRowsInSection:0]-1 inSection:0];

Don't Move Table View Cells with a Long Press Gesture on specific position

I've completely implemented the UILongGesture in my App which exchanges the cell value by drag and drop. For now I've requirement that if I move first row with last row then first row should remain at first position means don't want change the position.
I've tried chunk of codes and wasted my time but couldn't get result. Below is my code.
- (IBAction)longPressGestureRecognized:(id)sender{
UILongPressGestureRecognizer *longGesture = (UILongPressGestureRecognizer *)sender;
UIGestureRecognizerState state = longGesture.state;
CGPoint location = [longGesture locationInView:self.tblTableView];
NSIndexPath *indexpath = [self.tblTableView indexPathForRowAtPoint:location];
static UIView *snapshotView = nil;
static NSIndexPath *sourceIndexPath = nil;
switch (state) {
case UIGestureRecognizerStateBegan:
if (indexpath) {
sourceIndexPath = indexpath;
UITableViewCell *cell = [self.tblTableView cellForRowAtIndexPath:indexpath];
snapshotView = [self customSnapshotFromView:cell];
__block CGPoint center = cell.center;
snapshotView.center = center;
snapshotView.alpha = 0.0;
[self.tblTableView addSubview:snapshotView];
[UIView animateWithDuration:0.25 animations:^{
center.y = location.y;
snapshotView.center = center;
snapshotView.transform = CGAffineTransformMakeScale(1.05, 1.05);
snapshotView.alpha = 0.98;
cell.alpha = 0.0;
} completion:^(BOOL finished) {
cell.hidden = YES;
}];
}
break;
case UIGestureRecognizerStateChanged: {
CGPoint center = snapshotView.center;
center.y = location.y;
snapshotView.center = center;
if (indexpath && ![NSIndexPath isEqual:sourceIndexPath]) {
[self.namesArray exchangeObjectAtIndex:indexpath.row withObjectAtIndex:sourceIndexPath.row];
[self.tblTableView moveRowAtIndexPath:sourceIndexPath toIndexPath:indexpath];
sourceIndexPath = indexpath;
NSIndexPath *indexPathOfLastItem =[NSIndexPath indexPathForRow:([self.namesArray count] - 1) inSection:0];
NSLog(#"last :::: %#",indexPathOfLastItem);
if (indexpath==indexPathOfLastItem) {
[self.namesArray exchangeObjectAtIndex:indexPathOfLastItem.row withObjectAtIndex:sourceIndexPath.row];
[self.tblTableView moveRowAtIndexPath:indexPathOfLastItem toIndexPath:0];
UITableViewCell *cell = [self.tblTableView cellForRowAtIndexPath:sourceIndexPath];
cell.hidden = NO;
cell.alpha = 0.0;
}
}
break;
}
default: {
UITableViewCell *cell = [self.tblTableView cellForRowAtIndexPath:sourceIndexPath];
cell.hidden = NO;
cell.alpha = 0.0;
[UIView animateWithDuration:0.25 animations:^{
snapshotView.center = cell.center;
snapshotView.transform = CGAffineTransformIdentity;
snapshotView.alpha = 0.0;
cell.alpha = 1.0;
} completion:^(BOOL finished) {
sourceIndexPath = nil;
[snapshotView removeFromSuperview];
snapshotView = nil;
}];
break;
}
}
}
EDIT: What I have come across is the cell is not exchanging that's what I want but it is hidden. Here is the image: Image1 and Image2
First of all, I don't think you should do the row exchanges in UIGestureRecognizerStateChanged but instead in UIGestureRecognizerStateEnded. UIGestureRecognizerStateChanged is processed rapidly(many many times) while you are long pressing and moving your finger across the screen. So this causes the row exchange code to run many times which is not your intention I guess. UIGestureRecognizerStateBegan and UIGestureRecognizerStateEnded are run once per each longpress.
I would keep these following three lines of code in UIGestureRecognizerStateChanged and move the rest to UIGestureRecognizerStateEnded:
CGPoint center = snapshotView.center;
center.y = location.y;
snapshotView.center = center;
The UIGestureRecognizerStates are as follows:
UIGestureRecognizerStatePossible, // the recognizer has not yet recognized its gesture, but may be evaluating touch events. this is the default state
UIGestureRecognizerStateBegan, // the recognizer has received touches recognized as the gesture. the action method will be called at the next turn of the run loop
UIGestureRecognizerStateChanged, // the recognizer has received touches recognized as a change to the gesture. the action method will be called at the next turn of the run loop
UIGestureRecognizerStateEnded, // the recognizer has received touches recognized as the end of the gesture. the action method will be called at the next turn of the run loop and the recognizer will be reset to UIGestureRecognizerStatePossible
UIGestureRecognizerStateCancelled, // the recognizer has received touches resulting in the cancellation of the gesture. the action method will be called at the next turn of the run loop. the recognizer will be reset to UIGestureRecognizerStatePossible
UIGestureRecognizerStateFailed, // the recognizer has received a touch sequence that can not be recognized as the gesture. the action method will not be called and the recognizer will be reset to UIGestureRecognizerStatePossible
And instead of using switchs default:, I believe it will be better to cover more of these states in your state machine logic.
When your finger detaches from the screen during animation, iOS returns gesture.state = UIGestureRecognizerStatePossible.
So remember to handle that gesture!

UICollectionView Drag and Drop update all affected indexPaths

I have a UICollectionView that uses drag and drop functionality to perform manual re-order of the cells.
I only have 1 section that contains two columns of equally sized cells.
To explain my problem properly I would need to upload screenshots but I don't have enough rep, so I will have to explain using numbers.
1 2 3 4 5 6
This represents a CollectionView with 6 cells in 2 columns filling the entire screen.
So the problem i'm encountering is that if I swap, for example, cell 3 and cell 2 everything works correctly because cell 3 becomes cell 2 and vice versa, the issue happens when I swap cell 3 and cell 1 because then cell 3 becomes cell1 and cell1 becomes cell2 and cell2 becomes cell3.
Now it re-arranges but as soon as the app calls [self.collectionView reloadData]; cell3 will stay at 1 where I dropped it, but the new cell 2 and 3 will swap positions as if there indexes havn't updated.
A long press gesture handles the remove and insert of the cell(snapshot of cell) when dragged and all the tiles re-arrange and fit properly into the sections.
-(IBAction)longPressGestureRecognized:(id)sender {
UILongPressGestureRecognizer *longPress = (UILongPressGestureRecognizer *)sender;
UIGestureRecognizerState state = longPress.state;
CGPoint location = [longPress locationInView:self.collectionView];
NSIndexPath *indexPath = [self.collectionView indexPathForItemAtPoint:location];
static UIView *snapshot = nil;
static NSIndexPath *sourceIndexPath = nil;
switch (state) {
case UIGestureRecognizerStateBegan: {
if (indexPath) {
sourceIndexPath = indexPath;
UICollectionViewCell *cell = [self.collectionView cellForItemAtIndexPath:indexPath];
snapshot = [self customSnapshotFromView:(cell)];
__block CGPoint center = cell.center;
snapshot.center = center;
snapshot.alpha = 0.98;
[self.collectionView addSubview:snapshot];
[UIView animateWithDuration:0.25 animations:^{
center.y = location.y;
snapshot.center = center;
snapshot.transform = CGAffineTransformMakeScale(1.05, 1.05);
snapshot.alpha = 0.98;
cell.hidden = YES;
} completion:nil];
}
break;
}
case UIGestureRecognizerStateChanged: {
CGPoint center = snapshot.center;
center.y = location.y;
center.x = location.x;
snapshot.center = center;
if (indexPath && ![indexPath isEqual:sourceIndexPath]) {
[people2 exchangeObjectAtIndex:indexPath.item withObjectAtIndex:sourceIndexPath.item];
[self.collectionView moveItemAtIndexPath:sourceIndexPath toIndexPath:indexPath];
sourceIndexPath = indexPath;
[databaseClass arrangeData:people2];
}
break;
}
default: {
UICollectionViewCell *cell = [self.collectionView cellForItemAtIndexPath:sourceIndexPath];
[UIView animateWithDuration:0.25 animations:^{
snapshot.center = cell.center;
snapshot.transform = CGAffineTransformIdentity;
snapshot.alpha = 0.0;
cell.hidden = NO;
} completion:^(BOOL finished){
[snapshot removeFromSuperview];
snapshot = nil;
}];
sourceIndexPath = nil;
break;
}
}
}
I am using a sqlite method to store the arrangement of the tiles in the database.
+(void)arrangeData:(NSArray*)arrange {
[self databaseInit];
if (sqlite3_open(dbpath, &peopleDB ) == SQLITE_OK)
{
for (int i = 0; i < [arrange count]; i++) {
NSString *insertSQL = [NSString stringWithFormat:#"UPDATE PEOPLE SET ARRANGE = %i WHERE ID = %i",i+1,[[arrange objectAtIndex:i] ID]];
const char *insert_stmt = [insertSQL UTF8String];
sqlite3_prepare_v2( peopleDB, insert_stmt,-1, &statement, NULL);
if (sqlite3_step(statement) == SQLITE_DONE)
{
NSLog(#"Arranged Successfully");
}
else {
NSLog(#"%s SQLITE_ERROR '%s' (%1d)", __FUNCTION__, sqlite3_errmsg(peopleDB), sqlite3_errcode(peopleDB));
}
sqlite3_finalize(statement);
}
sqlite3_close(peopleDB);
}
Not that it matters much but the snapshot :
-(UIView *)customSnapshotFromView:(UIView *)inputView {
UIView *snapshot = [inputView snapshotViewAfterScreenUpdates:YES];
snapshot.layer.masksToBounds = NO;
snapshot.layer.cornerRadius = 0.0;
snapshot.layer.shadowOffset = CGSizeMake(-5.0, 0.0);
snapshot.layer.shadowRadius = 5.0;
snapshot.layer.shadowOpacity = 0.4;
return snapshot;
}
If anyone has any ideas on how to get the cells to stay where they are when the longpress ends I would be hugely appreciative.
I don't understand sqlite, but according to what you say, it seems that you swap data instead of moving the concerned data.
In your example you have to delete dataForCell3 from it's array and then insert it at the index of dataForCell1.
Seems that instead of that you swapped dataForCell3 and dataForCell1.

Creating a stack of photos with UICollectionView which a user can swipe through

I am attempting to create a stack of photos using UICollectionView in which a user can swipe a photo to the side. As a photo is dragged, it gets smaller (to a minimum scale) and the photo behind is enlarged to a maximum scale. This maximum scale is the original size of the foreground photo.
When the pan gesture which drags the foreground photo ends, if it is the minimum scale, it is sent to the back of the stack, otherwise, it is animated back to the centre of the screen and enlarged back to its original size (along with the growing photo behind it).
I hope you're still with me haha.
Now, assuming the photo being dragged is at its minimum size, I remove user interaction of the UICollectionView, animate the photo to the centre of the screen again, and its size is animated to match the rest of the background images. I then send the cell to the back of the UICollectionViews subviews.
Once all of these animations have taken place, I update my data source (an array of images) by adding the photo in front to the back, then deleting that photo at the front.
Then, I update the UICollectionView. This consists of a batch update in which I delete the item at an NSIndexPath of 0 : 0, then, insert an item at NSIndexPath ([photo count] - 1) : 0.
When this batch update finished, I reload my UICollectionView (as only the item at NSIndexPath 0 : 0 has a pan gesture) and re-add user interaction to the UICollectionView.
I hope this makes sense.
Notes
The size of the background photos is a percent of 80%.
The size of the foreground photo is a percent of 100%.
This is relative to the UICollectionView which fits the screen of the iPhone.
My problem
My code seems to work very well. My issue comes from when I take my finger off the screen (the pan gesture ends) and the animation of the photo (centring it to the centre of the screen and sizing it like the rest of the background photos) ends.
The photo dragged off reappears on screen, sizes from the size of the background photos (80%) to the size of the foreground photo (100%) then fades away. Once this happens the photos are reordered as expected.
Does anyone know why this might happen?
Here is my Pan Gesture code:
CGPoint touchTranslation = [gesture translationInView:self.view];
static CGPoint originalCenter;
DIGalleryCollectionViewLayout *galleryCollectionViewLayout = (DIGalleryCollectionViewLayout *)self.galleryCollectionView.collectionViewLayout;
if (gesture.state == UIGestureRecognizerStateBegan) {
//
// Began.
//
originalCenter = gesture.view.center;
} else if (gesture.state == UIGestureRecognizerStateChanged) {
//
// Changed.
//
CGPoint translate = [gesture translationInView:gesture.view.superview];
touchTranslation = CGPointMake(originalCenter.x + translate.x, originalCenter.y + translate.y);
[gesture.view setCenter:touchTranslation];
CGPoint pointsPhotoIsMovedBy = CGPointMake(fabsf(fabsf(originalCenter.x) - fabsf(touchTranslation.x)),
fabsf(fabsf(originalCenter.y) - fabsf(touchTranslation.y)));
// Update cells.
//
CGFloat currentIncrement = MAX(pointsPhotoIsMovedBy.x, pointsPhotoIsMovedBy.y);
for (NSIndexPath *indexPath in self.galleryCollectionView.indexPathsForVisibleItems) {
UICollectionViewCell *cell = [self.galleryCollectionView cellForItemAtIndexPath:indexPath];
if ([indexPath isEqual:[NSIndexPath indexPathForItem:0 inSection:0]]) {
//
// Front.
//
CGFloat frontScalePercent = MIN(GalleryCollectionViewLayoutFrontRatioDefault,
MAX(GalleryViewFrontScaleMinimum, GalleryCollectionViewLayoutFrontRatioDefault - (currentIncrement / 250.0f)));
[cell setTransform:CGAffineTransformMakeScale(frontScalePercent, frontScalePercent)];
} else if ([indexPath isEqual:[NSIndexPath indexPathForItem:1 inSection:0]]) {
//
// Next.
//
CGFloat nextScalePercent = MAX(GalleryCollectionViewLayoutBackRatioDefault,
MIN(GalleryViewNextScaleMaximum, (currentIncrement / 150.0f)));
[cell setTransform:CGAffineTransformMakeScale(nextScalePercent, nextScalePercent)];
} else {
//
// Background.
//
[cell setTransform:CGAffineTransformMakeScale(GalleryCollectionViewLayoutBackRatioDefault,
GalleryCollectionViewLayoutBackRatioDefault)];
}
}
} else if (gesture.state == UIGestureRecognizerStateEnded) {
//
// Ended.
//
if (gesture.view.transform.a == GalleryViewFrontScaleMinimum) {
//
// Next photo.
//
[self.galleryCollectionView setUserInteractionEnabled:NO];
[self.galleryCollectionView sendSubviewToBack:gesture.view];
[UIView animateWithDuration:0.3f
animations:^{
[gesture.view setCenter:self.galleryCollectionView.center];
[gesture.view setTransform:CGAffineTransformMakeScale(GalleryCollectionViewLayoutBackRatioDefault,
GalleryCollectionViewLayoutBackRatioDefault)];
}
completion:^(BOOL finished) {
//
// Data source
//
NSMutableArray *photos = [self.photos mutableCopy];
[photos addObject:[photos objectAtIndex:0]];
[photos removeObjectAtIndex:0];
[self setPhotos:photos];
// Contents
//
[self.galleryCollectionView performBatchUpdates:^{
[self.galleryCollectionView deleteItemsAtIndexPaths:#[[NSIndexPath indexPathForItem:0 inSection:0]]];
[self.galleryCollectionView insertItemsAtIndexPaths:#[[NSIndexPath indexPathForItem:(self.photos.count - 1) inSection:0]]];
} completion:^(BOOL finished) {
[self.galleryCollectionView reloadData];
[self.galleryCollectionView setUserInteractionEnabled:YES];
}];
}];
} else {
//
// Stay.
//
[UIView animateWithDuration:0.3f
delay:0.0f
options:UIViewAnimationOptionCurveEaseIn
animations:^{
//
// Front cell.
//
[gesture.view setCenter:self.galleryCollectionView.center];
[gesture.view setTransform:CGAffineTransformMakeScale(GalleryCollectionViewLayoutFrontRatioDefault,
GalleryCollectionViewLayoutFrontRatioDefault)];
// Next cell.
//
for (NSIndexPath *indexPath in self.galleryCollectionView.indexPathsForVisibleItems) {
if ([indexPath isEqual:[NSIndexPath indexPathForItem:1 inSection:0]]) {
//
// Next.
//
UICollectionViewCell *cell = [self.galleryCollectionView cellForItemAtIndexPath:indexPath];
[cell setTransform:CGAffineTransformMakeScale(GalleryCollectionViewLayoutBackRatioDefault,
GalleryCollectionViewLayoutBackRatioDefault)];
}
}
}
completion:^(BOOL finished) {
[galleryCollectionViewLayout setFrontRatio:GalleryCollectionViewLayoutFrontRatioDefault];
[galleryCollectionViewLayout setNextRatio:GalleryCollectionViewLayoutBackRatioDefault];
[galleryCollectionViewLayout invalidateLayout];
}];
}
}
And here is my prepareLayout: code for the collection view layout:
NSMutableDictionary *newLayoutInfo = [NSMutableDictionary dictionary];
NSMutableDictionary *cellLayoutInfo = [NSMutableDictionary dictionary];
NSIndexPath *indexPath;
for (NSInteger section = 0; section < [self.collectionView numberOfSections]; section++) {
NSInteger itemCount = [self.collectionView numberOfItemsInSection:section];
for (NSInteger item = 0; item < itemCount; item++) {
indexPath = [NSIndexPath indexPathForItem:item inSection:section];
UICollectionViewLayoutAttributes *itemAttributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
[itemAttributes setSize:[self sizeForPhotoAtIndexPath:indexPath inContainerOfSize:self.collectionView.bounds.size]];
[itemAttributes setCenter:self.collectionView.center];
// Z Index.
//
NSInteger zIndex = itemCount - item;
[itemAttributes setZIndex:zIndex];
// Scale cells based on z position.
//
if (zIndex == itemCount) {
//
// Foreground.
//
[itemAttributes setTransform:CGAffineTransformMakeScale(self.frontRatio, self.frontRatio)];
} else if (zIndex == itemCount - 1) {
//
// Next.
//
[itemAttributes setTransform:CGAffineTransformMakeScale(self.nextRatio, self.nextRatio)];
} else {
//
// Background.
//
[itemAttributes setTransform:CGAffineTransformMakeScale(GalleryCollectionViewLayoutBackRatioDefault,
GalleryCollectionViewLayoutBackRatioDefault)];
}
cellLayoutInfo[indexPath] = itemAttributes;
}
}
newLayoutInfo[GalleryCollectionViewLayoutCellKind] = cellLayoutInfo;
self.layoutInfo = newLayoutInfo;
And as an additional reference, here is my code for sizeForPhotoAtIndexPath: inContainerOfSize:
UIImage *photo = [[(DIGalleryViewController *)self.collectionView.delegate photos] objectAtIndex:indexPath.row];
CGSize size = CGSizeMake(photo.size.width, photo.size.height);
UIGraphicsBeginImageContext(containerSize);
[photo drawInRect:CGRectMake(0.0f, 0.0f, containerSize.width, containerSize.height)];
UIImage *resizedPhoto = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
CGFloat ratio = photo.size.width / resizedPhoto.size.width;
size = CGSizeMake(resizedPhoto.size.width, photo.size.height / ratio);
return size;

Resources