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.
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:
Option 2:
[UIView animateWithDuration: 1.0
animations: ^{
[_tableView scrollToRowAtIndexPath:indexPath
atScrollPosition:UITableViewScrollPositionMiddle
animated:NO];
}completion: ^(BOOL finished){
}
];
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.
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:
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.
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With