Custom Edit Control for UITableViewCell

In one of my recent iOS project I was supposed to replace table view cell default edit control with a custom button to match the designs. I guess everyone knows that its pretty simple to change edit control’s color for selected state, we simply need to set cell’s tint color.

custom-edit-controls-for-uitableviewcell-01
custom-edit-controls-for-uitableviewcell-02

But there is no way to customise selected or default state of cell edit control with our own custom image because there is no public API to access this control. So, to customise this we have to write our own code. Steps below will guide you to achieve this.

Step 1: Hide Default Edit Control

First of all add logic to hide default edit control and to achieve this define below mentioned UITableView delegate in table view controller and return NO from the same. This will stop row from indenting towards right, so, now the default edit control won’t be visible to the user.

//  MyTableViewController.m

- (BOOL)tableView:(UITableView *)tableView shouldIndentWhileEditingRowAtIndexPath:(NSIndexPath *)indexPath {
    return NO;
}

 

Step 2: Subclass UITableViewCell Class To Override Cell Delegates And Add Custom Edit Control Action

To add custom edit control we need to subclass UITableViewCell. This will provide us the ability to add our own custom edit control in the cell view and will be able to override -(void)willTransitionToState:state delegate method (UITableViewCell delegate). This method is invoked when a cell moves through various editing states. Using this method we can play with cell UI. So in this method we will only add the logic to capture cell’s edit state when it is not in deleting state.

//  CustomTableViewCell.m

- (void)willTransitionToState:(UITableViewCellStateMask)state {
    if(state & UITableViewCellStateShowingDeleteConfirmationMask) {
        self.deleting = YES;
    } else {
        self.deleting = NO;
    }

    [super willTransitionToState:state];
}

 

Now create IBOutlets for mainView, customEditControl and leadingSpaceMainViewConstraint. Override -(void)setEditing:animated: method to add our own indentation with animation logic and -(void)setSelected:animated: to mark self.customEditControl as selected on cell selection.

//  CustomTableViewCell.m

static NSInteger const kCustomEditControlWidth=42;

@interface CustomTableViewCell ()

@property (nonatomic, getter=isPseudoEditing) BOOL pseudoEdit;
@property (nonatomic, getter=isDeleting) BOOL deleting;
@property (weak, nonatomic) IBOutlet UIView *mainView;
@property (weak, nonatomic) IBOutlet UIButton *customEditControl;
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *leadingSpaceMainViewConstraint;

- (void)setEditing:(BOOL)editing animated:(BOOL)animated {
    [super setEditing:editing animated:animated];

    [self beginEditMode];
}

- (void)setSelected:(BOOL)selected animated:(BOOL)animated {
    [super setSelected:selected animated:animated];

    self.customEditControl.selected = selected;
}

// Animate view to show/hide custom edit control/button
- (void)beginEditMode {
    self.leadingSpaceMainViewConstraint.constant = self.editing && !self.isDeleting ? 0 : -kCustomEditControlWidth;

    [UIView animateWithDuration:0.3 animations:^{
        [self.mainView.superview layoutIfNeeded];
    }];
}

 

custom-edit-controls-for-uitableviewcell-03

Step 3: Make Custom Edit Control Button Tappable

Now we have nice edit control that matches all the other buttons in the app and everything will look fine. But tapping over edit control button won’t do anything because we haven’t linked this button with an action yet.

To make it work first lets add an IBAction for the same button in UITableViewCell subclass and define a protocol say CustomTableViewCellDelegate with -(void)selectCell: method and make tableView controller follow the same protocol. You must be wondering why we need this protocol at all instead we could have directly called setSelected: method on cell object, but it won’t trigger delegate methods. So we need this protocol and call selectRowAtIndexPath:animated:scrollPosition:/deselectRowAtIndexPath:animated: methods on table view to select/deselect a table view cell in -(void)selectCell: protocol method. Calling this method will take care of execution of delegate methods.

//  CustomTableViewCell.m

- (IBAction)customEditControlPressed:(id)sender {
    //[self setSelected:!self.selected animated:NO];
    [self.delegate selectCell:self];
}

 

//  CustomTableViewCell.h

@protocol CustomTableViewCellDelegate

@optional

