Skip to content

Latest commit

 

History

History
490 lines (374 loc) · 19.2 KB

README.md

File metadata and controls

490 lines (374 loc) · 19.2 KB

EMRReorderTableCells

This Objective-C class allows you to reorder cells in a UITableView. Each element in cells have to implement a property called "position".

You can use this to reorder simple elements or tables with subelements (i.e. tasks with subtasks)

This is an autoanswer to a StackOVerflow question (http://stackoverflow.com/questions/32521725/autoscroll-smoothly-uitableview-while-dragging-uitableviewcells-in-ios-app)

## Example of use ##

 [[EMRReorderTableCells alloc] initWithTableView:_taskTableView
                            elements:[_tasks objectAtIndex:0]
                      elementsOffset:5
                 collapseSubElements:YES
    collapseSubCellsAtIndexPathBlock:^(NSUInteger idx) {
        // In case you have subelements
        NSIndexPath *indexPath = [NSIndexPath indexPathForRow:idx inSection:0];
        [self expandCollapseSubtasksAtIndexPath:indexPath];
    } removeSubElementsForElementBlock:^(id element) {
        [self removeSubtasksForTask:(Task *)element];
    } removeSubElementsForElementAtIndexPathBlock:^(NSIndexPath *indexPath) {
        [self removeSubtasksAtIndexPath:indexPath];
    } areBrothersElements:^BOOL(id source, id target) {
        return [(Task *)source isBrotherOfTask:(Task *)target];
    } isSubElementBlock:^BOOL(id element) {
        return ![[(Task *)element subtask] isEqualToString:@"0"];
    } isHidingSubElementsBlock:^BOOL(id element) {
        return [[(Task *)element hidesubtasks] isEqualToNumber:@0];
    } completionBlock:^(id element) {
        // Update element
        [_httpClient createUpdateRequestForObject:element withPath:@"task/" withRegeneration:NO];
        [_httpClient update:nil];
    }];

Params

  • elements: NSArray with elements to reorder.

  • elementsOffset: Integer with an offset. Imagine a main menu where you have three main sections and a list of custom sections. If you want to allow only to reorder the custom sections, you have to specify an offset of 3. The first three cells will be locked.

  • collapseSubElements: Bool that specify if you want to collapse subelements or not

Blocks meaning

  • collapseSubCellsAtIndexPathBlock: A block that implement the action of collapsing subelements' cells

  • removeSubElementsForElementBlock: A block that implement the action of removing subelements from the element list for a given element.

  • removeSubElementsForElementAtIndexPathBlock: A block that implement the action of removing subelements from the element list for a given index path.

  • areBrothersElements: A block that return true if both elements are brothers (they are in the same level) or false if they are father and son or cousins.

  • isSubElementBlock: A block that return true if element is a subelement and false if is a single element without children.

  • isHidingSubElementsBlock: A block that return true if element if an element is hiding subelements and false if not.

  • completionBlock: A block that perform and action after finishing the reorder.

Example of blocks

-(void)expandCollapseSubtasksAtIndexPath:(NSIndexPath *)indexPath{
    NSLog(@"Row: %ld - Section: %ld", (long)indexPath.row, (long)indexPath.section);
    if (indexPath != nil)
    {
        Task *task = [[_tasks objectAtIndex:indexPath.section] objectAtIndex:indexPath.row];
        TaskTitleCell *cell = (TaskTitleCell *)[_taskTableView cellForRowAtIndexPath:indexPath];
        UILabel *subtasksNumberLabel = (UILabel *)[cell viewWithTag:107];
        UIButton *subtasksButton = (UIButton *)[cell viewWithTag:108];
        
        NSMutableArray *subtasksIndexPaths = [[NSMutableArray alloc] init];
        
        NSDictionary *subtasksAndIndexesDictionary = [task getSubtasksIndexesInTaskCollection:[_tasks objectAtIndex:indexPath.section] ofList:task.list];
        //NSDictionary *subtasksAndIndexesDictionary = [task getSubtasksIndexesInTaskCollection:[_tasks objectAtIndex:indexPath.section] ofList:task.list filteredBy:(int)_selectedFilter];
        
        NSIndexSet *indexes = [subtasksAndIndexesDictionary objectForKey:@"indexes"];
        NSArray *subtasks = [subtasksAndIndexesDictionary objectForKey:@"subtasks"];
        
        [indexes enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) {
            NSIndexPath *subtaskIndexPath = [NSIndexPath indexPathForRow:idx inSection:indexPath.section];
            [subtasksIndexPaths addObject:subtaskIndexPath];
        }];
        
        NSNumber *hidden;
        //Expand
        if (!subtasksButton.selected){
            hidden = @0;
            //[task setHidesubtasks:@0];
            subtasksNumberLabel.textColor = [UIColor colorWithRed:72.0/255.0 green:175.0/255.0 blue:237.0/255.0 alpha:1.0];
            
            [[_tasks objectAtIndex:indexPath.section] insertObjects:subtasks atIndexes:indexes];
            
            [_taskTableView insertRowsAtIndexPaths:subtasksIndexPaths withRowAnimation:UITableViewRowAnimationTop];
            
            //Collapse
        }else{
            hidden = @1;
            //[task setHidesubtasks:@1];
            subtasksNumberLabel.textColor = [UIColor whiteColor];
            
            NSArray *subtasks = [task getSubtasks];
            
            if (subtasks){
                
                [[_tasks objectAtIndex:indexPath.section] removeObjectsInArray:subtasks];
                
                [_taskTableView deleteRowsAtIndexPaths:subtasksIndexPaths withRowAnimation:UITableViewRowAnimationBottom];
            }
            
        }
        
        subtasksButton.selected = !subtasksButton.selected;
        //task.hidesubtasksValue = !subtasksButton.selected;
        task.hidesubtasks = hidden;
        
        [[NSManagedObjectContext MR_defaultContext] MR_saveToPersistentStoreAndWait];
    }
}

