Tom

13th September 2011

Reordering a UITableViewCell from any touch point

Guides | Tutorial By 3 years ago

(Now updated for iOS 7 and ARC)

A component of one of our upcoming apps is a Scramble type game where letters are rearranged to solve the word. I had a fantastic idea to use a UITableView flipped on it’s side with reordering enabled to make up the bulk of the Scramble reordering functionality. It worked without a hitch. During development we found that having the cell’s reordering grip only taking up a 44×44 square wasn’t very effective when our letters were a lot bigger, you would have to have your finger exactly in that small area in order to move the letters around. With a bit of magic I managed to make the reorder grip fill the entire cell, and this is how you do it:

Open the project startpoint and go to the LargeTableGripViewController.m. The tableView:willDisplayCell:forRowAtIndexPath: is the place we will need to put the code. Firstly we’ll see what subviews are actually in our cell. I’ve included a UIView+SubviewHunting category to make this a bit easier. Log all the subviews with the current code. It simply prints out a tree of all the subviews in a view.

Note that the -debugSubviews and -huntedSubviewWithClassName: methods are provided in the project startpoint. Make sure you download this.

[cell debugSubviews];

Run the project and you will see a view called “UITableViewCellReorderControl” being logged. This is where the grip touches are handled. You may have never heard of UITableViewCellReorderControl, that’s because it is a private class that isn’t publicly accessible to developers.

Because we dont have the class definition for “UITableViewCellReorderControl“, we will check it’s string description while looping through the subviews to make sure we are only affecting the “UITableViewCellReorderControl” class. I’ve made a convenience method for finding a nested subview based on class name.

UIView* reorderControl = [cell huntedSubviewWithClassName:@"UITableViewCellReorderControl"];
NSLog(@"%@", reorderControl);
[reorderControl setBackgroundColor:[UIColor redColor]];

Run the project, only the “UITableViewCellReorderControl“s will be logged, and they will be set to have a red background colour. Try reordering the table, you will notice that reordering only happens when your touches are exactly within the red box.

You can try setting the frame of this view, but the UITableView will just set it’s frame back after tableView:willDisplayCell:forRowAtIndexPath: is called making our attempts useless, so we need a workaround. This took me a while to figure out, but we need to add the “UITableViewCellReorderControl” to our own custom subview, and then add the custom subview to the cell and set the custom subview’s transform to stretch it over the entire cell.

UIView* reorderControl = [cell huntedSubviewWithClassName:@"UITableViewCellReorderControl"];

[reorderControl setBackgroundColor:[UIColor redColor]];

UIView* resizedGripView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, CGRectGetMaxX(reorderControl.frame), CGRectGetMaxY(reorderControl.frame))];
[resizedGripView setBackgroundColor:[UIColor greenColor]];
[resizedGripView addSubview:reorderControl];
[cell addSubview:resizedGripView];

Run the app and now all cell’s are filled with our custom green view with the red grip on the right. Reordering functionality will still work. Now it’s time to figure our what transform this custom view requires for it to fill the entire cell.

UIView* reorderControl = [cell huntedSubviewWithClassName:@"UITableViewCellReorderControl"];

[reorderControl setBackgroundColor:[UIColor redColor]];

UIView* resizedGripView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, CGRectGetMaxX(reorderControl.frame), CGRectGetMaxY(reorderControl.frame))];
[resizedGripView setBackgroundColor:[UIColor greenColor]];
[resizedGripView addSubview:reorderControl];
[cell addSubview:resizedGripView];

CGSize sizeDifference = CGSizeMake(resizedGripView.frame.size.width - reorderControl.frame.size.width, resizedGripView.frame.size.height - reorderControl.frame.size.height);
CGSize transformRatio = CGSizeMake(resizedGripView.frame.size.width / reorderControl.frame.size.width, resizedGripView.frame.size.height / reorderControl.frame.size.height);

//	Original transform
CGAffineTransform transform = CGAffineTransformIdentity;

//	Scale custom view so grip will fill entire cell
transform = CGAffineTransformScale(transform, transformRatio.width, transformRatio.height);

