danh
danh

Reputation: 62686

Resize a UITableView on scroll

I've got a VC with a table view. When an event occurs, I want to drop in a UIView from the top of the main view. When the user scrolls the table view, I want to re-layout the view so that the dropped in view "scrolls away". I figured I'd do this by moving the upperView's frame and resizing the table view (both in relation to the scroll offset). I've got it almost working as follows:

- (void)scrollViewDidScroll:(UIScrollView *)scrollView {

    CGFloat contentOffsetY = scrollView.contentOffset.y;

    if (contentOffsetY > 0) {
        CGFloat upperHeight = self.upperView.frame.size.height;
        CGFloat fullTableHeight = self.view.frame.size.height;

        CGFloat offsetY = (contentOffsetY < upperHeight)? -scrollView.contentOffset.y : -upperHeight;

        self.upperView.frame = CGRectMake(0, offsetY, 320, upperHeight);
        scrollView.frame = CGRectMake(0, upperHeight+offsetY, 320, fullTableHeight-(upperHeight+offsetY));
    }
    NSLog(@"%f", self.upperView.frame.origin.y);
}

The upper view origin starts at 0,0.

The problem is, after a little dragging back and forth, I lose the top few pixels of that upper view. It can't seem to get it's origin y back to zero. The logging reads negative values, and only gets to -1, with the most careful dragging. Has anybody done something like this? Much obliged if you can help.

Upvotes: 0

Views: 2125

Answers (2)

danh
danh

Reputation: 62686

Figured this out: The UITableView doesn't thoroughly message didScroll during the bounce. This is why I was missing a few pixels. Resizing during the bounce makes the bounce get mixed up and stop. This fix on my code above allows the bounce to work (by moving, not resizing the table) and makes sure the upper view is correctly placed during the bounce (when contentOffset.y <= 0).

- (void)scrollViewDidScroll:(UIScrollView *)scrollView {

    CGFloat contentOffsetY = scrollView.contentOffset.y;

    CGFloat upperHeight = self.upperView.frame.size.height;
    CGFloat fullTableHeight = self.view.frame.size.height;

    CGFloat offsetY = (contentOffsetY < upperHeight)? -scrollView.contentOffset.y : -upperHeight;

    if (contentOffsetY > 0) {
        self.upperView.frame = CGRectMake(0, offsetY, 320, upperHeight);
        scrollView.frame = CGRectMake(0, upperHeight+offsetY, 320, fullTableHeight-(upperHeight+offsetY));
    } else {
        self.upperView.frame = CGRectMake(0, 0, 320, upperHeight);
        scrollView.frame = CGRectMake(0.0, upperHeight, 320, scrollView.frame.size.height);
    }
    [super scrollViewDidScroll:scrollView];
}

Upvotes: 0

rob mayoff
rob mayoff

Reputation: 386018

It sounds like you always scroll the table view to the top when you show the drop-in view. Assuming that's the case, there is a better way to do this.

UITableView inherits the contentInset property from UIScrollView. The contentInset property defines a border on each edge of the scroll view. Each border has its own thickness, which is zero by default. These borders just affect how far the scroll view is willing to let the user scroll the content - they don't hide the content! If you set the top inset larger than zero, and give the scroll view a subview with a negative Y origin, that subview can be visible in the border, and will scroll with the rest of the scroll view's content.

So we'll set the table view's top inset to the height of the drop-in view, and add the drop-in view as a subview of the table view with its origin set to the negative of its height. This will make it fit perfectly on the screen above the first row of the table view, and it will scroll with the table view. When we detect that the drop-in view has been scrolled fully off-screen, we can just remove it from the table view and set the table view's top inset back to zero.

We'll need an instance variable that tracks the current state of the drop-in view:

typedef enum {
    DropInViewStateHidden,
    DropInViewStateAppearing,
    DropInViewStateVisible
} DropInViewState;

@implementation ViewController {
    DropInViewState _dropInViewState;
}

In my test project, I just used a button to trigger the drop-in view. Here's the action:

- (IBAction)dropIn {
    if (_dropInViewState != DropInViewStateHidden)
        return;

    CGRect frame = self.dropInView.frame;
    frame.origin.y = -frame.size.height;
    self.dropInView.frame = frame;
    [self.tableView addSubview:self.dropInView];

    self.tableView.contentInset = UIEdgeInsetsMake(frame.size.height, 0, 0, 0);
    [self.tableView setContentOffset:frame.origin animated:YES];

    _dropInViewState = DropInViewStateAppearing;
}

When the table view scrolls, we check the state of the drop-in view. If it is in the “visible” state and has been scrolled off-screen, we hide it. There's a tricky bit because when we make the drop-in view visible, and scroll it onto the screen, we can receive scrollViewDidScroll: messages that would make us think the drop-in view has been hidden. That's why we start out in the DropInViewStateAppearing state, and transition to the DropInViewVisible state when we know the view has appeared.

- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
    switch (_dropInViewState) {
        case DropInViewStateHidden:
            break;
        case DropInViewStateVisible:
            if (scrollView.contentOffset.y >= 0) {
                // dropInView has been scrolled off-screen
                self.tableView.contentInset = UIEdgeInsetsZero;
                [self.dropInView removeFromSuperview];
                _dropInViewState = DropInViewStateHidden;
                break;
            }
        case DropInViewStateAppearing:
            // When I first add dropInView to tableView and tell tableView
            // to scroll to reveal dropInView, I may get a bunch of
            // scrollViewDidScroll: messages with contentOffset.y >= 0.
            // I don't want those messages to hide dropInView, so I sit in
            // DropInViewStateAppearing until contentOffset.y goes negative,
            // which means at least part of dropInView is actually on-screen.
            if (scrollView.contentOffset.y < 0)
                _dropInViewState = DropInViewStateVisible;
            break;
    }
}

Upvotes: 1

Related Questions