UIView on top of UIScrollview, pass through only scrolls, not taps - ios

I have a UITableview with a subclass of UIVIew on top of a section of the table, like so:
-uitable--
|--uiview-|
|| ||
|| ||
|---------|
| |
| |
-----------
The view has the same superview as the tableview, but covers the tableview partially. There are a few buttons on the UIView. I want the user to be able to scroll on the view and subsequently move the tableview (as if he were scrolling on the table). But, if he taps a button on the view, I want that tap to register on the view, not get sent down to the tableview. Right now, I am overriding the view's - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event method to always return NO, which works for the scrolling but has the effect of sending all touches down to the tableview, rendering my buttons ineffective. Is there any way for me to pass down swipe/pan gestures to the tableview but keep the tap gestures?
Thanks,

Instead of always returning NO from pointInside, check to see if the point intersects any of your UIButton subviews - and return YES if it does.
- (BOOL) pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
for ( UIView* sv in self.subviews )
{
if ( CGRectContainsPoint( sv.frame, point) )
return YES;
}
return NO;
}
EDIT: alternate solution
Per your comment you'd like the user to begin scrolling the UITableView with a touch-down on one of the subview buttons. To make this work you'll need to make the UIView that contains your buttons a subview of the UITableView.
By doing this the UIView will then begin to scroll along with the UITableViewCells. To prevent this you need to adjust the frame as scrolling happens, or possibly lock it in place using constraints.
- (void) viewDidLoad
{
[super viewDidLoad];
// instantiate your subview containing buttons. mine is coming from a nib.
UINib* n = [UINib nibWithNibName: #"TSView" bundle: nil];
NSArray* objs = [n instantiateWithOwner: nil options: nil];
_tsv = objs.firstObject;
// add it to the tableview:
[self.tableView addSubview: _tsv];
}
// this is a UIScrollView delegate method - but UITableView IS a UIScrollView...
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
[scrollView bringSubviewToFront:_tsv];
CGRect fixedFrame = _tsv.frame;
fixedFrame.origin.y = 100 + scrollView.contentOffset.y;
_tsv.frame = fixedFrame;
}

Related

Search Bar does not receive touches

I have a search bar implemented and it 'sticks' to the tableView. Now, after the tableView starts scrolling, the searchBar does not receive touches and become the first responder, instead, the cell behind it does and navigates to the next view. What can I do to make the searchBar receive touches?
Code for keeping it static:
-(void)scrollViewDidScroll:(UIScrollView *)scrollView {
UISearchBar *searchBar = self.search;
[[self.tableView.tableHeaderView superview] bringSubviewToFront:self.tableView.tableHeaderView];
CGRect searchBarFrame = searchBar.frame;
if (self.inSearchMode)
{
searchBarFrame.origin.y = 0;
}
else
{
searchBarFrame.origin.y = MAX(0, scrollView.contentOffset.y + scrollView.contentInset.top);
}
self.search.frame = searchBarFrame;
}
If you think about it, the table header frame will be outside the table bounds when you scroll.
Try to add it as a subview to the tableView directly and set its frame relative to tableView bounds to keep it sticky.

using scrollViewWillEndDragging to ensure UIScrollView stops at defined increments

