Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Autoscroll smoothly UITableView while dragging UITableViewCells in iOS app

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){

                                             }
                             ];
like image 420
EnriMR Avatar asked Sep 11 '15 10:09

EnriMR


People also ask

Should I use a scroll view or a UITableView?

For example, many developers make their life harder using a scroll view when a UITableView would be a better choice. Finally, architecture is crucial for table views. The code of the table view data source often ends inside view controllers when it should go into a separate class.

What is a uitableviewcell?

The visual representation of a single row in a table view. A UITableViewCell object is a specialized type of view that manages the content of a single table row. You use cells primarily to organize and present your app’s custom content, but UITableViewCell provides some specific customizations to support table-related behaviors, including:

What is the autoscroll iOS addon?

Just as the name implies, this add-on incorporates automatic scrolling into iOS. AutoScroll is very easy to use. Upon swiping once to get started, the tweak disables iOS’ native scrolling inertia, which means no slowdown and no halt.

How to reload data from uitableviewcontroller?

Were you using a UITableViewController? One of the few things it does is call reloadData () on the table view. Now you have to do it yourself. Place it after you assign the data source to the table view.


1 Answers

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.

like image 72
EnriMR Avatar answered Nov 15 '22 13:11

EnriMR