Swift UIScrollView not working with buttons [duplicate] - ios

I have a horizontal scroll view with a line of buttons. The scroll view will not scroll unless I do an extremely fast swipe.
If I set the buttons to userInteractionEnabled = NO, then the scrolling works as expected, but of course, then the buttons don't work at all.
This app worked fine in iOS 7 and before. It seems to be a iOS 8 "feature". I did notice that I can catch the button's drag event, but I don't know how to redirect it back to the scrollView.
I'm thinking I'll need to replace my buttons with UIViews and manage the events myself but I'd be grateful if someone has other ideas or solutions.

I found that in iOS 8, the UIScrollView's underlying UIPanGestureRecognizer is not respecting the UIScrollView's delaysContentTouches property. I consider this an iOS 8 bug. Here's my workaround:
theScrollView.panGestureRecognizer.delaysTouchesBegan = theScrollView.delaysContentTouches

I've had this problem with a UITableView which has custom UITableViewCells with UIButtons on it. I put this in my UITableview class.
- (BOOL)touchesShouldCancelInContentView:(UIView *)view {
return YES;
}
Solved my problem.
EDIT: Just to clearify, you can create a subclass of UIScrollview and add this to solve the problem.

This hack works for me:
UITapGestureRecognizer *nothingTap = [[UITapGestureRecognizer alloc] init];
nothingTap.delaysTouchesBegan = YES;
[_scrollView addGestureRecognizer:nothingTap];
credit: https://devforums.apple.com/thread/241467

I made a subclass of UIScrollView to fix this issue. You only need this method in it:
- (BOOL)touchesShouldBegin:(NSSet *)touches withEvent:(UIEvent *)event inContentView:(UIView *)view
{
UITouch *touch = [touches anyObject];
if(touch.phase == UITouchPhaseMoved)
{
return NO;
}
else
{
return [super touchesShouldBegin:touches withEvent:event inContentView:view];
}
}
Then just remember to set the class to your subclass in the storyboard if you're using one and you're good to go.

I have same issue and I solved it by creating a subclass of UIScrollView and set it's cancelContentTouches value to TRUE and its working fine.
- (BOOL)touchesShouldCancelInContentView:(UIView *)view {
return YES;
}
Hope it will work for you.

Related

ScrollsToTop feature is not working in iOS9 properly

I have a UIViewController having a UITableView with large data. I want to implement the feature of scrolltotop on press of the statusbar.
I have a lot of UIView set through my storyboard so I first try to set scrollsToTop = NO for all and then set the scrollsToTop = YES for the UITableView specifically in viewDidLoad.
The same settings/configuration works with iOS8 but not with iOS9.
I have applied below code to disable scroll for all subviews in start:
- (void)disableScrollsToTopPropertyOnAllSubviewsOf:(UIView *)view {
for (UIView *subview in view.subviews) {
if ([subview isKindOfClass:[UIScrollView class]]) {
((UIScrollView *)subview).scrollsToTop = NO;
}
[self disableScrollsToTopPropertyOnAllSubviewsOf:subview];
}
}
Try it using content offset
self.tableView.contentOffset = CGPointMake(0, 0 - self.tableView.contentInset.top);
Make sure you call it on viewDidAppear:
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
// Call scroll to top here
}
I hope this can help.
Fortunately, this started working flawlessly in iOS 10.0
You might have more than one UIScrollView in your view hierarchy, and you have to set only the one you want to scroll to top to Yes and the others scrollsToTop = No;.

ios 8 - buttons in horizontal scroll view intercepting pan event - scroll does not work

I have a horizontal scroll view with a line of buttons. The scroll view will not scroll unless I do an extremely fast swipe.
If I set the buttons to userInteractionEnabled = NO, then the scrolling works as expected, but of course, then the buttons don't work at all.
This app worked fine in iOS 7 and before. It seems to be a iOS 8 "feature". I did notice that I can catch the button's drag event, but I don't know how to redirect it back to the scrollView.
I'm thinking I'll need to replace my buttons with UIViews and manage the events myself but I'd be grateful if someone has other ideas or solutions.
I found that in iOS 8, the UIScrollView's underlying UIPanGestureRecognizer is not respecting the UIScrollView's delaysContentTouches property. I consider this an iOS 8 bug. Here's my workaround:
theScrollView.panGestureRecognizer.delaysTouchesBegan = theScrollView.delaysContentTouches
I've had this problem with a UITableView which has custom UITableViewCells with UIButtons on it. I put this in my UITableview class.
- (BOOL)touchesShouldCancelInContentView:(UIView *)view {
return YES;
}
Solved my problem.
EDIT: Just to clearify, you can create a subclass of UIScrollview and add this to solve the problem.
This hack works for me:
UITapGestureRecognizer *nothingTap = [[UITapGestureRecognizer alloc] init];
nothingTap.delaysTouchesBegan = YES;
[_scrollView addGestureRecognizer:nothingTap];
credit: https://devforums.apple.com/thread/241467
I made a subclass of UIScrollView to fix this issue. You only need this method in it:
- (BOOL)touchesShouldBegin:(NSSet *)touches withEvent:(UIEvent *)event inContentView:(UIView *)view
{
UITouch *touch = [touches anyObject];
if(touch.phase == UITouchPhaseMoved)
{
return NO;
}
else
{
return [super touchesShouldBegin:touches withEvent:event inContentView:view];
}
}
Then just remember to set the class to your subclass in the storyboard if you're using one and you're good to go.
I have same issue and I solved it by creating a subclass of UIScrollView and set it's cancelContentTouches value to TRUE and its working fine.
- (BOOL)touchesShouldCancelInContentView:(UIView *)view {
return YES;
}
Hope it will work for you.

