How to Make a Gesture-Driven To-Do List App Like Clear: Part 2/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 second in a three-part tutorial series that takes you through developing a to-do list app that is completely free […] By Colin Eberhardt.

Leave a rating/review
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

Deleting Items... Again

In order to reinstate the toDoItemDeleted: method that was added at the start of this article (you did comment it out instead of deleting it, didn’t you?), the table needs to be extended to expose a visible cells method and a mechanism for re-rendering all the cells.

Open SHCTableView.h and add the following method prototypes:

// an array of cells that are currently visible, sorted from top to bottom.
-(NSArray*)visibleCells;

// forces the table to dispose of all the cells and re-build the table.
-(void)reloadData;

Now add the method implementations to SHCTableView.m:

-(NSArray*) visibleCells {
    NSMutableArray* cells = [[NSMutableArray alloc] init];
    for (UIView* subView in [self cellSubviews]) {
        [cells addObject:subView];
    }
    NSArray* sortedCells = [cells sortedArrayUsingComparator:^NSComparisonResult(id obj1, id obj2) {
        UIView* view1 = (UIView*)obj1;
        UIView* view2 = (UIView*)obj2;
        float result = view2.frame.origin.y - view1.frame.origin.y;
        if (result > 0.0) {
            return NSOrderedAscending;
        } else if (result < 0.0){
            return NSOrderedDescending;
        } else {
            return NSOrderedSame;
        }
    }];
    return sortedCells;
}

-(void)reloadData {
    // remove all subviews
    [[self cellSubviews] makeObjectsPerformSelector:@selector(removeFromSuperview)];
    [self refreshView];
}

The implementation for visibleCells is nice and simple, making use of the cellSubViews method discussed earlier that returns the children of the scroll view. The method sorts the subviews in order of appearance before returning them.

reloadData is even simpler – all of the cells are removed from their parent (i.e., the scroll view) and then refreshView is invoked.

One important thing to note is that the above code does not recycle the cells. This allows the delete method to make radical changes to the cells that are under the ownership of our table, without worrying about glitches that might occur afterwards. In your case, reloadData really does throw everything away and start again, from scratch.

Uncomment toDoItemDeleted: in SHCViewController.m, build and run, and now, enjoy a glitch-free delete experience!

Editing Items

Currently the to-do items are rendered using a UILabel subclass – UIStrikethroughLabel. In order to make the items editable, you need to switch to UITextFields instead.

Fortunately, this is a very easy change to make. Simply edit SHCStrikethroughLabel.h and change the superclass from UILabel to UITextField:

@interface SHCStrikethroughLabel : UITextField

Unfortunately, UITextField is a little dumb, and hitting Return (or Enter) does not close the keyboard. So you have to do a bit more work here if you don't want to be stuck with a keyboard over half of your nice, snazzy UI. :]

Switch to SHCTableViewCell.h and change the @interface line as follows:

@interface SHCTableViewCell : UITableViewCell <UITextFieldDelegate>

Since SHCTableViewCell contains the UIStrikethroughLabel instance, you set it to support the UITextFieldDelegate protocol so that the table cell knows when Return is tapped on the keyboard. (Because UIStrikethroughLabel is now a UITextField subclass, it contains a delegate property that expects a class that supports UITextFieldDelegate.)

Next, open SHCTableViewCell.m and add the following code to initWithStyle:reuseIdentifier:, right after the creation of the SHCStrikethroughLabel instance:

_label.delegate = self;
_label.contentVerticalAlignment = UIControlContentVerticalAlignmentCenter;

The above sets up the label's delegate to be the SHCTableViewCell instance. It also sets the control to center vertically within the cell. If you omit the second line, you'll notice that the text now displays aligned to the top of each row. That just doesn't look right. :]

Now all you need to do is implement the relevant UITextFieldDelegate methods. Add the following code:

#pragma mark - UITextFieldDelegate
-(BOOL)textFieldShouldReturn:(UITextField *)textField {
    // close the keyboard on enter
    [textField resignFirstResponder];
    return NO;
}

