9

I have implemented a system to reorder cells in my UITableViews. Everything is ok except that I can't reorder cells to a position that is not being showed in the iPhone screen.

So I have implemented a condition to check if I have to scroll

NSArray *indexVisibles = [_tableView indexPathsForVisibleRows];
NSInteger indexForObject = [indexVisibles indexOfObject:indexPath];
if (indexForObject == NSNotFound){
     [_tableView scrollToRowAtIndexPath:indexPath
                                          atScrollPosition:UITableViewScrollPositionTop
                                                  animated:YES];
}

My problem is that the animation is not sweet and clean.

enter image description here

I think that the operation of checking if a cell is showed is very huge for my system and make a small delay when I move cells, but also, I'm not sure why the scroll is so hard when the cell is hide.

I have change UITableViewScrollPositionTop to UITableViewScrollPositionMiddle and now it is better but the velocity is very big so always scroll goes to the top of my UITableView.

I would like to do it slowly.

Other failure tries:

Option 1:

[UIView animateWithDuration:0.2
                                             animations:^{_tableView.contentOffset = CGPointMake(0.0, _tableView.contentOffset.y - 50);}
                                             completion:^(BOOL finished){ }];

But this has got two problems:

  1. Movement is still heavy
  2. When drag to the first element, this element is middle hide

enter image description here

Option 2:

[UIView animateWithDuration: 1.0
                                             animations: ^{
                                                 [_tableView scrollToRowAtIndexPath:indexPath
                                                                   atScrollPosition:UITableViewScrollPositionMiddle
                                                                           animated:NO];
                                             }completion: ^(BOOL finished){

                                             }
                             ];
EnriMR
  • 3,924
  • 5
  • 39
  • 59
  • 1
    Please check this repository hope this is work for you. https://github.com/hpique/HPReorderTableView – Rupal Patel Sep 14 '15 at 11:56
  • It seems to be right but it has got some bugs and it is not applicable to my project. But I'm studying your code to find the error and fix them – EnriMR Sep 17 '15 at 14:40

3 Answers3

12

I have solved my problem with a very beautiful solution so I'm going to explain in three simple steps how to do it.

I have use some inspiration from https://github.com/hpique/HPReorderTableView and share it in my own repository https://github.com/enrimr/EMRReorderTableCells

A. Manage gestureRecognition

longPressGestureRecognized:

- (IBAction)longPressGestureRecognized:(id)sender {

    _reorderGestureRecognizer = (UILongPressGestureRecognizer *)sender;

    CGPoint location = [_reorderGestureRecognizer locationInView:_tableView];
    NSIndexPath *indexPath = [self getCellIndexPathWithPoint:location];

    UIGestureRecognizerState state = _reorderGestureRecognizer.state;
    switch (state) {
        case UIGestureRecognizerStateBegan: {

            NSIndexPath *indexPath = [_tableView indexPathForRowAtPoint:location];
            if (indexPath == nil)
            {
                [self gestureRecognizerCancel:_reorderGestureRecognizer];
                break;
            }

            // For scrolling while dragging
            _scrollDisplayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(scrollTableWithCell:)];
            [_scrollDisplayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];

            // Check for the right indexes (between margins of offset
            if (indexPath.row >=_elementsOffset && indexPath.row < [_elements count]+_elementsOffset){

                if (indexPath) {
                    sourceIndexPath = indexPath;

                    id sourceElement = [_elements objectAtIndex:sourceIndexPath.row-_elementsOffset];

                    snapshot = [self createSnapshotForCellAtIndexPath:indexPath withPosition:location];
                }
            } else {
                sourceIndexPath = nil;
                snapshot = nil;
            }
            break;
        }

        case UIGestureRecognizerStateChanged: {

            [self calculateScroll:_reorderGestureRecognizer];

            if (sourceIndexPath != nil && indexPath.row >=_elementsOffset && indexPath.row < [_elements count]+_elementsOffset){
                [self updateSnapshotWithPosition:location];

                // Is destination valid and is it different from source?
                if (indexPath && ![indexPath isEqual:sourceIndexPath]) {
                    if (indexPath.row - sourceIndexPath.row <= 1){

                        id sourceElement = [_elements objectAtIndex:sourceIndexPath.row-_elementsOffset];
                        id targetElement = [_elements objectAtIndex:indexPath.row-_elementsOffset];

                        sourceIndexPath = [self exchangeElement:sourceElement byElement:targetElement];
                    }

                }
            }
            break;
        }

        case UIGestureRecognizerStateEnded:
        {
            // For scrolling while dragging
            [_scrollDisplayLink invalidate];
            _scrollDisplayLink = nil;
            _scrollRate = 0;


            // Check if it is the last element
            if (sourceIndexPath != nil){
                id element;
                if (indexPath.row <=_elementsOffset){
                    element = [_elements firstObject];
                } else if (indexPath.row > [_elements count]-1+_elementsOffset){
                    element = [_elements lastObject];
                } else {
                    element = [_elements objectAtIndex:indexPath.row-_elementsOffset];
                }  
            }

        }

        default: {
            // Clean up.
            [self deleteSnapshotForRowAtIndexPath:sourceIndexPath];

            [appDelegate startTimer];

            break;
        }
    }
}