//	Move custom view so the grip's top left aligns with the cell's top left
transform = CGAffineTransformTranslate(transform, -sizeDifference.width / 2.0, -sizeDifference.height / 2.0);

[resizedGripView setTransform:transform];

This will make the grip take up the entire cell, you can now reorder from any point on the cell, perfect. Now just some cleanup, remove the red and green colours from the custom view and the “UITableViewCellReorderControl” view. The 3 line grip image will remain. The “UITableViewCellReorderControl” uses a UIImageView subview to display this image, so just loop through the subviews and check the class type, then set any UIImageView‘s image to nil.

for(UIImageView* cellGrip in reorderControl.subviews)
{
	if([cellGrip isKindOfClass:[UIImageView class]])
		[cellGrip setImage:nil];
}

And that is all that’s required, not too much code at all, just a bit of subview hunting and testing to see what works. Here is the complete tableView:willDisplayCell:forRowAtIndexPath: method we wrote

- (void) tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
	//	Grip customization code goes in here...
	UIView* reorderControl = [cell huntedSubviewWithClassName:@"UITableViewCellReorderControl"];

	UIView* resizedGripView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, CGRectGetMaxX(reorderControl.frame), CGRectGetMaxY(reorderControl.frame))];
	[resizedGripView addSubview:reorderControl];
	[cell addSubview:resizedGripView];

	CGSize sizeDifference = CGSizeMake(resizedGripView.frame.size.width - reorderControl.frame.size.width, resizedGripView.frame.size.height - reorderControl.frame.size.height);
	CGSize transformRatio = CGSizeMake(resizedGripView.frame.size.width / reorderControl.frame.size.width, resizedGripView.frame.size.height / reorderControl.frame.size.height);

	//	Original transform
	CGAffineTransform transform = CGAffineTransformIdentity;

	//	Scale custom view so grip will fill entire cell
	transform = CGAffineTransformScale(transform, transformRatio.width, transformRatio.height);

	//	Move custom view so the grip's top left aligns with the cell's top left
	transform = CGAffineTransformTranslate(transform, -sizeDifference.width / 2.0, -sizeDifference.height / 2.0);

	[resizedGripView setTransform:transform];

	for(UIImageView* cellGrip in reorderControl.subviews)
	{
		if([cellGrip isKindOfClass:[UIImageView class]])
			[cellGrip setImage:nil];
	}
}

