Swipe UITableView onto screen, keep swiping?

2019-03-22 05:49发布

I want a tableview that starts offscreen and can scroll on-screen, reach the top, and keep scrolling. I've made a visual of the desired interaction below.

I've tried two things, and neither work exactly like I need.

The first thing I did was put the tableview in a scrollview, and move the scrollview when panning is detected on the tableview. This blocks touches in the tableview, and even if I could detect when the tableview hit the top of the screen, I'm not sure how I would continue scrolling.

The second thing I tried was to set the content size of the scrollview to the height of the tableview. This lets the tableview scroll, but I only seem to be able to receive touches in the initial small rectangle labeled "List Item 1". As the tableview scrolls, I can't grab the middle and scroll it anymore.

What's the best way to build this interaction? Edit: A map surrounds this bottom view to the left, right, and mostly top. When the bottom view is pulled up, the map is visible to the left and right.

1.)
1

2.)
2

3.) (and this keeps scrolling for as many items are as in the list.)
3

3条回答
Lonely孤独者°
2楼-- · 2019-03-22 06:22

Why not change this completely. You said you have a map "underneath" the tableview. So when scrolled up the map will be hidden over by the table view. I presume when you scroll down again the map will be revealed?

You should be able to do this by using the UITableView header. Either a section header or a table view header. They behave slightly differently upon scrolling.

I'd maybe do it this way...

Use a table view header on the table. In this header you place your map view.

By default this will be pinned to the top of the table so if you scroll the table up then the map will slide off the top of the screen with it.

However, if you then intercept the scroll view delegate method - (void)scrollViewDidScroll:(UIScrollView *)scrollView; then you can work out if the table is being scrolled upwards and offset the map view so it stays where it is.

i.e. if the table is scrolled to (0, 10) then offset the map to (0, -10) so it looks like it hasn't moved.

This will give you the scroll in and out feature of the tableview and keep the map in view and responding to touches.

查看更多
孤傲高冷的网名
3楼-- · 2019-03-22 06:26

I guess you want something like this:

small list

or this:

big list

I laid out my table view over my map view. I set the table view's contentInset and contentOffset like this:

- (void)viewDidLoad {
    [super viewDidLoad];
    self.tableView.rowHeight = 44;
}

- (void)viewDidLayoutSubviews {
    [super viewDidLayoutSubviews];
    self.tableView.contentInset = (UIEdgeInsets){ .top = self.view.bounds.size.height - self.tableView.rowHeight };
    self.tableView.contentOffset = CGPointMake(0, -self.tableView.contentInset.top);
}

Note that, although the default row height is 44, tableView.rowHeight return -1 unless you explicitly set it. (Setting it to 44 in the storyboard doesn't change this.)

I used a subclass of UITableView in which I did two things:

  1. I explicitly set self.backgroundColor = [UIColor clearColor]. I found that setting the background color to clear in the storyboard didn't work.

  2. I overrode pointInside:withEvent::

    - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
        return point.y >= 0 && [super pointInside:point withEvent:event];
    }
    

Note that you don't care about contentInset here. The table view's contentOffset.y (which is the same as its bounds.origin.y) is set to a negative number when its top content inset is exposed. It's set to 0 when the top of item 0 is at the top edge of the table view, which isn't the case when the item as at the bottom edge of the screen.

Another thing you might want is to prevent the table from stopping half-on the screen. If the user drags item 0 halfway up the screen, you want the table to scroll so item 0 is all the way at the top of the screen (if there are sufficient items), and if the user drags item 0 halfway down the screen, you want the table to scroll so just item 0 is showing.

I did that by making my view controller act as the table view's delegate and implementing this delegate method, inherited from UIScrollViewDelegate:

- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset {
    CGFloat yMin = -self.tableView.contentInset.top;
    CGFloat yMax = MIN(0, self.tableView.contentSize.height - self.tableView.bounds.size.height);
    if (targetContentOffset->y < yMax) {
        if (velocity.y < 0) {
            targetContentOffset->y = yMin;
        } else {
            targetContentOffset->y = yMax;
        }
    }
}

That method is carefully written so that it works for tables too short to fill the screen vertically, and for tables that can fill the screen vertically.

I've uploaded my test project here: https://github.com/mayoff/tableView-over-mapview

Update for side-by-side tables

I don't think side-by-side tables is going to be a good user interface. I think it's going to be confusing. But here's how you do it.

The view hierarchy looks like this:

  • Root view
    • MKMapView
    • MyScrollView
      • ScrollContentView
        • MyTableView for first table
        • MyTableView for second table
        • MyTableView for third table
        • etc.

The map view and the scroll view have the same frames. The scroll view handles the sideways scrolling and each table view is independently scrollable vertically.

Since the scroll view should only capture touches that land in one of the table views, it needs a custom hitTest:withEvent: that returns nil for touches outside any of the table views:

@implementation MyScrollView

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    UIView *hitView = [super hitTest:point withEvent:event];
    return hitView == self ? nil : hitView;
}

@end

But this won't actually do the job, because (in my implementation) the scroll view has just one big subview, the ScrollContentView. So we need to do the same thing in ScrollContentView:

@implementation ScrollContentView

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    UIView *hitView = [super hitTest:point withEvent:event];
    return hitView == self ? nil : hitView;
}

That's sufficient to pass touches down to the map view if they land outside of the tables.

I also use ScrollContentView to lay out the tables and set the scroll view's content size:

- (void)layoutSubviews {
    [super layoutSubviews];

    // Layout of subviews horizontally:
    // [gutter/2][gutter][subview][gutter][subview][gutter][subview][gutter][gutter/2]
    // where 3 * gutter + subview = width of superview

    CGSize superSize = self.superview.bounds.size;
    CGFloat x = kGutterWidth * 3 / 2;
    CGFloat subWidth = superSize.width - kGutterWidth * 3;

    for (UITableView *subview in self.subviews) {
        subview.frame = CGRectMake(x, 0, subWidth, superSize.height);
        x += subWidth + kGutterWidth;

        CGFloat topInset = superSize.height - subview.rowHeight;
        subview.contentInset = (UIEdgeInsets){ .top = topInset };
        subview.contentOffset = CGPointMake(0, -topInset);
    }

    x += kGutterWidth / 2;
    self.frame = CGRectMake(0, 0, x, superSize.height);
    ((UIScrollView *)self.superview).contentSize = self.bounds.size;

    _pageWidth = subWidth + kGutterWidth;
}

I also made my view controller be the scroll view's delegate, and implemented a delegate method to force the scroll view to stop on “page” (table) boundaries:

- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset {
    CGFloat pageWidth = contentView.pageWidth;

    // Force scroll view to stop on a page boundary.
    CGFloat pageNumber = targetContentOffset->x / pageWidth;
    if (velocity.x < 0) {
        pageNumber = floor(pageNumber);
    } else {
        pageNumber = ceil(pageNumber);
    }
    pageNumber = MAX(0, MIN(pageNumber, contentView.subviews.count - 1));
    targetContentOffset->x = pageNumber * pageWidth;
}

The result:

multiple tables side-by-side

I've updated the git repository with this version.

查看更多
Animai°情兽
4楼-- · 2019-03-22 06:36

You ought to be able to do this pretty easily by setting your table view’s top contentInset to something high (as sha suggested in the comments) and then making your UITableView a subclass so you can override -pointInside:withEvent:. Using that and the current contentOffset, you can determine whether the incoming event is inside the area you want to be scrollable, and return YES or NO accordingly; if you return NO, then the touch should fall through to the map view as intended.

查看更多
登录 后发表回答