gestureRecognizerCancel:

It is use to cancel gesture recognition to finish the reorder action.

-(void) gestureRecognizerCancel:(UIGestureRecognizer *) gestureRecognizer
{ // See: http://stackoverflow.com/a/4167471/143378
    gestureRecognizer.enabled = NO;
    gestureRecognizer.enabled = YES;
}

scrollTableWithCell:

The method it is called to make scrolling movement when you are in the limits of the table (up and down)

- (void)scrollTableWithCell:(NSTimer *)timer
{
    UILongPressGestureRecognizer *gesture = _reorderGestureRecognizer;
    const CGPoint location = [gesture locationInView:_tableView];

    CGPoint currentOffset = _tableView.contentOffset;
    CGPoint newOffset = CGPointMake(currentOffset.x, currentOffset.y + _scrollRate * 10);

    if (newOffset.y < -_tableView.contentInset.top)
    {
        newOffset.y = -_tableView.contentInset.top;
    }
    else if (_tableView.contentSize.height + _tableView.contentInset.bottom < _tableView.frame.size.height)
    {
        newOffset = currentOffset;
    }
    else if (newOffset.y > (_tableView.contentSize.height + _tableView.contentInset.bottom) - _tableView.frame.size.height)
    {
        newOffset.y = (_tableView.contentSize.height + _tableView.contentInset.bottom) - _tableView.frame.size.height;
    }

    [_tableView setContentOffset:newOffset];

    if (location.y >= 0 && location.y <= _tableView.contentSize.height + 50)
    {

        [self updateSnapshotWithPosition:location];
        NSIndexPath *indexPath = [self getCellIndexPathWithPoint:location];

        // CHeck if element is between offset limits.
        if (![indexPath isEqual:sourceIndexPath] &&
            indexPath.row >= _elementsOffset &&
            indexPath.row - _elementsOffset < [_elements count] &&
            sourceIndexPath.row >= _elementsOffset &&
            sourceIndexPath.row - _elementsOffset < [_elements count])
        {
            id sourceElement = [_elements objectAtIndex:sourceIndexPath.row-_elementsOffset];
            id targetElement = [_elements objectAtIndex:indexPath.row-_elementsOffset];
            [self exchangeElement:sourceElement byElement:targetElement];
            sourceIndexPath = indexPath;
        }
    }
}

B. Snapshot management

createSnapshotForCellAtIndexPath:withPosition

Method that creates a snapshot (a image copy) of the cell you are moving

-(UIView *)createSnapshotForCellAtIndexPath:(NSIndexPath *)indexPath withPosition:(CGPoint)location{
    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;
    }];

    return snapshot;
}

customSnapshoFromView:

Returns a customized snapshot of a given view. */

- (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.
    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;
}

updateSnapshotWithPosition:

Given a CGPoint, it changes the snapshot position to show the cell you are moving in the right place of the _tableView

-(void)updateSnapshotWithPosition:(CGPoint)location{
    CGPoint center = snapshot.center;
    center.y = location.y;
    snapshot.center = center;
}

deleteSnapshotForRowAtIndexPath:

When dragging finishes, you need to delete the snapshot from the _tableView

-(void)deleteSnapshotForRowAtIndexPath:(NSIndexPath *)sourceIndexPath{
    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) {
        [snapshot removeFromSuperview];
    }];
}

calculateScroll

-(void)calculateScroll:(UIGestureRecognizer *)gestureRecognizer{

    const CGPoint location = [gestureRecognizer locationInView:_tableView];

    CGRect rect = _tableView.bounds;
    // adjust rect for content inset as we will use it below for calculating scroll zones
    rect.size.height -= _tableView.contentInset.top;

    //[self updateCurrentLocation:gestureRecognizer];

    // tell us if we should scroll and which direction
    CGFloat scrollZoneHeight = rect.size.height / 6;
    CGFloat bottomScrollBeginning = _tableView.contentOffset.y + _tableView.contentInset.top + rect.size.height - scrollZoneHeight;
    CGFloat topScrollBeginning = _tableView.contentOffset.y + _tableView.contentInset.top  + scrollZoneHeight;

    // we're in the bottom zone
    if (location.y >= bottomScrollBeginning)
    {
        _scrollRate = (location.y - bottomScrollBeginning) / scrollZoneHeight;
    }
    // we're in the top zone
    else if (location.y <= topScrollBeginning)
    {
        _scrollRate = (location.y - topScrollBeginning) / scrollZoneHeight;
    }
    else
    {
        _scrollRate = 0;
    }

}