Draggable UIView stops posting touchesBegan after being added to UIScrollView

In Xcode 5.1 I have created a simple test app for iPhone:
The structure is: scrollView -> contentView -> imageView -> image 1000 x 1000 on the top.
And on the bottom of the single view app I have seven draggable custom UIViews.
The dragging is implemented in Tile.m with touchesXXXX methods.
My problem is: once I add a draggable tile to the contentView in my ViewController.m file - I can not drag it anymore:
- (void) handleTileMoved:(NSNotification*)notification {
Tile* tile = (Tile*)notification.object;
//return;
if (tile.superview != _scrollView && CGRectIntersectsRect(tile.frame, _scrollView.frame)) {
[tile removeFromSuperview];
[_contentView addSubview:tile];
[_contentView bringSubviewToFront:tile];
}
}
The touchesBegan isn't called for the Tile anymore as if the scrollView would mask that event.
I've searched around and there was a suggestion to extend the UIScrollView class with the following method (in my custom GameBoard.m):
- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
UIView* result = [super hitTest:point withEvent:event];
NSLog(#"%s: %hhd", __PRETTY_FUNCTION__,
[result.superview isKindOfClass:[Tile class]]);
self.scrollEnabled = ![result.superview isKindOfClass:[Tile class]];
return result;
}
Unfortunately this doesn't help and prints 0 in debugger.
The problem is, partly, because user interactions are disabled on the content view. However, enabling user interactions disables scrolling as the view captures all touches. So here is the solution. Enable user interactions in storyboard, but subclass the content view like so:
#interface LNContentView : UIView
#end
#implementation LNContentView
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
UIView* result = [super hitTest:point withEvent:event];
return result == self ? nil : result;
}
#end
This way, hit test passes only if the accepting view is not self, the content view.
Here is my commit:
https://github.com/LeoNatan/ios-newbie
The reason Tile views don't get touches is that scroll view's pan gesture recogniser consumes the events. What you need is, attach a UIPanGestureRecongnizer to each of your tiles and configure them as follows:
UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(pan:)]; // handle drag in pan:method
[tile addGestureRecognizer:pan];
UIPanGestureRecognizer *scrollPan = self.scrollView.panGestureRecognizer;
[scrollPan requireGestureRecognizerToFail:pan];
Here you let scroll view's pan gesture recogniser know that you only wish scrolling to happen if none of the tiles are bing dragged.
I've checked the approach — it does work indeed. Regarding your code, you'll need to handle all touches in the gesture recogniser rather than Tile view because touch events may be consumed/delayed by hit-tested view's gesture recogniser before they reach the view itself. Please refer to UIGestureRecognizer documentation to learn more about the topic.
It looks as ir one of the views in the hierarchy is capturing the events.
Have a look at the section
The Responder Chain Follows a Specific Delivery Path
Of the Apple doc's here
Edit:
Sorry I was writing from memory. This is how i resolved a similar issue in an app of myself:
I use UITapGestureRecognizer in the view(s) that I want to detect the touch. Implement the following delegate method of the UITapGestureRecognizer:
- (void) touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event
The touches' set contains all the objects (views) that received the event.

Touch events on UITableView's backgroundView

