Episode #75

A Tale of UIScrollView Customization

16 minutes
Published on July 11, 2013

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

In this episode I attempt to implement a custom scroll view behavior, where a table view can grow while scrolling to eventually encompass the entire screen. The implementation, while mostly functional, has drawbacks and the code is complicated. After taking a break, I approach the problem anew, and implement it much cleaner.

Episode Links

Attempt 1 (Complicated)

@interface ViewController ()

@property (nonatomic, assign) CGRect originalTableFrame;
@property (nonatomic, assign) BOOL decelerating;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    [self loadScrollerImages];
    [self addTableHeader];
    self.originalTableFrame = self.tableView.frame;
}

- (void)addTableHeader {
    CGFloat y = self.filterView.frame.origin.y;
    CGFloat height = self.filterView.frame.size.height;

    self.tableView.tableHeaderView = self.filterView;
    CGRect tableFrame = self.tableView.frame;
    tableFrame.origin.y = y;
    tableFrame.size.height += height;
    self.tableView.frame = tableFrame;
}

- (void)loadScrollerImages {
    const int NumPages = 3;
    self.scrollView.contentSize = CGSizeMake(NumPages * self.view.frame.size.width, 200);
    self.scrollView.pagingEnabled = YES;
    for (int i = 0; i < NumPages; i++) {
        NSString *imageName = [NSString stringWithFormat:@"scroller-image-%d.jpg", i + 1];
        UIImage *image = [UIImage imageNamed:imageName];
        UIImageView *imageView = [[UIImageView alloc] initWithImage:image];

        CGRect frame = imageView.frame;
        frame.origin.x = i * self.view.frame.size.width;
        imageView.frame = frame;
        [self.scrollView addSubview:imageView];
    }
}


#pragma mark - UIScrollViewDelegate

-(void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset {
    NSLog(@"will end dragging, target: %@", NSStringFromCGPoint((*targetContentOffset)));
    self.decelerating = YES;
}

-(void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
    self.decelerating = NO;
}

- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
    CGPoint offset = scrollView.contentOffset;
    BOOL scrollingUp = offset.y > 0;
    BOOL adjustUp = (scrollingUp && self.tableView.frame.origin.y > 0);
    BOOL adjustDown = (!scrollingUp && self.tableView.frame.origin.y < self.scrollView.frame.size.height);
    CGFloat adjustment = fabsf(offset.y);

    if (self.decelerating) {
        // dampen the effect?
        adjustment = adjustment / 3.0f;
    }

    if (adjustUp) {
        // move table view up by offset & increase height
        CGRect frame = self.tableView.frame;
        frame.origin.y -= adjustment;
        frame.origin.y = MAX(0, frame.origin.y);
        frame.size.height += adjustment;
        frame.size.height = MIN(self.view.bounds.size.height, frame.size.height);
        self.tableView.frame = frame;

        // reset content offset
        self.tableView.contentOffset = CGPointZero;
    } else if (adjustDown) {
        // move table view down by offset & decrease height
        CGRect frame = self.tableView.frame;
        frame.origin.y += adjustment;
        frame.origin.y = MIN(self.originalTableFrame.origin.y, frame.origin.y);
        frame.size.height -= adjustment;
        frame.size.height = MAX(self.originalTableFrame.size.height, frame.size.height);
        self.tableView.frame = frame;

        // reset content offset
        self.tableView.contentOffset = CGPointZero;
    }
}
...
@end

This method, although it works, has some animation problems with deceleration. We could probably address this by changing the offset behavior when decelerating, but it gets even more complicated.

At this point, I decided to take a break. When I came back, I threw out that implementation and started over.

Attempt 2 (Simpler)

The first step was to wrap the entire thing in a single scroll view...

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    [self loadScrollerImages];
    [self addTableHeader];

    self.outerScrollView = [[UIScrollView alloc] initWithFrame:self.view.bounds];
    self.outerScrollView.delegate = self;
    self.outerScrollView.showsVerticalScrollIndicator = NO;

    // force the table to resize the content
    [self.tableView reloadData];

    CGRect tableFrame = self.tableView.frame;
    tableFrame.size.height = self.tableView.contentSize.height;
    self.tableView.frame = tableFrame;

    // add the views to the scroll view instead
    [self.outerScrollView addSubview:self.scrollView];
    [self.outerScrollView addSubview:self.tableView];

    // set the content size
    self.outerScrollView.contentSize = CGSizeMake(self.outerScrollView.frame.size.width,
                                                    self.scrollView.frame.size.height +
                                                    self.tableView.frame.size.height
                                                  );

    [self.view addSubview:self.outerScrollView];
}

Then we just need to fix the top scroll view when the user scrolls.

- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
    if (scrollView == self.outerScrollView) {
        CGRect frame = self.scrollView.frame;
        frame.origin.y = scrollView.contentOffset.y;
        self.scrollView.frame = frame;
    }
}

This implementation turned out to be a lot less code, and didn't have any of the deceleration problems of the previous attempt.

There is still a downside to this implementation, however, as pointed out by a colleague: we are effectively disabling cell reuse here for our table view, since its frame is as tall as the content. If this turned out to be a problem, then we might have to reconsider attempt 1.