How to Make a Gesture-Driven To-Do List App Like Clear: Part 3/3
This is a post by Tutorial Team Member Colin Eberhardt, CTO of ShinobiControls, creators of playful and powerful iOS controls. Check out their app, ShinobiPlay. You can find Colin on Google+ and Twitter This is the last in a three-part tutorial series that walks you through creating a simple to-do list app free of buttons, […] By Colin Eberhardt.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
How to Make a Gesture-Driven To-Do List App Like Clear: Part 3/3
40 mins
Time To Refactor
Part of me wants to steamroll ahead to the next gesture, pinch-to-add, which is going to be a lot of fun. But there is something about the current code that is bugging me.
In order to separate the pull-down code from the “core” table logic, which includes cell recycling, you have created a subclass and added the logic there. You could follow the same steps when the next gesture is added, subclassing SHCTableViewDragAddNew with a new class SHCTableViewDragAddNewAndPinchToAddNew, which is a bit verbose.
But it's not just the naming that is a problem. What if you wanted to remove the pull-to-add gesture? The code is now sandwiched within a class hierarchy, and as a result is hard to extract.
Image courtesy of the stock.xchng user dreamtwist
Another design principle that is often cited is Composition over Inheritance. Applying this principle here, the pull-to-add gesture should not be implemented as a subclass; rather, it should be an entirely separate class that collaborates with (i.e., is composed with) the table view in order to add the required behavior.
Let’s start making these changes and see where it leads you.
Open SHCTableViewDragAddNew.h and change the superclass to NSObject as follows:
@interface SHCTableViewDragAddNew : NSObject
The class still needs to be associated with the table view in some way, so switch to SHCTableViewDragAddNew.m and add an SHCTableView instance variable to the class:
// the table that this gesture is associated with
SHCTableView* _tableView;
You need to supply an instance of SHCTableView to this class. Open SHCTableViewDragAddNew.h and add the following initializer prototype:
-(id)initWithTableView:(SHCTableView *)tableView;
You will no longer be creating this class via Interface Builder, so you can safely remove initWithCoder: in SHCTableViewDragAddNew.m and replace it with your new initializer:
-(id)initWithTableView:(SHCTableView *)tableView {
self = [super init];
if (self) {
_placeholderCell = [[SHCTableViewCell alloc] init];
_placeholderCell.backgroundColor = [UIColor redColor];
_tableView = tableView;
}
return self;
}
At this point, you’ll probably notice that much of SHCTableViewDragAddNew does not compile. The previous code you added for the UIScrollViewDelegate methods makes references to self, assuming that this class is a SHCTableView subclass. You will need to change all of these self references to the newly added instance variable.
Here is the complete implementation of these delegate methods, just in case you get a bit lost:
-(void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
// this behaviour starts when a user pulls down while at the top of the table
_pullDownInProgress = scrollView.contentOffset.y <= 0.0f;
if (_pullDownInProgress) {
// add your placeholder
[_tableView insertSubview:_placeholderCell atIndex:0];
}
}
-(void)scrollViewDidScroll:(UIScrollView *)scrollView {
if (_pullDownInProgress && _tableView.scrollView.contentOffset.y <= 0.0f) {
// maintain the location of the placeholder
_placeholderCell.frame = CGRectMake(0, - _tableView.scrollView.contentOffset.y - SHC_ROW_HEIGHT,
_tableView.frame.size.width, SHC_ROW_HEIGHT);
_placeholderCell.label.text = -_tableView.scrollView.contentOffset.y > SHC_ROW_HEIGHT ?
@"Release to Add Item" : @"Pull to Add Item";
_placeholderCell.alpha = MIN(1.0f, - _tableView.scrollView.contentOffset.y / SHC_ROW_HEIGHT);
} else {
_pullDownInProgress = false;
}
}
-(void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
// check whether the user pulled down far enough
if (_pullDownInProgress && - _tableView.scrollView.contentOffset.y > SHC_ROW_HEIGHT) {
[_tableView.dataSource itemAdded];
}
_pullDownInProgress = false;
[_placeholderCell removeFromSuperview];
}
Next, you must change the view controller so that your UI has an instance of SHCTableView once again, and yes, this means undoing the previous steps. (Sorry!)
Select SHCViewController.xib, select the table and, using the Identity Inspector, change the custom class back to SHCTableView. Then switch to SHCViewController.h and change the type of the outlet back again as well:
@property (weak, nonatomic) IBOutlet SHCTableView *tableView;
Now you need some way to inform the SHCTableViewDragAddNew class of the scrolling behavior of the table.
The SHCTableView currently exposes its scroll view as a property, so you could assign SHCTableViewDragAddNew as the delegate. However, SHCTableView is already assigned to the scroll view delegate! It looks like you're stuck, doesn’t it? Fortunately not!
If you look at UITableViewDelegate, you’ll see that it also conforms to (i.e., extends) UIScrollViewDelegate. So you could employ a similar approach here. Your custom table currently does not have a delegate, so let's add one!
Open SHCTableView.h and add the following property:
@property (nonatomic, assign) id<UIScrollViewDelegate> delegate;
To make SHCTableViewDragAddNew use this delegate, open SHCTableViewDragAddNew.h and adopt the delegate protocol:
@interface SHCTableViewDragAddNew : NSObject <UIScrollViewDelegate>
Then set the delegate by opening SHCTableViewDragAddNew.m and adding the following to the end of initWithTableView: (within the if condition):
_tableView.delegate = self;
You need to redirect any messages sent to the scroll view delegate (i.e., the SHCTableView) to this delegate. The table already has an implementation of scrollViewDidScroll:, so you can easily forward that one to the new delegate.
Open SHCTableView.m and replace scrollViewDidScroll: with the following:
-(void)scrollViewDidScroll:(UIScrollView *)scrollView {
[self refreshView];
// forward the delegate method
if ([self.delegate respondsToSelector:@selector(scrollViewDidScroll:)]){
[self.delegate scrollViewDidScroll:scrollView];
}
}
Note: scrollViewDidScroll: is an optional protocol method, so the code first checks that the delegate has implemented that method before calling it.
Note: scrollViewDidScroll: is an optional protocol method, so the code first checks that the delegate has implemented that method before calling it.
You could go ahead and add a similar forwarding implementation for every single method in UIScrollViewDelegate, but that would be a repetitive and un-rewarding task (although if you want to practice your typing skills, be my guest!). A much more cunning approach is to make use of Objective-C message forwarding.
If the runtime fails to find a method that matches a message’s selector, there are various other ways you can handle the message. One possibility is to forward the message to another object. Using this technique, you can very easily give the table view delegate the opportunity to receive a message, before following the standard message routing.
Add the following methods to SHCTableView.m:
#pragma mark - UIScrollViewDelegate forwarding
-(BOOL)respondsToSelector:(SEL)aSelector {
if ([self.delegate respondsToSelector:aSelector]) {
return YES;
}
return [super respondsToSelector:aSelector];
}
-(id)forwardingTargetForSelector:(SEL)aSelector {
if ([self.delegate respondsToSelector:aSelector]) {
return self.delegate;
}
return [super forwardingTargetForSelector:aSelector];
}
forwardingTargetForSelector: is the method that performs the rerouting. When any UIScrollViewDelegate selectors are sent to the table view, it first checks whether the delegate responds to that selector, and if so, forwards the message. So, as an example, the scrollViewWillBeginDragging: selector will be rerouted to the delegate, which will be your SHCTableViewDragAddNew class.
This code is slightly confusing at first, but it is super powerful!
Image courtesy of the stock.xchng user storm110
Finally, you can reinstate the pull-to-add gesture by simply adding it as an instance variable within the app's view controller. Open SHCViewController.m and add the instance variable:
SHCTableViewDragAddNew* _dragAddNew;
Create an instance of this class at the end of viewDidLoad::
_dragAddNew = [[SHCTableViewDragAddNew alloc] initWithTableView:self.tableView];
I would say that this is a much more elegant solution. :] The support for this gesture can now be added or removed with a single line of code, with the table view inheritance structure totally unaffected. Furthermore, the interface for SHCTableViewDragAddNew is simplicity itself, with a single init method and nothing more.
What are you waiting for? Build and run your app to make sure that everything works just as before!