-(BOOL)textFieldShouldBeginEditing:(UITextField *)textField {
    // disable editing of completed to-do items
    return !self.todoItem.completed;
}

-(void)textFieldDidEndEditing:(UITextField *)textField {
    // set the model object state when an edit has complete
    self.todoItem.text = textField.text;
}

The above code is pretty self-explanatory, since all it does is close the keyboard when Enter is tapped, not allow the cell to be edited if the item has already been completed, and set the to-do item text once the editing completes.

Build, run, and enjoy the editing experience!

After a little bit of testing, you will probably notice one small issue. If you edit an item that is in the bottom half of the screen (or just less than half for you lucky iPhone 5 owners!), when the keyboard appears, it covers the item you are editing.

This does not lead to a good user experience. There’s a simple fix: scrolling the item to the top of the page when editing starts.

The edit lifecycle is currently only visible to the SHCTableViewCell. You’ll need to expose this via the protocol so that you can add some extra behavior.

Open SHCTableViewCellDelegate.h and add the following line below the #import line:

@class SHCTableViewCell;

Next, add a couple of editing lifecycle method definitions:

// Indicates that the edit process has begun for the given cell
-(void)cellDidBeginEditing:(SHCTableViewCell*)cell;

// Indicates that the edit process has committed for the given cell
-(void)cellDidEndEditing:(SHCTableViewCell*)cell;

These protocol methods are simply invoked when the relevant UITextFieldDelegate method is invoked in SHCTableViewCell.m. Add the following to the file:

-(void)textFieldDidEndEditing:(UITextField *)textField {
    [self.delegate cellDidEndEditing:self];
    self.todoItem.text = textField.text;
}

-(void)textFieldDidBeginEditing:(UITextField *)textField {
    [self.delegate cellDidBeginEditing:self];
}

Note: textFieldDidEndEditing: is already implemented, so you should replace the method with the new one. textFieldDidBeginEditing: is a new method that needs to be added to the class.

Note: textFieldDidEndEditing: is already implemented, so you should replace the method with the new one. textFieldDidBeginEditing: is a new method that needs to be added to the class.

Since SHCViewController is already set as the delegate for each cell, you don't have to set the delegate, but you do need to add a new instance variable to SHCViewController.m, as follows:

    // the offset applied to cells when entering “edit mode”
    float _editingOffset;

Next, add implementations for the new delegate methods:

-(void)cellDidBeginEditing:(SHCTableViewCell *)editingCell {
    _editingOffset = _tableView.scrollView.contentOffset.y - editingCell.frame.origin.y;
    for(SHCTableViewCell* cell in [_tableView visibleCells]) {
        [UIView animateWithDuration:0.3
                         animations:^{
                             cell.frame = CGRectOffset(cell.frame, 0, _editingOffset);
                             if (cell != editingCell) {
                                 cell.alpha = 0.3;
                             }
                         }];
    }
}

-(void)cellDidEndEditing:(SHCTableViewCell *)editingCell {
    for(SHCTableViewCell* cell in [_tableView visibleCells]) {
        [UIView animateWithDuration:0.3
                         animations:^{
                             cell.frame = CGRectOffset(cell.frame, 0, -_editingOffset);
                             if (cell != editingCell)
                             {
                                 cell.alpha = 1.0;
                             }
                         }];
    }
}

The above code animates the frame of every cell in the list in order to push the cell being edited to the top. The alpha is also reduced for all the cells other than the one being editing.

The above also requires that the table view exposes its scroll view, so add a read-only property as follows to SHCTableView.h:

// the UIScrollView that hosts the table contents
@property (nonatomic, assign, readonly) UIScrollView* scrollView;

Also add the following getter to SHCTableView.m so that the existing _scrollView instance variable is returned when the scrollView property is accessed.

-(UIScrollView *)scrollView {
    return _scrollView;
}

Build, run, and rejoice!

As a user starts editing items, they are gracefully animated to the top of the screen. When the user hits Enter, the items gracefully slide back into place.

Colin Eberhardt

Contributors

Colin Eberhardt

Author

Over 300 content creators. Join our team.