C. How to use it

In your init method, assign a gesture recognizer to the table view. Assign as action the method longPressGestureRecognized: as follows:

    _reorderGestureRecognizer = [[UILongPressGestureRecognizer alloc]
                                               initWithTarget:self action:@selector(longPressGestureRecognized:)];

    [_tableView addGestureRecognizer:_reorderGestureRecognizer];

Declare the variables you will need to use the above code explained

@implementation YourClassName{

    CADisplayLink *_scrollDisplayLink;
    CGFloat _scrollRate;
    UIView *snapshot; // A snapshot of the row user is moving.
    NSIndexPath *sourceIndexPath; // Initial index path, where gesture begins.
}

And that's everything you will need to solve the problem I had.

EnriMR
  • 3,924
  • 5
  • 39
  • 59
  • can you share all source code of your view controller or can you show what is '_elementsOffset'(how it is calculated) and other helper methods e.g. 'calculateScroll' – Karaban Jan 10 '17 at 08:26
  • elementsOffset is optional, you can use it if your table have elements in the top of your table that you wanted to lock. I have share in this repository my class https://github.com/enrimr/EMRReorderTableCells if you have any question, don't hesitate to ask me ;) Hope it can help you! – EnriMR Jan 11 '17 at 12:31
  • @EnriMR can you help out on https://stackoverflow.com/questions/72259537/drag-drop-table-view-cell-has-weired-buggy-animation-while-dragging-cell – Kishan Bhatiya May 16 '22 at 12:59
1

I was also looking for a way to render tableview cells by long pressing them. And I found a repository for that purpose. If you don't mind putting 3rd party library to your project check it out! :)

LPRTableView

Personally I use this code for my app Cheetah Note. and they are working like a charm! Highly recommended!

Jason Nam
  • 2,011
  • 2
  • 13
  • 22
  • 2
    I'm working with Objective-C but in the description of that repository, I have found that https://github.com/bvogelzang/BVReorderTableView .I'm going to test it – EnriMR Sep 11 '15 at 11:31
  • 1
    I can't compile that project – EnriMR Sep 11 '15 at 11:37
  • Yes I'm sorry to hear that.. The repository haven't been updated for so long. You may use LPRTableView with this tutorial https://developer.apple.com/library/prerelease/ios/documentation/Swift/Conceptual/BuildingCocoaApps/MixandMatch.html In the tutorial you would find a way to import swift class to object file :) – Jason Nam Sep 11 '15 at 13:52
0

Using option one try to scroll to the dragged cell position - 1 when the dragged cell is at the top:

NSArray *indexVisibles = [_tableView indexPathsForVisibleRows];
NSInteger indexForObject = [indexVisibles indexOfObject:indexPath];
if (indexForObject == 0){
     [_tableView scrollToRowAtIndexPath:indexPathForCellBefore
                                          atScrollPosition:UITableViewScrollPositionTop
                                                  animated:YES];
}

To smoothly scroll the tableview use the following snippet:

 [UIView animateWithDuration: 1.0 animations: ^{
     [_tableView scrollToRowAtIndexPath:indexPathForCellBefore atScrollPosition:UITableViewScrollPositionTop animated:NO];
 } completion: ^(BOOL finished){
 }];

Of-course you need to modify that to work when you scroll down.

hasan
  • 23,815
  • 10
  • 63
  • 101
  • It doesn't help me because the animation for `scrollToRowAtIndexPath:atScrollPosition:Animated` is very strong, I would like to scroll the UITableView smoothly – EnriMR Sep 14 '15 at 12:16
  • 1
    Yes, it solves one of my problems that is how to maintain the cell you are dragging in the position 0 when you drag it to the top, but the movement is not smooth – EnriMR Sep 14 '15 at 12:17
  • thats what I expected. just to confirm it. I will try to check for the other problem. – hasan Sep 14 '15 at 12:18
  • 3
    I've tried that `if (indexForObject == 0 && indexPath.row > 0){ [_tableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:indexPath.row-1 inSection:0] atScrollPosition:UITableViewScrollPositionMiddle animated:YES]; }` I don't know the difference for UITableViewScrollPositionMiddle and UITableViewScrollPositionTop. But with middle, it goes pretty fast (something that I neither want) But it's more real animation – EnriMR Sep 14 '15 at 12:19
  • Please check this repository hope this is work for you. github.com/hpique/HPReorderTableView – Rupal Patel Sep 14 '15 at 12:22