Full Source code: https://github.com/boctor/idev-recipes/tree/master/SideSwipeTableView
Problem:
The Twitter iPhone app pioneered the ability to swipe on a tweet and have a menu appear, letting you do things like reply or favorite the tweet.
Tweets in the Twitter app are table view cells in a table view. How do we recreate this feature and add the ability to side swipe on table view cells?
Solution:
This feature has two distinct parts. The first is detecting that the user swiped on the table. The second is animating in and animating out the menu view.
Detecting swipes in iOS 4
iOS 4 introduced Gesture Recognizers which make gestures like swiping, tapping and pinching very east to detect. Specifically we can create a couple of UISwipeGestureRecognizer objects, one for the right direction and another for the left detection and attach them to the table view:
// Setup a right swipe gesture recognizer UISwipeGestureRecognizer* rightSwipeGestureRecognizer = [[[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(swipeRight:)] autorelease]; rightSwipeGestureRecognizer.direction = UISwipeGestureRecognizerDirectionRight; [tableView addGestureRecognizer:rightSwipeGestureRecognizer]; // Setup a left swipe gesture recognizer UISwipeGestureRecognizer* leftSwipeGestureRecognizer = [[[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(swipeLeft:)] autorelease]; leftSwipeGestureRecognizer.direction = UISwipeGestureRecognizerDirectionLeft; [tableView addGestureRecognizer:leftSwipeGestureRecognizer];
Now when the user swipes left or right anywhere in the table, our swipeRight: or swipeLeft: methods will get called. The touch handling code that tracks the user’s finger and figures out their intent is blissfully handled for us.
Detecting swipes in iOS 3
When this feature was introduced in what was then the Tweetie app, it worked in iOS 3 well before iOS 4 was released. You might smartly argue that if iOS 3 currently makes up 1-2% of users out there it isn’t worth developing for and I admit this is a valid point. Still it’s an interesting technical mystery and that’s just the kind of thing we love solving!
You might have thought like I did, that the Twitter app must have implemented its own touch handling, guessing based on the location of your finger whether you were trying to do a swipe, but this is wrong.
In the Twitter app it doesn’t matter if you swipe left or swipe right, the animation of the menu always happens from left to right. This is the same behavior as the editing of table view cells and it turns out this is how the Twitter app does it: It hijacks the built in swipe to delete feature of table view cells. There are 3 parts to making this work:
1. Enabling swipe to delete
To enable the swipe-to-delete feature of table views (wherein a user swipes horizontally across a row to display a Delete button), you must implement the tableView:commitEditingStyle:forRowAtIndexPath: method
So the first step is to implement the tableView:commitEditingStyle:forRowAtIndexPath: method.
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath { }
The method doesn’t have to do anything. Once it is implemented, you’ll be able to side swipe on a cell and the Delete button will appear.
2. Disabling the Delete button
Apple’s Inserting and Deleting Rows and Sections documentation indicates that when you explicitly put a table in editing mode by calling setEditing:animated:, the same message is then sent to each of the visible cells.
The documentation for a table view cell’s setEditing:animated: indicates that when this method is called, insertion/deletion control are animated in.
So disabling the Delete button turns out to be relatively simple: Override the table view cell’s setEditing:animated: and don’t call the superclass’s implementation.
- (void)setEditing:(BOOL)editing animated:(BOOL)animated { // We suppress the Delete button by explicitly not calling // super's implementation if (supressDeleteButton) { // Reset the editing state of the table back to NO UITableView* tableView = [self getTableView:self]; tableView.editing = NO; } else [super setEditing:editing animated:animated]; }
3. Getting notified when a swipe occurred
Apple’s docs for tableView:willBeginEditingRowAtIndexPath: are crystal clear: This method is called when the user swipes horizontally across a row.
The implementation of this method in iOS 3 is a parallel to the swipeLeft: and swipeRight: methods we registered with UISwipeGestureRecognizers under iOS 4. When any of these methods are called, we know that a swipe happened and we are ready to animate in the menu.
Animating in the menu view
Before we animate in the menu, we first add it as a subview of the table view.
As you can see in the image at the top of this post, we are animating the existing cell content offscreen while simultaneously animating in the menu. Here is a rough illustration of how both the cell content and the menu have to animate in sync during a left to right animation:
So we first set the frame of the menu, placing it offscreen. Depending of the direction, we’d put it offscreen on the right or left side of the table. Next we’d start an animation block and set the frame of the menu to be at 0 x-offset. Inside the same animation block we also set the cell’s frame to be offscreen on the other side of the table.
- (void) addSwipeViewTo:(UITableViewCell*)cell direction:(UISwipeGestureRecognizerDirection)direction { // Change the frame of the side swipe view to match the cell sideSwipeView.frame = cell.frame; // Add the side swipe view to the table [tableView addSubview:sideSwipeView]; // Remember which cell the side swipe view is displayed on and the swipe direction self.sideSwipeCell = cell; sideSwipeDirection = direction; // Move the side swipe view offscreen either to the left or the right depending on the swipe direction CGRect cellFrame = cell.frame; sideSwipeView.frame = CGRectMake(direction == UISwipeGestureRecognizerDirectionRight ? -cellFrame.size.width : cellFrame.size.width, cellFrame.origin.y, cellFrame.size.width, cellFrame.size.height); // Animate in the side swipe view animatingSideSwipe = YES; [UIView beginAnimations:nil context:nil]; [UIView setAnimationDuration:0.2]; [UIView setAnimationDelegate:self]; [UIView setAnimationDidStopSelector:@selector(animationDidStopAddingSwipeView:finished:context:)]; // Move the side swipe view to offset 0 sideSwipeView.frame = CGRectMake(0, cellFrame.origin.y, cellFrame.size.width, cellFrame.size.height); // While simultaneously moving the cell's frame offscreen // The net effect is that the side swipe view is pushing the cell offscreen cell.frame = CGRectMake(direction == UISwipeGestureRecognizerDirectionRight ? cellFrame.size.width : -cellFrame.size.width, cellFrame.origin.y, cellFrame.size.width, cellFrame.size.height); [UIView commitAnimations]; }
Animating out the menu view
When the menu is animated away, there is a little bounce of the cell content as it comes back into view.
Here is another rough illustration showing the three animations that make up a bounce, showing at each step where the cell content and menu are:
There are multiple ways we can achieve this animation. One is CAKeyframeAnimation where you specify a path and the animation follows that path.
Instead the code simply chains together the 3 separate animations. Since we might care about iOS 3, we don’t use animation blocks, but instead use begin/commit animation methods and register an animation stop selector where we start the next animation.
Just like we did when animating the menu in, at each step we animate both the menu as well as the cell to give the illusion that the cell content is pushing the menu out of view.
UPDATE: It was pointed out on Hacker News that the Twitter app actually puts the menu behind the cell and then only animates the cell content in and out. The menu isn’t animated at all. I’ve updated the code so that by default it now does this style of animation. If you really liked the pushing behavior where both the menu and cell content are animated, there is a PUSH_STYLE_ANIMATION #define that you can set to YES to get it back.
Full Source code: https://github.com/boctor/idev-recipes/tree/master/SideSwipeTableView
Great post! Looking forward to trying it out. Love this blog! Keep up the good work!
Nice work! Thanks a lot
Excellent posts, I look forward to when I see these on twitter or when I see them via RSS. Always well done!
Great post Peter.
Thanks.
Great article! I would like to stress that those 1-2% of 3.x users is probably not that low. Think of the old ipod users that didn’t want to pay for the upgrade to 4.x from 3.1.3. Just my 2 cents.
Some interesting behavior noticed:
On an iPhone 3Gs device (iOS 4.3.1)
– Run the app as-is, scroll down to row #8
– swipe left (or right) on row #8 to show the menu.
– On row #7, tap with one finger (pause for a second, but continuing to hold down) and swipe left (or right).
– You should see the standard “Delete” red button appear
Not sure if this is expected behavior or a bug in the implementation.
Thanks for the detailed report! I just pushed a change to GitHub to fix this. The problem is that we continue to implement tableView:commitEditingStyle:forRowAtIndexPath: so the Delete button is always enabled. My latest change implements tableView:canEditRowAtIndexPath: and returns NO when we are using gestures.
[…] Read the original here: How does the Twitter iPhone app implement side swiping on a table … […]
Awesome! I always look forward to seeing an article from this site pop up in my RSS feed!
Peter, in my previous comment, I forgot to say how much I enjoy all of these tutorials that you do. They are awesome.
Perhaps I’m missing it, but is there a way to determine which row (indexPath) was “swiped” when you click on one of the button items. I notice the UIAlert happens and tells you which button you clicked on, but I don’t see a “for what row” value. I think this would be essential in a real implementation since you want to perform an action (retweet, reply, etc…) for the given row.
Thanks!
Thanks Brian! To find the indexPath of the row where the swiped menu appears you can use [tableView indexPathForCell:sideSwipeCell]. I updated the code on Github so the alert includes the row index.
Amazingly simple! I think yours works better then Twitter too. Keep up the good work!
thanks!
do you know how much i love you, haha. I really enjoy learning everything from you. Hope to be somebody like you~~
Brillant !
I now have to merge this with the pull to refresh feature 😉
BTW, the demo code is using this :
UIImage* newImage = [UIImage imageWithCGImage:newCGImage scale:startImage.scale orientation:startImage.imageOrientation];
[UIImage imageWithCGImage: scale: orientation:] have been added in iOS4.x, on iOS3.x, using [UIImage imageWithCGImage:] instead should be sufficient in most cases, isn’t it ?
Hey Boctor,
I’ve noticed that there is a bit of lag when scrolling when a sideSwipeView is on the screen on a large tableview with detailText and imageViews. I think implementing FastScrolling will help to a point, but it doesn’t solve the problem of the removeSideSwipe: taking up memory away from scrolling. I added a “if (sideSwipeCell) to scrollViewWillBeginDragging and that seemed to help a bit.
I’m wondering if you have any other thoughts on how to speed up scrolling? I have some other ideas that I’m testing. I’ll let you know what I come up with.
Conceptually, I think this is the root of the problem.
Normally, when you create an animation you set the Duration of the animation. iOS then knows how many frames per second it needs to render to execute your animation and can do it very efficiently. With ScrollViews, the system has no idea when you begin dragging how many frames it needs to render and has to make it up by doing this process in real time. That is what makes ScrollViews very expensive operations. Since animations can only happen on the main thread, executing both a removeSideSwipe and Scroll at the same time is super expensive and causes the lag in responsiveness.
I’ve been trying to delay the removeSideSwipe by using GCD, but I’m pretty new to using dispatches so I haven’t gotten very good results.
Anybody else have any ideas? I’m happy to test them out.
Excellentte!
Love the tutorials! How about making one regarding how Path & Instagram implement UIImagePickerController? Did they make it from scratch?
They probably use something like this.
https://github.com/elc/ELCImagePickerController
The tutorial is amazing!!! thanks a lot!!!
I want to ask if you can implement this when you are in a searchResultsTableView
thanks again!
Did you find how to do it ?
You wrote “iOS 4 introduced Gesture Recognizers ….”. According to the link that you included, Apple says “To help applications detect gestures, iOS 3.2 introduces gesture recognizers, objects that inherit directly from the…”. Theoretically the Gesture Recognizers are not iOS 4, but rather iOS 3.2 and up.
Thanks for the post. It still helped me find what I was looking for.
Hey Boctor,
Great tutorial.
I am trying to modify this implementation a bit so the buttons are loaded each time a swipe action is made, and different buttons are loaded for different types of cells.
When I re-setup sideSwipeView using the following code,
self.sideSwipeView.frame = CGRectMake(tblView.frame.origin.x, tblView.frame.origin.y, sideSwipeView.frame.size.width, sideSwipeView.frame.size.height);
the autoresizingmask seems to go haywire. Once the orientation is changed, it doesnt seem to autoresize. Any idea how this can be handled?
Thanks.
[…] http://idevrecipes.com/2011/04/14/how-does-the-twitter-iphone-app-implement-side-swiping-on-a-table/ Categories: Uncategorized Tags: iPhone Development LikeBe the first to like this post. Comments (0) Trackbacks (0) Leave a comment Trackback […]
Another fan. Awesome work, and so well articulated. Thank you!
[…] Side swiping in table cells (tags: iphone ios code howto objectivec) […]
I’m having an issue with sideSwipeCell. The gradient and buttons are below the swiped cell. Any ideas?
Hi,
Thanks for this great tutorial, it works fine but (there is always a but lol) I have issue 🙂
If you use a clearColor and put an image on the background of the tableView, when the SwipeCell dissappear, the orginal cell appear on the swipeCell with the SwipeCell in background, at the end of the animation, the swipeCell dissapear.
How can I fix it ?
Thanks
Spectacular. Thank you very much!
To those who see the button’s and background down in the cell below the swiped cell. You’ve probably got a 44 px navigationBar above the tableview. I did. Just subtract that 44 px from the initWithFrame:
self.sideSwipeView = [[[UIView alloc] initWithFrame:CGRectMake(self.tableView.frame.origin.x, self.tableView.frame.origin.y -44, self.tableView.frame.size.width, self.tableView.rowHeight)] autorelease];
I am looking for something similar, but not exactly the same: I would like to show images when swiping left and right. Like AppsFire does. Is there any code or example to accomplish that?
I almost forgot to mention: GREAT BLOG!!!
THIS IS JUST PERFECT!!! thank you.. would wish if it does generate sound when you swipe the cell and the buttons shine each time like twitter app…
btw, thanks Michael, in my case I fixed the height by (-22 px)
Just what I am searching for !
One question from a new bie : do you mind explaining how can I modify my project to include that effect? I didn’t understand why you use a navigationController and wich class I have to manipulate, suppose that I have a class containing a UiTableView with a custom UiTableViewCell.
Thanks a lot and glad to “discover” your nice blog.
Any one did implement sending Email from these buttons using MFMailComposeViewController ? I get crash when I tried this.
great post, keep your work…^^
Amazing post, impressive code, beautiful blog design.
Respect!
Hey, great post! My app required a pan gesture recognizer, so this only helped me about halfway, but I documented my progress at https://github.com/spilliams/sparrowlike
[…] tableView在向自身发送setEditing:animated:消息的前后,会向其delegate分别发送tableView:willBeginEditingRowAtIndexPath: ,tableView:didEndEditingRowAtIndexPath:消息。在这些方法中可相应更新tableView的显示。How does the Twitter iPhone app implement side swiping on a table?中通过实现tableView:willBeginEditingRowAtIndexPath:方法使得用户在tableView的行上swipe时可滑出菜单。 […]
Hi, may i just ask what if you have two table view in one single xib. How will you manipulate the code? I want to implement side swipe on both table view and im having a hard time to make the other one function. Thank you so much.
by the way. Great post. it’s really helpful
This is the best implementation of this that i’ve seen, however i’m not sure how to extend it. The Outlet for the tableview is set in the parent class and not the subclass so how would I use this with multiple tableviews?
i have successfully implemented the swipe function but when i click on the cell, the 2nd cell with the button over laps with the original cell. it has double layer. How to make sure the 2nd cell always hidden behind the 1st cell when we click on the first cell. It will always hide behind the 1st cell unless the user swipe the cell.
Yes, I noticed this with all the other swipe cell implementations on github etc.
If you enable the PUSH_STYLE_ANIMATION #define then you should get your desired behaviour. I think this is because the sideSwipeView’s frame coordinates will actually be ‘offscreen’ rather than behind the cell until the user swipes.
Great work! How is this project licensed? I’d love to use it in my project!
I have the setupSideSwipeView method working in my project, but the buttons aren’t displaying and neither is the touchUpInsideAction: method.
any idea why this might be?
thanks for any help
Same issue for me !
We can’t see the buttons
Thank you for any help
[…] tableView在向自身发送setEditing:animated:消息的前后,会向其delegate分别发送tableView:willBeginEditingRowAtIndexPath: ,tableView:didEndEditingRowAtIndexPath:消息。在这些方法中可相应更新tableView的显示。How does the Twitter iPhone app implement side swiping on a table?中通过实现tableView:willBeginEditingRowAtIndexPath:方法使得用户在tableView的行上swipe时可滑出菜单。 […]