@property (nonatomic, readonly, getter=isPseudoEditing) BOOL pseudoEdit;
- (void)selectCell:(CustomTableViewCell *)cell;

@end

 

Calling selectRowAtIndexPath:animated:scrollPosition:/deselectRowAtIndexPath:animated: methods will not trigger call to corresponding delegate methods (-tableView:willSelectRowAtIndexPath: or tableView:didSelectRowAtIndexPath:), nor will it send out a notification. So we need to call these methods manually.

//  MyTableViewController.m
 
@interface MyTableViewController ()
 
- (void)selectCell:(CustomTableViewCell *)cell {
    NSIndexPath *indexPath =  [self.tableView indexPathForCell:cell];
    UITableView *tableView = self.tableView;
 
    if (!!cell.selected) {
        if ([tableView.delegate respondsToSelector:@selector(tableView:willDeselectRowAtIndexPath:)]) {
            [tableView.delegate tableView:tableView willDeselectRowAtIndexPath:indexPath];
        }

        [tableView deselectRowAtIndexPath:indexPath animated:NO];

        if ([tableView.delegate respondsToSelector:@selector(tableView:didDeselectRowAtIndexPath:)]) {
            [tableView.delegate tableView:tableView didDeselectRowAtIndexPath:indexPath];
        }
 
    } else {
        if ([tableView.delegate respondsToSelector:@selector(tableView:willSelectRowAtIndexPath:)]) {
            [tableView.delegate tableView:tableView willSelectRowAtIndexPath:indexPath];
        }

        [tableView selectRowAtIndexPath:indexPath animated:NO scrollPosition:UITableViewScrollPositionNone];
 
        if ([tableView.delegate respondsToSelector:@selector(tableView:didSelectRowAtIndexPath:)]) {
            [tableView.delegate tableView:tableView didSelectRowAtIndexPath:indexPath];
        }
    }
}

 

Don’t forgot to assign tableView controller as a delegate to the cell.

//  MyTableViewController.m
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    ...
    cell.delegate = self;
    ...
    return cell;
}

You are done here.

custom-edit-controls-for-uitableviewcell-04

But, same won’t work for iOS 8 because of a bug in the SDK. In iOS 8 -(BOOL)tableView:shouldIndentWhileEditingRowAtIndexPath: method stops row from indenting towards right, but the edit control would still be visible.

custom-edit-controls-for-uitableviewcell-06
custom-edit-controls-for-uitableviewcell-05

So as a workaround we need to stop the cell to go in edit mode while table is in edit mode. To achieve this we need to do following changes in our code.

Override -(void)setEditing:animated: in table view controller to set a flag named pseudoEdit. We will use same in UITableViewCell subclass to restrict cell to enter in edit mode.

//  MyTableViewController.m

@property (nonatomic, getter=isPseudoEditing) BOOL pseudoEdit;

- (void)setEditing:(BOOL)editing animated:(BOOL)animated {
    // Move this assignment to the method/action that
    // handles table editing for bulk operation.
    self.pseudoEdit = YES;

    [super setEditing:editing animated:animated];
}

Update previously defined protocol to include a @property named pseudoEdit. This will help us in getting pseudoEdit flag’s status in UITableViewCell subclass.

//  CustomTableViewCell.h
@protocol CustomTableViewCellDelegate
...
@optional
@property (nonatomic, readonly, getter=isPseudoEditing) BOOL pseudoEdit;
...
@end

The code below will restrict the cell from entering the default edit mode.

//  CustomTableViewCell.m

@property (nonatomic, getter=isPseudoEditing) BOOL pseudoEdit;

- (void)setEditing:(BOOL)editing animated:(BOOL)animated {
    if ([self.delegate isPseudoEditing]) {
        self.pseudoEdit = editing;
        [self beginEditMode];
    } else {
        [super setEditing:editing animated:animated];
    }
}

 

-(void)willTransitionToState: method is no more needed. Replace self.editing & !self.isDeleting condition with self.isPseudoEditing in -(void)beginEditMode method above

self.leadingSpaceMainViewConstraint.constant = self.isPseudoEditing ? 0 : -kCustomEditControlWidth;

 

Voilà ! We are done now.

You can download Sample Code from here

Leave a Reply

Your email address will not be published. Required fields are marked *