And here is the project endpoint.

  • http://iro-iro.de chris

    Hey Tom!
    Thanks a bunch! I was looking for a way to redesign the reorder button. When I came across your for-loop I felt quite embarrassed because looping through the views to get the three-bar-image should be the first thing to try, wouldn’t it?! Sometimes it is this easy…

    So cheers!
    Chris

  • Sam

    Awesome walkthough, this helped me out a ton.
    Thanks!

    (btw your captchas are really hard…)

  • http://Website Nitesh

    Thanks a Tom!!

    Brilliant work!!

  • http://www.moggytech.co.uk/ MoggyTech

    Really useful post, and I love the step by step how you worked out the framing etc. Really makes it clear to see where the code came from.

    Thank you!

  • http://Website Lukasz

    This does not work from iOS 5 perfectly anymore. willDisplayCell is not called immediately when you put cell in editing mode but when you finish dragging it. This means default reorder control is shown initially without change up until you touch and move it to the other place in UITableView. It is being refreshed when you drop it.

  • http://Website steve

    Great post. Just what I need. Love this comments box too : )

    One question though, will this get through Apple’s testing?

    Thanks, Steve

    • Tom

      Because we’re only comparing the private class via it’s string description it will be fine – I have done this on many projects all on the AppStore :-)

  • http://Website Ranjit

    Hi Tom, thanks for your post, I am trying to do the same thing, I have a customcell,with a button on it on left side,what I want is that, the cells should be reorder only if you touch the button and no where.Can you help me out

    • Tom

      You should be able to use most of the code above, with some minor tweaks. Substitute by scale/translation transform for just a translation, equal to that of the reorder width minus the cell’s width

  • http://Website Ranjit

    Thanks, tom, I have one problem, I have kept the reorder control on left side and not on the whole cell. The problem that I am facing is that, whenever I scroll the cells and if the cell goes out of the screen then the reorder control for that cell disappears. How to solve this issue.

    Thanks

    • Tom

      Replace the transform code above with:

      const CGAffineTransform transform = CGAffineTransformMakeTranslation(view.frame.size.width - cell.frame.size.width, 1);
  • http://Website Ranjit

    Was that for me, I didnt understand your comment

    • Tom

      Yep Ranjit – if you mean you just want to move the reorder control to the left then my transform code snippet above will work

  • http://Website Ranjit

    Sorry, my mistake, I will explain you what is happening, I have implemented using your code, but with a small change the control to move cells is on left side and not on right. Now whenever I scroll my tableview, then cells at the top go out of the screen, and again when I scroll back, to see the cells, what is happening is that the reoreder control disappears. How to solve this.

    • Tom

      Use my exact code from the blog, but replace the transform line with the code in my comment above

  • http://Website Ranjit

    Hey it works, But I dont understand how? Can you throw some light on it

    • Tom

      I don’t know what your code looked like, but this is exactly the same as mine in the original blog except for the transform part – which will work as desired

  • http://www.facebook.com/rajesh.clarion Rajesh Clarion

    Nice concept

  • http://twitter.com/kyleplattner Kyle Plattner

    This solution works really well until the view is scrolled out of the view (dequeued) and then back into the view (enqueued). Once the cell reenters the view the cell can no longer be grabbed. Any ideas?

  • http://twitter.com/M0rph3v5 Benjamin de Jager

    Is it possible to use this and keep the didselectrow working? (I’m always in the editing state).

  • http://www.facebook.com/abhinavsingh89 Abhinav Singh

    Hey!!
    Great idea!! I just need a bit of help. I am trying to move this to the left instead of right. I made the constant translation and my frame looks proper. But I have button on the right also. That button doesnt seems to works even though reordering control has been moved to right. Part of the button works which is not below the reordering control .Can you provide some insights on this? Like how it can be done?

  • Samir Saxena

    Kudos for this great tutorial..
    Please explain, if using this code, it is possible to also delete a cell by swiping left or right?

  • Ranjit

    Hi, I used your code for iphone , it works perfectly fine, Now I am creating an ipad app with Master- Detail View using UISplitViewController, Now in both master and deatilViews, I have UITableView, So what happens is that , when I select a row in master View , I update the rows in DetailView. So on launch of my application, I am selecting row 0 in MasterView by default so that the rows in detailView are updated. Here the reorder control shifts to left from right. Now when I select a different row in MasterView and then come back to the previous row, now the reorder control is at right. How to solve this issue?

  • FastEddieJr

    I am using this in a subview that works perfectly when the subview is first called, but when the subview is called a second time it loses re-ordering capabilities. Any suggestions as to why?

  • Ranjit

    RightSwipeGesture and didSelectRowAtIndexPath functions are not called when I use your code. How to solve this

  • Kyle Plattner

    Any idea on how to fix this for iOS 7?

    • http://b2cloud.com.au/ Tom

      As a quick fix replace

      for(UIView* view in cell.subviews)
      with
      for(UIView* view in [[cell.subviews objectAtIndex:0] subviews])

      I’ll update the code a bit later to be a bit more robust

    • http://b2cloud.com.au/ Tom

      As a quick fix replace

      for(UIView* view in cell.subviews)
      with
      for(UIView* view in [[cell.subviews objectAtIndex:0] subviews])

      I’ll update the code a bit later to be a bit more robust

      • Kyle Plattner

        That worked. Please let me know when you update the code. Thanks.

        • http://b2cloud.com.au/ Tom

          Ok give this a go now. I have added an extra class, you can get it straight from the project endpoint download

          • Kyle Plattner

            So you are just recursively crawling subviews until you find the one with the right description?

          • Kyle Plattner

            So you are just recursively crawling subviews until you find the one with the right description?

          • http://b2cloud.com.au/ Tom

            Yes. Either way will work, however this is a bit more future proof than just going on level deep as I was before

  • Kyle Plattner

    When this was implemented in iOS 6 a scroll of the tableview was distinguished from a reorder. This is not the case in iOS 7. Any idea how I can have editing turned on for tap-hold drag reordering and still allow scrolling?

    • http://b2cloud.com.au/ Tom

      It’s working fine for me. If you’re using my code example .zip download, I manually disabled scrolling, see the [largeGripTable setScrollEnabled:NO]; call in viewDidLoad

      • Kyle Plattner

        If you enable scrolling you cannot scroll and drag to reorder while in editing mode. This worked under iOS 6.

        • http://b2cloud.com.au/ Tom

          Try this, although I’d recommend moving the code for resizing the grip into a new function

          - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView

          {

          [largeGripTable setEditing:NO];

          }

          - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate

          {

          if(!decelerate)

          {

          [largeGripTable setEditing:YES];

          for(UITableViewCell* cell in largeGripTable.visibleCells)

          [self tableView:largeGripTable willDisplayCell:cell forRowAtIndexPath:nil];

          }

          }

          - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView

          {

          [largeGripTable setEditing:YES];

          for(UITableViewCell* cell in largeGripTable.visibleCells)

          [self tableView:largeGripTable willDisplayCell:cell forRowAtIndexPath:nil];

          }

          • Kyle Plattner

            It scrolls the first time you try and then you can’t get it out of editing mode.

      • Kyle Plattner

        Change [largeGripTable setScrollEnabled:NO] to YES and then try to scroll consistently. About half the time it will try to reorder instead of scroll.

      • Kyle Plattner

        Would you be willing to take a minute and look at our project to see what I am referencing?

  • James

    Hey Tom, I directly used ur code in a UITableViewController instead of just a ViewController in iOS7 and everything works fine. I now need to color code each of my cells based on a certain property of the cell and iOS guidlines say I should do this in willDisplayCells as well. I input a toggle since I only want to be able to edit by dragging in editMode(I also need to be able to click on each cell to activate a push segue) My question is how do I implement the color background of the cell in willdisplayCell without interfering with ur code and but also take effect when isEditing is NO. Thanks.

    • http://b2cloud.com.au/ Tom

      You should just be able to put it in the willDisplayCell: method either before or after my block of code. I don’t think it will interfere if all you’re doing is changing the background color

      • James

        THanks for the speedy response. Do I need to section ur code off somehow via () or can I just began addting code right after ur section?

        • http://b2cloud.com.au/ Tom

          I would create methods to split the logic up, and then call both from the willDisplayCell: method

  • James

    Also, my view controller is a subclass of tableviewcontroller so does this mean I don’t have to do anything related to table view delegate?

  • James

    Is there a way to make reorderControl in the background of the cell but still function? I want to set a background color of blue on my cell but then the right side is left white which is awkward and when I set the color of reorderControl to blue it covers the whole cell and blocks the text. Any ideas on how to get around this? Thanks so much.
    One strategy I thought is to make the green portion of the cell come all the way to the right. In your guide, you simple covered the green portion with the red grip. I’m not sure how to implement this though. Any advice would be much appreciated.

    This is from ur guide:
    //Move custom view so the grip’s top left aligns with the cell’s top left

    I want to do this but also align cell’s right with grip’s right so they are perfectly on top of each other.

    • James

      One strategy I thought is to make the green portion of the cell come all the way to the right. In your guide, you simple covered the green portion with the red grip. I’m not sure how to implement this though. Any advice would be much appreciated

      • James

        // Move custom view so the grip’s top left aligns with the cell’s top left

        i want to do this but align cell’s right with grip’s right so they r perfectly on top of each other.

  • bhushan uparkar

    when i long press any cell, its losing focus and goes back to original position. unable to drop. any ideas why this is happening. My UITableview is added programatically.

    • http://b2cloud.com.au/ Tom

      Download my project end point and work from there, it works fine in that

  • Samir Saxena

    Kudos for this great tutorial..
    Please explain, if using this code, it is possible to also delete a cell by swiping left or right?

Recommended Posts

Make iOS talk (speech synthesizing)

Post by 3 years ago

iOS7 introduced the abiltiy for developers to do speech synthesizing. This means now you can translate text into speech. It’s quite simple to do: First import the AVFoundation framework – you may also need to

Got an idea?

We help entrepreneurs, organizations and established brands from around
the country bring ideas to life. We would love to hear from you!