I have many UIButtons within a UIScrollView. Those UIButtons have actions attached to them in Touch Down Repeat. My problem is my scroll view doesn't scroll when I touch a button then scroll, but it works fine if I touch outside of the button.
How can I allow my scroll view to scroll even though a button is pressed?
As long as you have the Cancellable Content Touches in Interface Builder set it should work. You can also set it in code:
scrollView.canCancelContentTouches = YES;
So view.canCancelContentTouches = YES works OK if you don't also have delaysContentTouches set to YES. If you do though, the buttons won't work at all. What you need to do is subclass the UIScrollView (or UICollectionView/UITableView) and implement the following:
Objective-C
- (BOOL)touchesShouldCancelInContentView:(UIView *)view {
if ([view isKindOfClass:UIButton.class]) {
return YES;
}
return [super touchesShouldCancelInContentView:view];
}
Swift 2
override func touchesShouldCancelInContentView(view: UIView) -> Bool {
if view is UIButton {
return true
}
return super.touchesShouldCancelInContentView(view)
}
Use a UITapGestureRecognizer with delaysTouchesBegan as a property set to true.
Related
I have a UIView in a UIScrollView and under the UIView there are some labels, and two buttons.
Everything works fine, except the buttons. They do not respond to touch.
Everything is done in Storyboard and the labels and buttons are all using autoresize
I have tried to add:
scrollView.canCancelContentTouches = true
scrollView.delaysContentTouches = true
To no effect.
The view hierarchy is as follows:
-View
--Scroll View
---View
----Label
----Label
----More Labels
----Button
----Button
Write your own class subclass UIScroll view and add below code
- (BOOL)touchesShouldCancelInContentView:(UIView *)view {
return NO;
}
or modify your code
scrollView.canCancelContentTouches = false
scrollView.delaysContentTouches = false
The view hierarchy is as follows:
-View
1.--Scroll
2---View . <--- Check this UIVIEW User interaction is Enable or disable
3----Label
4----Label
5---More Labels
6----Button
7----Button
I have a scrollview with some subviews as tiles. The scrollview has its "Delays content touches" and "Cancellable Content Touches" set to YES.
I capture touches in each subview with touchesBegan, touchesEnded and touchesMoved.
When you tap a button and almost immediatly start to scroll, the button highlights and the scrollview do not scroll, without any code needed.
When I do exactly the same thing without changing anything, touching the view but outside the button, these touch methods are triggered, but the scrollview scrolls.
What may I do in those touch methods to cancel the scrolling when a touch is done outside the button to have the same behaviour that prevent the scrollview to scroll ?
I solved this adding this code in touchesBegan and touchesEnded when touch is catched by the subview.
UIView* superView = self.view.superview;
while (superView != nil) {
if ([superView isKindOfClass:[UIScrollView class]]) {
UIScrollView* superScroll = (UIScrollView*)superView;
superScroll.scrollEnabled = YES/NO; // put the right value depending on the touch method you are in
}
superView = superView.superview;
}
If you want to detect touches inside any of the subviews of the UIScrollView, you will have to subclass UIScrollView and override the touchesShouldBegin and touchesShouldCancelInContentView methods which are specifically created for this purpose.
Other than this, there is no way you can identify touches in the subviews as UIScrollView tends to handle all touches itself and doesn't pass them to its subviews.
Courtesy:-https://stackoverflow.com/a/392562/1865424
If you have any further issue regarding this.Happy to help you.
Depending on what you are trying to do, maybe you can add:
UILongPressGestureRecognizer *longPressDetect = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:#selector(disableScrolling:)];
[subView addGestureRecognizer:longPressDetect];
and then add a method that disable and re-enable the scrollView from scrolling such as:
-(void)disableScrolling:(UILongPressGestureRecognizer*)longPress {
if (gesture.state == UIGestureRecognizerStateBegan) {
scrollView.scrollEnabled = NO;
}
if (gesture.state == UIGestureRecognizerStateEnded) {
scrollView.scrollEnabled = YES;
}
}
I am developing an iPhone app which has a shopping cart, and I'm using a UITableView to display the cart. There is a cell for each item, and the -tableFooterView is set to a custom view which gives the user a text field to verify their credit card's CVV and a button to complete the checkout process.
When the user taps in the CVV text field, I resize the table view so that the keyboard doesn't cover anything.
- (void)keyboardWillShow:(NSNotification *)n
{
// I'll update this to animate and scroll the view once everything works
self.theTableView.frameHeight = self.view.frameHeight - KEYBOARD_HEIGHT_PORTRAIT_IPHONE;
}
After entering their CVV, the user can tap the Done key to dismiss the keyboard:
- (BOOL)textFieldShouldReturn:(UITextField *)textField {
[textField resignFirstResponder];
return NO;
}
All of that works, however, while the keyboard is visible, my checkout button (a normal UIButton) does not respond to touch events. The table scrolls, but the button's touchUpInside event is never fired.
Once I tap Done and the keyboard is dismissed, the checkout button will recognize touchUpInside events.
From what I've seen, it appears that any button that was covered by the keyboard does not respond to my touches (even it scrolled out from behind the keyboard) until the keyboard is dismissed. Buttons in this same -tableFooterView that are not ever covered by the keyboard remain responsive to touch while the keyboard is visible.
Same behavior when running on iOS 5 and iOS 4.
Can anyone offer any suggestions of what may be going on? Or any helpful ideas for troubleshooting?
Thanks!
Edit - Update
In fact, the portion of the tableFooterView that is covered by the keyboard is not responding to touch events. In my custom UIView subclass, I implemented -touchesBegan:withEvent: and just log that a touch occurred.
Touching anywhere in the view gets a log statement before the keyboard is shown. However, after the tableview is resized, only touching the upper portion of the view generates a log statement.
Also I just realized, the portion of the tableFooterView that was covered by the keyboard turns to the color of the containing view's background color once I scroll that portion to be visible.
I had the same problem. I think it is a bug in iOS, but I've discovered a workaround for it:
- (void)keyboardWillShow:(NSNotification *)note {
NSDictionary* userInfo = [note userInfo];
// get the size of the keyboard
NSValue *boundsValue = [userInfo objectForKey:UIKeyboardFrameBeginUserInfoKey];
CGSize keyboardSize = [boundsValue CGRectValue].size; // screen size
CGFloat keyboardHeight;
if (self.interfaceOrientation == UIInterfaceOrientationPortrait ||
self.interfaceOrientation == UIInterfaceOrientationPortraitUpsideDown) {
keyboardHeight = keyboardSize.height;
} else {
keyboardHeight = keyboardSize.width;
}
// resize the view with the keyboard
__block CGRect origFrame = self.view.frame;
__block CGRect viewFrame = origFrame;
viewFrame.size.height -= keyboardHeight - ((self.tabBarController != nil) ? self.tabBarController.tabBar.frame.size.height : 0);
// Set the height to zero solves the footer view touch events disabled bug
self.view.frame = CGRectMake(origFrame.origin.x, origFrame.origin.y,
viewFrame.size.width, 0);
// We immediately set the height back in the next cycle, before the animation starts
dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
self.view.frame = origFrame; // The original height
// start the animation
[UIView animateWithDuration:0.4 animations:^{
[self.view setFrame:viewFrame];
}];
});
}
The trick is to set the height of the tableView to 0 and back to original in the next running cycle.
This works for me either iOS 4.3 and 5.1.
I think that resizing the UITableView causes it to send the UIButton (a subview) to the back of the view hierarchy. You may need to bring it to the front explicitly after resizing the frame.
[self.theTableView bringSubviewToFront:yourUIButton];
Following worked for me.
Had a table view footer with button, for that button action is linked via xib, apart from that added following code more -
#IBOutlet private weak var myButton: CustomUIButton!
override public func awakeFromNib() {
super.awakeFromNib()
let myButtonTapGesture = UITapGestureRecognizer(target: self, action: #selector(myButtonAction(_:)))
myButton.addGestureRecognizer(myButtonTapGesture)
}
#IBAction func myButtonAction(_ sender: AnyObject) {
// button action implementation
}
So I have to add a tap gesture on button.
I'm using an UIPageViewController in my application and I wanted to have a few UIButtons inside it, sort of like a menu. The problem I have is that when I put an UIButton (or any other interactive element) near the edges of the screen and tap it, instead of the UIButton action being applied, what happens is that the page changes (because the tap on the edge of the screen changes the page on the UIPageViewController). I'd like to know if there's a way to make it so that the UIButton has higher priority than the UIPageViewController so that when I tap the button, it applies the appropriate action instead of changing the page.
I came here with the same problem. Split’s link has the answer.
Make your root view controller the delegate of each of the UIPageViewController’s gesture recognizers, then prevent touches from being delivered if they occur inside any UIControl:
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
{
return ([touch.view isKindOfClass:[UIControl class]] == NO);
}
UIPageViewController has two UIGestureRecognizers. You can access them via gestureRecognizers property. Determine which one is UITapGestureRecognizer and then use this. Hope this helps.
For people that just want to copy/paste code, here is mine :
// I don't want the tap on borders to change the page
-(void) desactivatePageChangerGesture {
for (UIGestureRecognizer* gestureRecognizer in self.pageViewController.gestureRecognizers) {
if ([gestureRecognizer isKindOfClass:[UITapGestureRecognizer class]]) {
gestureRecognizer.enabled = NO;
}
}
}
Just call this function after the UIPageViewController creation.
I had this same problem, and was unsure how to handle the UIGestureRecognizer delegate methods. This short example assumes you are using the "Page Based Application" project type in Xcode 4. Here is what I did:
In RootViewController.h, I made sure to announce that RootViewController would handle the UIGestureRecognizerDelegate protocol:
#interface RootViewController : UIViewController <UIPageViewControllerDelegate, UIGestureRecognizerDelegate>
In RootViewController.m, I assigned RootViewController as the delegate for the UITapGestureRecognizer. This is done at the end of the viewDidLoad method. I did this by iterating over each gestureRecognizer to see which one was the UITapGestureRecognizer.
NSEnumerator *gestureLoop = [self.view.gestureRecognizers objectEnumerator];
id gestureRecognizer;
while (gestureRecognizer = [gestureLoop nextObject]) {
if ([gestureRecognizer isKindOfClass:[UITapGestureRecognizer class]]) {
[(UITapGestureRecognizer *)gestureRecognizer setDelegate:self];
}
}
Finally, I added the gestureRecognizer:shouldReceiveTouch method to the bottom of RootViewController.m (This is copied directly from Split's link):
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
if ([touch.view isKindOfClass:[UIControl class]]) {
// we touched a button, slider, or other UIControl
return NO; // ignore the touch
}
return YES; // handle the touch
}
Comment out these line from your code
self.view.gestureRecognizers = self.pageViewController.gestureRecognizers;
or use UIGestureRecognizer as told by Split
Hope this will help you
OLD ANSWER: If your UIPageViewController has a transitionStyle of UIPageViewControllerTransitionStyleScroll and you are in iOS 6.0+, then you can't use the gestureRecognizer:shouldReceiveTouch: method, because there is no way to set the delegate to self on the gestureRecognizers since pageViewController.gestureRecognizers will return nil. See UIPageViewController returns no Gesture Recognizers in iOS 6 for more information about that.
If you simply want to make sure your UIPageViewController passes along button touch events to a UIButton, you can use
for (UIScrollView *view in _pageViewController.view.subviews) {
if ([view isKindOfClass:[UIScrollView class]]) {
view.delaysContentTouches = NO;
}
}
if you have a transitionStyle of UIPageViewControllerTransitionStyleScroll and you are in iOS 6.0+.
See this answer about why delaysContentTouches = NO is needed for some cases of a UIButton in a UIScrollView
UPDATE: After doing a little more research it appears that if your issue is that the UIButton click seems to only be called sometimes, then that is actually probably the desired behavior inside a UIScrollView. A UIScrollView uses the delaysContentTouches property to automatically determine if the user was trying to scroll or trying to press a button inside the scroll view. I would assume it is best to not alter this behavior to default to NO since doing so will result in an inability to scroll if the user's finger is over a button.
None of the solutions here where you intercept the UIPageViewController's tap gesture recognizers worked for me. I'm targeting iOS 8 and 9.
What worked is to override the functions touchesBegan, touchesCancelled, touchesMoved, and touchesEnded in my custom button which is a subclass of UIControl. Then I just manually send the .TouchUpInside control event if the touch began and ended within the frame of my custom button.
I didn't have to do anything special for the containing page view controller, or the view controller that contains the page view controller.
Swift 5 answer here should do the job.
pageViewController.view.subviews.compactMap({ $0 as? UIScrollView }).first?.delaysContentTouches = false
I have added UIPickerView to the UIScrollView but now UPickerView is not scrolling. When I add it to the self.view it scrolls smoothly.
Here i my code
monthsArray = [[NSArray alloc] initWithObjects:#"Jan",#"Feb",#"Mar",#"Apr",#"May",#"Jun",#"Jul",#"Aug",#"Sep",#"Oct",#"Nov",#"Dec",nil];
UIPickerView *objPickerView = [[UIPickerView alloc] initWithFrame:CGRectMake(185,350,100,100)];
objPickerView.userInteractionEnabled=YES;
objPickerView.delegate = self;
objPickerView.showsSelectionIndicator = YES;
[objScrollView addSubView:objPickerView];
I have included the delegete and its methods. have a look on this issue. Thanks in advance.
If I am not clear please tell me.
I am using a subclass of UIPickerView for the same purpose, but mine is much simpler:
#implementation ScrollablePickerView
- (UIScrollView *)findScrollableSuperview {
UIView *parent = self.superview;
while ((nil != parent) && (![parent isKindOfClass:[UIScrollView class]])) {
parent = parent.superview;
}
UIScrollView* scrollView = (UIScrollView *)parent;
return scrollView;
}
- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent*)event {
UIScrollView* scrollView = [self findScrollableSuperview];
if (CGRectContainsPoint(self.bounds, point)) {
scrollView.canCancelContentTouches = NO;
scrollView.delaysContentTouches = NO;
} else {
scrollView.canCancelContentTouches = YES;
scrollView.delaysContentTouches = YES;
}
return [super hitTest:point withEvent:event];
}
#end
Works like a charm - at least for me.
From the UIScrollView class documentation:
Important: You should not embed UIWebView or UITableView objects in UIScrollView objects. If you do so, unexpected behavior can result because touch events for the two objects can be mixed up and wrongly handled.
They don't mention UIPickerView there, but I wonder if it should have been added to that list. It shares in common with the others the characteristic of using touches to scroll things.
I guess this might solve your problem (be sure to check the comments too):
http://www.alexc.me/uiscrollview-and-uidatepicker/153/
Basically you have to set DelaysContentTouches and CanCancelContentTouches to NO on the scroll view, as it steals the touch events from the picker.
UIPickerView is not designed to be scrolled or moved at all. You should place some text field instead and show UIPickerView when user taps on it.
I had a similar issue with a UIPickerView nested within a UIScrollView and solved it by subclassing UIPickerView. Changes to DelaysContentTouches/CanCancelContentTouches didn't help, and the other "answers" here that basically say "don't do it" -- well, that's no answer at all! It can be done and you can get a picker to behave within a scrollView.
I answered the following question on subclassing UIPickerView, including some code at the end which may help you:
Responding to touchesBegan in UIPickerView instead of UIView
I've been running into a similar problem. I've got a UIPickerView that I turned horizontal by applying a transform ( which works pretty well), however inside of a scrollview, scrolling only works at the left hand side.
What I think is happening is that the UIPicker looks up it's parent chain to see if there are any gesture recognisers, and sets itself as a delegate, so it can disable gesture recognition further up the chain for touches within it's boundaries, and while the transform changes the visual boundaries, it doesn't change the frame, is it is looking for touches within it's original bounds.
If this isn't what is happening, you could use this to prevent the scroll view stealing your touches.
I'm think I'm going to switch to a custom UISCrollView instead of my transformed UIPickerView.
On iOS 11 and iOS 12 changing canCancelContentTouches and delaysContentTouches when UIScrollView is already been dragging doesn't help. So I did the hack by intercepting some touches before scrollView.panGestureRecognizer receives them.
Here is Swift 4 code. Maybe we can check if touch is inside picker more elegantly. But it works good.
public class TableView: UITableView {
/// Check if view is a picker's subview
private func isInPickerView(_ view: UIView) -> Bool {
var prev: UIView? = view
while prev != nil {
if prev is UIPickerView || prev is UIDatePicker {
return true
}
prev = prev?.superview
}
return false
}
// UITableView is already UIGestureRecognizerDelegate internally,
// so we just need to overwrite 1 method here
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer,
shouldReceive touch: UITouch) -> Bool {
// we don't care about any recognizers except self.panGestureRecognizer
guard gestureRecognizer == panGestureRecognizer else {
return true
}
// if touch is inside picker - we don't pass this touch
// to panGestureRecognizer
let location = touch.location(in: self)
if let view = self.hitTest(location, with: nil), isInPickerView(view) {
return false
}
return true
}
}