I've got a UITableViewController, and the tableView has a view as backgroundView (even more, it's a MKMapView), set with
self.tableView.backgroundView = _mapView;
Currently the background view is showing on the screen (I also have a transparent table header to achieve this). I'm not able to make the mapView (tableView's backgroundView) respond to user interaction. My table header, which is above the map view, is a subclass of UIView with the following override:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
LogDebug(#"Clic en transparent view");
if (_transparent)
{
return nil;
}
return [super hitTest:point withEvent:event];
}
so it's supposed to pass through the events; I don't know what's wrong, and didn't find any solution in other similar questions.
I must keep the mapView as the tableView's background property, so please take this into account.
Any idea?
it's relatively late to answer the question. But I ran into this problem on my own project, and after some research I got the answer. First off, I didn't assign the view in the back to tableView.backgroundView. Instead, I made the tableView.backgroundColor transparent then put another view under the table view.
EDIT:
by "under the table" I mean the tableView and the anotherView are children to the same parent view and tableView.zPosition is greater than anotherView.zPosition.
Then all I did is override the tableView:
(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
and let the tableView avoid holding the events that are not in its cells.
Here's the code:
-(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
/// the height of the table itself.
float height = self.bounds.size.height + self.contentOffset.y;
/// the bounds of the table.
/// it's strange that the origin of the table view is actually the top-left of the table.
CGRect tableBounds = CGRectMake(0, 0, self.bounds.size.width, height);
return CGRectContainsPoint(tableBounds, point);
}
it works like a charm for me. hope it'll help any other people who run into this problem.
I guess that the touchEvents cant be passed to tableView's backgroundView.
So, what i did is ,
For headerView which is a subclass of UIVIEW, i added a class property as mapView.
In HeaderView.h
#property (nonatomic,assign) UIView *mapView;
And, in tableViewController
-(UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section{
HeaderView *view =[[TestView alloc] initWithFrame:CGRectMake(0, 0, 200, 100)];
view.mapView =tableView.backgroundView;
return view;
}
And override hitTest in HeaderView and return this mapView.
-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
return self.mapView;
}
Now the mapView receives that touchEvent and responds accordingly..Add extra code that you might interest.
Hope this helps.
Santhu answer almost worked for me, I passed hit test to mapView instead of just returning it, also I needed to made a point check because I wanted only header touch events passing through so there is my version of hit test on the HeaderView:
-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
if(point.y > self.frame.size.height){
return nil;
}
return [self.mapView hitTest:point withEvent:event];
}
Put this in a UITableView subclass, the downside is you can no longer tap cells but it hopefully is a pointer in the right direction.
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{}
FYI, this is an old post, and UITableView.backgroundView does receive touches now. So, if your content in your backgroundView isn't receiving touches it's due to some other issue.

UIScrollview delaysContentTouches issue

I have UIScrollView loaded with UIButtons and on UIButton action I have highlighted UIImage of each UIButton.
If I don't set delaysContentTouches as NO then highlighted UIImage of UIButton will not shown if I touch up UIButton very fast. After I set delaysContentTouches property as NO then only UIButton highlighted UIImage is shown.
Now after setting delaysContentTouches property as NO for UIScrollView. I can not scroll my UIScrollView by dragging on the UIButtons. Now how can I resolve this issue.
Please give me an advise.
Thanks in advance.
Here's what works for me. Subclass UIScrollView, and implement only this method:
- (BOOL)touchesShouldCancelInContentView:(UIView *)view {
return YES;
}
Then set delaysContentTouches = NO;
Voila! Works just like the home screen: Highlights buttons immediately, but still allows scrolling :)
I found that in iOS 8, the UIScrollView's underlying UIPanGestureRecognizer is not respecting the UIScrollView's delaysContentTouches property. I consider this an iOS 8 bug. Here's my workaround:
theScrollView.panGestureRecognizer.delaysTouchesBegan = theScrollView.delaysContentTouches
OK I have resolved by implementing below method :
- (BOOL)touchesShouldCancelInContentView:(UIView *)view
{
NSLog(#"touchesShouldCancelInContentView");
if ([view isKindOfClass:[UIButton class]])
return NO;
else
return YES;
}
Unable to find a satisfactory solution online so far (and it seems to be that Apple is ignoring the issue). Found a thread on Apple's developer forum with some suggestions in there that may help: UIScrollView: 'delaysContentTouches' ignored
I was able to use the workaround from this link. To summarize the workaround (I'm para-quoting here):
UIEvent objects contain a time stamp.
You can record the time stamp at the time of touchesBegan on your
embedded subview.
In touchesMoved of scrollView's subview, look at the time stamp and
location again.
If the touch has not moved very far and more than, say, 0.1 seconds
have passed, you can assume the user touched the subview and then
delayed movement.
In this case, the UIScrollView will have decided, independently, that
this is NOT a scrolling action even though it will never tell you
that.
So, you can have a local state variable to flag that this condition of
delayed movement occurred and process events received by the subview.
Here's my code:
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
// store the timestamp
_beginDragTimeStamp = event.timestamp;
// your embedded subview's touches begin code
}
-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event{
// compare and ignore drag if time passed between tap and drag is less than 0.5s
if(event.timestamp - _beginDragTimeStamp < 0.5) return;
// your drag code
}
I had same issue & same hierarchy of the views, With latest sdk , just use it :
Setting delaysContentTouches to NO for UIButton in the same UITableViewCell.
self.scrollview.delaysContentTouches = NO
Create a subclass of the UIScrollView (or UITableView, or UICollectionView, or any other UIScrollView subclass that you use).
Implement the below method:
- (BOOL)touchesShouldCancelInContentView:(UIView *)view {
if ([view isKindOfClass:UIButton.class]) {
return YES;
}
return [super touchesShouldCancelInContentView:view];
}
Set this subclass at xib/storyboard as a "Custom Class" class if you use the interface builder.
Unselect Delay Touch Down in a xib or set delaysContentTouches = NO in code.

Resources