-(void)removeSubtasksForTask:(Task *)task{
        NSNumber *hidden = @1;
        
        NSArray *subtasks = [task getSubtasks];
    
        [_tasks removeObjectsInArray:subtasks];
        
        task.hidesubtasks = hidden;
        
        [[NSManagedObjectContext MR_defaultContext] MR_saveToPersistentStoreAndWait];
}

-(void)removeSubtasksAtIndexPath:(NSIndexPath *)indexPath{
    Task *task = [_tasks objectAtIndex:indexPath.row];
    TaskTitleCell *cell = (TaskTitleCell *)[_taskTableView cellForRowAtIndexPath:indexPath];
    UILabel *subtasksNumberLabel = (UILabel *)[cell viewWithTag:107];
    UIButton *subtasksButton = (UIButton *)[cell viewWithTag:108];
    
    NSMutableArray *subtasksIndexPaths = [[NSMutableArray alloc] init];
    NSNumber *hidden = @1;

    subtasksNumberLabel.textColor = [UIColor whiteColor];
    
    NSArray *subtasks = [task getSubtasks];
    
    [_tasks removeObjectsInArray:subtasks];
    
    for (int i=1;i<=subtasks.count; i++){
        NSIndexPath *subtaskIndexPath = [NSIndexPath indexPathForRow:indexPath.row+i inSection:0];
        [subtasksIndexPaths addObject:subtaskIndexPath];
    }
    
    [_taskTableView deleteRowsAtIndexPaths:subtasksIndexPaths withRowAnimation:UITableViewRowAnimationBottom];
    
    subtasksButton.selected = !subtasksButton.selected;
    task.hidesubtasks = hidden;
    
    [[NSManagedObjectContext MR_defaultContext] MR_saveToPersistentStoreAndWait];
}

Explanation of reorder

I have use some inspiration from https://github.com/hpique/HPReorderTableView

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

LICENSE

Copyright (c) 2017. Enrique Ismael Mendoza Robaina

EMRReorderTableCElls is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version.

Playtch is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.