I would like my UIScrollView to naturally glide to endings at certain incremental values, corresponding to every 50 points of width of a horizontally-scrolling UIScrollView To do this, I customed scrollViewWillEndDragging, like so (as recommended, but not described in detail in an answer here Scrolling a horizontal UIScrollView in stepped increments?):
- (void) scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset{
if(fmodf(targetContentOffset->x, 50.0)){
int roundingNumber1 = 50;
CGFloat newOffset = roundingNumber1 * floor(((scrollView.contentOffset.x)/roundingNumber1)+0.5);
targetContentOffset->x = newOffset;
}
}
However, I am not sure this is actually stopping the view at specific increments, and I also notice that the effect is asymmetric. Though my scrolling motions/velocity/etc are the same, scrolling right is much less fluid than scrolling left. Scrolling right stops faster and more abruptly. There's a video here. Why is this behavior asymmetric and how can I change it?
The reason I thin the scrolling is not stopping at increments of 50 is that I also have a UILabel underneath the scroll view that gets updated by other delegate functions to show the offset. The value it shows is rarely close to 50 when the scrolling is done. Is this because the scrolling is not incrementing to values of 50 or because I am not updating at the right times?
Thanks for any advice.
- (void) scrollViewDidScroll:(UIScrollView *)scrollView{
if(abs(self.lastOffset - scrollView.contentOffset.x) > 49){
CGFloat newNumber = scrollView.contentOffset.x;
self.numberProperty.text = [NSString stringWithFormat:#"%.00f", scrollView.contentOffset.x];
self.lastOffset = scrollView.contentOffset.x;
[self.view setNeedsDisplay];
}
}
- (void) scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView{
self.numberProperty.text = [NSString stringWithFormat:#"%.00f", scrollView.contentOffset.x];
}
Rather than implementing these delegate methods, you can set pagingEnabled on your scroll view to true to get this behavior for free. When paging is enabled, the scroll view will automatically snap to the nearest "page" when you end dragging, where the page width is equal to the scroll view's width.
Based on your video, it looks like the items in your scroll view are smaller than the width of the scroll view itself. To use paging, you'll have to do the following:
Make your scroll view have the same width as one of your items (50.0 units in your case).
Set scrollView.clipsToBounds to false so that the scroll view draws subviews outside of its much smaller bounds.
Set scrollView.pagingEnabled to true so that the scroll view scrolls with paging.
At this point, paging will work but you won't be able to drag the scroll view outside of its bounds. To make this work, you'll need to embed the scroll view in a larger view that forwards touch events to it.
Create a "touch forwarding" class and add it to your view.
This class takes all touch events it receives and sends them to its targetView property instead. DJK is a random prefix I made up for the class name.
#interface DJKTouchForwardingView : UIView
/** The view to which touch events should be forwarded. */
#property (weak, nonatomic) UIView *targetView;
#end
#implementation DJKTouchForwardingView
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
UIView *child = nil;
if ((child = [super hitTest:point withEvent:event]) == self) {
return self.targetView;
}
return child;
}
#end
Make the scroll view a subview of the touch forwarding view and assign it to the targetView property.
Your scroll view will now be scrollable within the bounds of the touch forwarding view and will snap to 50 unit pages.

How to animate views according to tableview scrolling directions without content scrolling

i'm trying to implement a specific tableview behaviour (Like on Facebook app).
I want to have a dynamic header that will be magnified every time the user scrolls up and will be shrieked when the user scroll down.
In addition i want the tableview to cause the effect of pushing the header and than scrolling the tableview cells.
I used the method:
- (void)scrollViewDidScroll:(UIScrollView *)aScrollView
in this method i calculated the offset and the direction and called a method that shrink or magnify the header accordingly
so far so good.
The thing is that the animation is being performed with the tableview scrolling.
To avoid it, I created a custom scrollview on to of the top of my tableview, I taged the two scrollviews differently.
In the scrollview i created a weak reference of the tableview and a boolean value that indicated if the scrollview should return the tableview touch.
When the shrinking\magnifying animation was finished i changed the boolean value so it will signal the custom scrollview to return the tableview in my HitTest methods that i implemented inside the scrollview.
But hitTest not called when the user keep scrolling (without leafing the finger), in additions now my buttons inside my tableViewCell aren't reacting.
Here is my HitTest Method:
- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
UIView* result = [super hitTest:point withEvent:event];
if (_recieveTouchOnTable)
{
return _table;
}
else
return result;
}
Here is my scrollViewDidScroll method:
(onProgress means that the animation is being performed, so keep returning the custom scrollview)
Tag = 2 = the custom scrollview
Tag = 1 = the tableview
- (void)scrollViewDidScroll:(UIScrollView *)aScrollView {
CGFloat yVelocity = [aScrollView.panGestureRecognizer velocityInView:aScrollView].y;
CGFloat offset = lastCustomScrollViewContentOffset.y-aScrollView.contentOffset.y;
lastCustomScrollViewContentOffset =aScrollView.contentOffset;
if (yVelocity<0)
offset = fabs(offset)*-1;
else if(yVelocity>0)
offset = fabs(offset);
if (offset!=0 && aScrollView.tag == 2)
[self layoutViewAccorrdingToTableviewScorlingVelocity:offset];
if (!onProgress ){
customScrollView.recieveTouchOnTable=YES;
}
}
Am i missing something, or maybe there's a more simple way to do it?

Nested UIScrollView with pagingEnabled on the encompassing one doesn't receive the first bottom swipe after page change

I'am facing a little problem with my App that has a bit complicated structure :
So first things first the views hirearchy :
|- UICollectionView (horizontal scrolling)
-> UICollectionViewCell
|-> UIScrollView A (PagingEnabled = YES, vertical scrolling only)
|-> UIImageView = page 1
|-> UIScrollView B (PagingEnabled = NO, vertical scrolling only) = page 2
|-> UIView (content...)
The problem is that when I scroll on UIScrollView A to make page 2 appear, the first scroll to the see the bottom on UIScrollView B is ignored, no scroll at all, but the second one has the right behaviour.
I think that it's a kind of "focusing" problem, but I can't figure out how to make this work properly.
Any clue on how to give this first touch on page 2 to the right UIScrollView ?
Have you tried hitTest:withEvent: method? This method helps you choose which view should receive the touch.
For example, you could subclass UIScrollView and set your UIScrollView A's class the new class you just created. Adding the code below to this custom UIScrollView class will let you deliver the touch to it's child view's first
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
for (UIView *subView in self.subviews) {
// Convert the point to the subView's coordinate system.
CGPoint pointForTargetView = [subView convertPoint:point fromView:self];
if (CGRectContainsPoint(subView.bounds, pointForTargetView)) {
// Calling subView's hitTest:withEvent get the right view
return [subView hitTest:pointForTargetView withEvent:event];
}
}
UIView *hitView = [super hitTest:point withEvent:event];
// // Uncomment if you want to make the view 'transparent' to touches:
// // meaning the parts of the view without any subview will deliver the touch
// // to the views 'behind' it
// if (hitView == self) {
// return nil;
// }
return hitView;
}
For more detailed info please check WWDC - Advanced Scrollviews and Touch Handling Techniques (around 15:00 is where this subject starts).
Also, thanks to Tim Arnold for the clue.

Swipe UITableView onto screen, keep swiping?

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.)
2.)
3.) (and this keeps scrolling for as many items are as in the list.)
I guess you want something like this:
or this:
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:
I explicitly set self.backgroundColor = [UIColor clearColor]. I found that setting the background color to clear in the storyboard didn't work.
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:
I've updated the git repository with this version.
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.
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.

Resources