Episode #107

Swipe to Reveal Cells

16 minutes
Published on February 13, 2014

This video is only available to subscribers. Get access to this video and 572 others.

In this episode I customize UITableViewCell to provide swipe to reveal behavior, similar to Mail.app. We use UIScrollView's delegate methods to ensure that we never land mid-way through the swipe, raise notifications to make sure only one cell is open at a time, and we use a help app called Reveal to assist us in visualizing the view hierarchy.

Episode Links

Creating our custom buttons

- (void)awakeFromNib {
    self.scrollView.showsHorizontalScrollIndicator = NO;
    self.scrollView.showsVerticalScrollIndicator = NO;

    self.moreButton = [UIButton buttonWithType:UIButtonTypeCustom];
    self.moreButton.titleLabel.font = [UIFont systemFontOfSize:14.0];
    self.moreButton.backgroundColor = [UIColor colorWithWhite:0.76 alpha:1.0];
    self.moreButton.frame = CGRectMake(0, 0, kRevealWidth / 2.0, self.contentView.frame.size.height);
    [self.moreButton setTitle:@"More..." forState:UIControlStateNormal];

    self.deleteButton = [UIButton buttonWithType:UIButtonTypeCustom];
    self.deleteButton.titleLabel.font = [UIFont systemFontOfSize:14.0];
    self.deleteButton.backgroundColor = [UIColor redColor];
    self.deleteButton.frame = CGRectMake(self.moreButton.frame.size.width, 0, kRevealWidth / 2.0, self.contentView.frame.size.height);
    [self.deleteButton setTitle:@"Delete" forState:UIControlStateNormal];

    self.buttonContainerView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, kRevealWidth, self.deleteButton.frame.size.height)];
    [self.buttonContainerView addSubview:self.moreButton];
    [self.buttonContainerView addSubview:self.deleteButton];

    [self.scrollView insertSubview:self.buttonContainerView
                       belowSubview:self.innerContentView];

    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(onOpen:)
                                                 name:RevealCellDidOpenNotification
                                               object:nil];
}

Next we need to reposition our buttons to the right side, accounting for scroll offset.

Positioning the buttons

- (void)layoutSubviews {
    [super layoutSubviews];

    self.contentView.frame = self.bounds;
    self.scrollView.contentSize = CGSizeMake(self.contentView.frame.size.width + kRevealWidth, self.scrollView.frame.size.height);

    [self repositionButtons];
}

- (void)repositionButtons {
    CGRect frame = self.buttonContainerView.frame;
    frame.origin.x = self.contentView.frame.size.width - kRevealWidth + self.scrollView.contentOffset.x;
    self.buttonContainerView.frame = frame;
}

Handling the scroll

- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
    // make sure the buttons are fixed in place
    [self repositionButtons];

    // don't allow scrolling to the right   
    if (scrollView.contentOffset.x < 0) {
        scrollView.contentOffset = CGPointZero;
    }

    // if we're open, we need to close the others
    if (scrollView.contentOffset.x >= kRevealWidth) {
        _isOpen = YES;
        [[NSNotificationCenter defaultCenter] postNotificationName:RevealCellDidOpenNotification
                                                            object:self];
    } else {
        _isOpen = NO;
    }
}

Handling the open notification

- (void)onOpen:(NSNotification *)notification {
    if (notification.object != self) {
        if (_isOpen) {
            dispatch_async(dispatch_get_main_queue(), ^{
               [UIView animateWithDuration:0.25
                                animations:^{
                                    self.scrollView.contentOffset = CGPointZero;
                                }];
            });
        }
    }
}

Snapping to open or closed

We don't want our scroll to land in between the open & closed states. We can do this by adjusting where the scroll will end when we finish dragging.

- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView
                     withVelocity:(CGPoint)velocity
              targetContentOffset:(inout CGPoint *)targetContentOffset {
    if (velocity.x > 0) {
        (*targetContentOffset).x = kRevealWidth;
    } else {
        (*targetContentOffset).x = 0;
    }
}