UIPickerView is not scrolling when added to UIScrollView! - iphone

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
}
}

Related

UITableView scrolling problems when inside a UIScrollView

I have a UIScrollView (with paging) to which I add three UIViews. Each of these UIViews has a UITableView inside. So, the user should be able to scroll horizontally to the page he wants and then scroll vertically in the corresponding table.
However, some of the tables don't receive the scrolling gestures. Usually the first one does behave good, but the other ones do not. I can't select cells nor scroll the table up or down.
I used the default settings for the UIScrollView, except for these ones defined in the viewDidLoad:
- (void)viewDidLoad
{
[super viewDidLoad];
//Load the view controllers
[self loadViewControllers];
//Configure the scroll view
self.scrollView.pagingEnabled = YES;
self.scrollView.contentSize = CGSizeMake(CGRectGetWidth(self.scrollView.frame) * viewControllers.count, CGRectGetHeight(self.scrollView.frame));
self.scrollView.showsHorizontalScrollIndicator = NO;
self.scrollView.showsVerticalScrollIndicator = NO;
self.scrollView.scrollsToTop = NO;
self.scrollView.delegate = self;
//Configure the page control
self.pageControl.numberOfPages = viewControllers.count;
self.pageControl.currentPage = 0;
}
I can't figure out why I can't scroll some of the tables... Any help would be greatly appreciated.
Thanks in advance!
Try to set
self.scrollView.delaysContentTouches = YES;
self.scrollView.canCancelContentTouches = NO;
Maybe the UIScrollView don't pass touch informations to the subviews.
I tried to reproduce a simplified version of your needs using basically Interface Builder and it seems to me it's working using basic coding and using default settings. Can you pls check my quick n dirty Github repo and kindly ask to reply whether it is applicable to your situation or what is missing.
https://github.com/codedad/SO_ScrollView_with_Tables
By default Interface Builder creates UIScrollView and UITableViews enabling:
Delays Content Touches ON
Cancellable Content Touches ON
Things I would check:
Check your View Hierarchies - Is something being laid on top of your UITableView, causing it not to receive a tap?
Are your UITableViews being disabled anywhere? I would set a breakpoint in tableView:didSelectRowAtIndexPath: and see if that method is being called.
Check this post
I guess those aren't sure-fire answers but hopefully they'll help discover the problem!
This worked for me
I programmatically added the tableView to my scroll view using addSubview:
UIGestureRecognizerDelegate is needed.
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
if ([touch.view isDescendantOfView:self.signUpJammerList]) {
return NO;
}
return YES;
}

Dynamically setting layout on UICollectionView causes inexplicable contentOffset change

According to Apple's documentation (and touted at WWDC 2012), it is possible to set the layout on UICollectionView dynamically and even animate the changes:
You normally specify a layout object when creating a collection view but you can also change the layout of a collection view dynamically. The layout object is stored in the collectionViewLayout property. Setting this property directly updates the layout immediately, without animating the changes. If you want to animate the changes, you must call the setCollectionViewLayout:animated: method instead.
However, in practice, I've found that UICollectionView makes inexplicable and even invalid changes to the contentOffset, causing cells to move incorrectly, making the feature virtually unusable. To illustrate the problem, I put together the following sample code that can be attached to a default collection view controller dropped into a storyboard:
#import <UIKit/UIKit.h>
#interface MyCollectionViewController : UICollectionViewController
#end
#implementation MyCollectionViewController
- (void)viewDidLoad
{
[super viewDidLoad];
[self.collectionView registerClass:[UICollectionViewCell class] forCellWithReuseIdentifier:#"CELL"];
self.collectionView.collectionViewLayout = [[UICollectionViewFlowLayout alloc] init];
}
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
return 1;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
UICollectionViewCell *cell = [self.collectionView dequeueReusableCellWithReuseIdentifier:#"CELL" forIndexPath:indexPath];
cell.backgroundColor = [UIColor whiteColor];
return cell;
}
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
{
NSLog(#"contentOffset=(%f, %f)", self.collectionView.contentOffset.x, self.collectionView.contentOffset.y);
[self.collectionView setCollectionViewLayout:[[UICollectionViewFlowLayout alloc] init] animated:YES];
NSLog(#"contentOffset=(%f, %f)", self.collectionView.contentOffset.x, self.collectionView.contentOffset.y);
}
#end
The controller sets a default UICollectionViewFlowLayout in viewDidLoad and displays a single cell on-screen. When the cells is selected, the controller creates another default UICollectionViewFlowLayout and sets it on the collection view with the animated:YES flag. The expected behavior is that the cell does not move. The actual behavior, however, is that the cell scroll off-screen, at which point it is not even possible to scroll the cell back on-screen.
Looking at the console log reveals that the contentOffset has inexplicably changed (in my project, from (0, 0) to (0, 205)). I posted a solution for the solution for the non-animated case (i.e. animated:NO), but since I need animation, I'm very interested to know if anyone has a solution or workaround for the animated case.
As a side-note, I've tested custom layouts and get the same behavior.
UICollectionViewLayout contains the overridable method targetContentOffsetForProposedContentOffset: which allows you to provide the proper content offset during a change of layout, and this will animate correctly. This is available in iOS 7.0 and above
I have been pulling my hair out over this for days and have found a solution for my situation that may help.
In my case I have a collapsing photo layout like in the photos app on the ipad. It shows albums with the photos on top of each other and when you tap an album it expands the photos. So what I have is two separate UICollectionViewLayouts and am toggling between them with [self.collectionView setCollectionViewLayout:myLayout animated:YES] I was having your exact problem with the cells jumping before animation and realized it was the contentOffset. I tried everything with the contentOffset but it still jumped during animation. tyler's solution above worked but it was still messing with the animation.
Then I noticed that it happens only when there were a few albums on the screen, not enough to fill the screen. My layout overrides -(CGSize)collectionViewContentSize as recommended. When there are only a few albums the collection view content size is less than the views content size. That's causing the jump when I toggle between the collection layouts.
So I set a property on my layouts called minHeight and set it to the collection views parent's height. Then I check the height before I return in -(CGSize)collectionViewContentSize I ensure the height is >= the minimum height.
Not a true solution but it's working fine now. I would try setting the contentSize of your collection view to be at least the length of it's containing view.
edit:
Manicaesar added an easy workaround if you inherit from UICollectionViewFlowLayout:
-(CGSize)collectionViewContentSize { //Workaround
CGSize superSize = [super collectionViewContentSize];
CGRect frame = self.collectionView.frame;
return CGSizeMake(fmaxf(superSize.width, CGRectGetWidth(frame)), fmaxf(superSize.height, CGRectGetHeight(frame)));
}
2019 actual solution
Say you have a number of layouts for your "Cars" view.
Let's say you have three.
CarsLayout1: UICollectionViewLayout { ...
CarsLayout2: UICollectionViewLayout { ...
CarsLayout3: UICollectionViewLayout { ...
It will jump when you animate between layouts.
It's just an undeniable mistake by Apple. It jumps when you animate, without question.
The fix is this:
You must have a global float, and, the following base class:
var avoidAppleMessupCarsLayouts: CGPoint? = nil
class FixerForCarsLayouts: UICollectionViewLayout {
override func prepareForTransition(from oldLayout: UICollectionViewLayout) {
avoidAppleMessupCarsLayouts = collectionView?.contentOffset
}
override func targetContentOffset(
forProposedContentOffset proposedContentOffset: CGPoint) -> CGPoint {
if avoidAppleMessupCarsLayouts != nil {
return avoidAppleMessupCarsLayouts!
}
return super.targetContentOffset(forProposedContentOffset: proposedContentOffset)
}
}
So here are the three layouts for your "Cars" screen:
CarsLayout1: FixerForCarsLayouts { ...
CarsLayout2: FixerForCarsLayouts { ...
CarsLayout3: FixerForCarsLayouts { ...
That's it. It now works.
Incredibly obscurely, you could have different "sets" of layouts (for Cars, Dogs, Houses, etc.), which could (conceivably) collide. For this reason, have a global and a base class as above for each "set".
This was invented by passing user #Isaacliu, above, many years ago.
A detail, FWIW in Isaacliu's code fragment, finalizeLayoutTransition is added. In fact it's not necessary logically.
The fact is, until Apple change how it works, every time you animate between collection view layouts, you do have to do this. That's life!
This issue bit me as well and it seems to be a bug in the transition code. From what I can tell it tries to focus on the cell that was closest to the center of the pre-transition view layout. However, if there doesn't happen to be a cell at the center of the view pre-transition then it still tries to center where the cell would be post-transition. This is very clear if you set alwaysBounceVertical/Horizontal to YES, load the view with a single cell and then perform a layout transition.
I was able to get around this by explicitly telling the collection to focus on a specific cell (the first cell visible cell, in this example) after triggering the layout update.
[self.collectionView setCollectionViewLayout:[self generateNextLayout] animated:YES];
// scroll to the first visible cell
if ( 0 < self.collectionView.indexPathsForVisibleItems.count ) {
NSIndexPath *firstVisibleIdx = [[self.collectionView indexPathsForVisibleItems] objectAtIndex:0];
[self.collectionView scrollToItemAtIndexPath:firstVisibleIdx atScrollPosition:UICollectionViewScrollPositionCenteredVertically animated:YES];
}
Jumping in with a late answer to my own question.
The TLLayoutTransitioning library provides a great solution to this problem by re-tasking iOS7s interactive transitioning APIs to do non-interactive, layout to layout transitions. It effectively provides an alternative to setCollectionViewLayout, solving the content offset issue and adding several features:
Animation duration
30+ easing curves (courtesy of Warren Moore's AHEasing library)
Multiple content offset modes
Custom easing curves can be defined as AHEasingFunction functions. The final content offset can be specified in terms of one or more index paths with Minimal, Center, Top, Left, Bottom or Right placement options.
To see what I mean, try running the Resize demo in the Examples workspace and playing around with the options.
The usage is like this. First, configure your view controller to return an instance of TLTransitionLayout:
- (UICollectionViewTransitionLayout *)collectionView:(UICollectionView *)collectionView transitionLayoutForOldLayout:(UICollectionViewLayout *)fromLayout newLayout:(UICollectionViewLayout *)toLayout
{
return [[TLTransitionLayout alloc] initWithCurrentLayout:fromLayout nextLayout:toLayout];
}
Then, instead of calling setCollectionViewLayout, call transitionToCollectionViewLayout:toLayout defined in the UICollectionView-TLLayoutTransitioning category:
UICollectionViewLayout *toLayout = ...; // the layout to transition to
CGFloat duration = 2.0;
AHEasingFunction easing = QuarticEaseInOut;
TLTransitionLayout *layout = (TLTransitionLayout *)[collectionView transitionToCollectionViewLayout:toLayout duration:duration easing:easing completion:nil];
This call initiates an interactive transition and, internally, a CADisplayLink callback that drives the transition progress with the specified duration and easing function.
The next step is to specify a final content offset. You can specify any arbitrary value, but the toContentOffsetForLayout method defined in UICollectionView-TLLayoutTransitioning provides an elegant way to calculate content offsets relative to one or more index paths. For example, in order to have a specific cell to end up as close to the center of the collection view as possible, make the following call immediately after transitionToCollectionViewLayout:
NSIndexPath *indexPath = ...; // the index path of the cell to center
TLTransitionLayoutIndexPathPlacement placement = TLTransitionLayoutIndexPathPlacementCenter;
CGPoint toOffset = [collectionView toContentOffsetForLayout:layout indexPaths:#[indexPath] placement:placement];
layout.toContentOffset = toOffset;
Easy.
Animate your new layout and collectionView's contentOffset in the same animation block.
[UIView animateWithDuration:0.3 animations:^{
[self.collectionView setCollectionViewLayout:self.someLayout animated:YES completion:nil];
[self.collectionView setContentOffset:CGPointMake(0, -64)];
} completion:nil];
It will keep self.collectionView.contentOffset constant.
If you are simply looking for the content offset to not change when transition from layouts, you can creating a custom layout and override a couple methods to keep track of the old contentOffset and reuse it:
#interface CustomLayout ()
#property (nonatomic) NSValue *previousContentOffset;
#end
#implementation CustomLayout
- (CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset
{
CGPoint previousContentOffset = [self.previousContentOffset CGPointValue];
CGPoint superContentOffset = [super targetContentOffsetForProposedContentOffset:proposedContentOffset];
return self.previousContentOffset != nil ? previousContentOffset : superContentOffset ;
}
- (void)prepareForTransitionFromLayout:(UICollectionViewLayout *)oldLayout
{
self.previousContentOffset = [NSValue valueWithCGPoint:self.collectionView.contentOffset];
return [super prepareForTransitionFromLayout:oldLayout];
}
- (void)finalizeLayoutTransition
{
self.previousContentOffset = nil;
return [super finalizeLayoutTransition];
}
#end
All this is doing is saving the previous content offset before the layout transition in prepareForTransitionFromLayout, overwriting the new content offset in targetContentOffsetForProposedContentOffset, and clearing it in finalizeLayoutTransition. Pretty straightforward
If it helps add to the body of experience: I encountered this problem persistently regardless of the size of my content, whether I had set a content inset, or any other obvious factor. So my workaround was somewhat drastic. First I subclassed UICollectionView and added to combat inappropriate content offset setting:
- (void)setContentOffset:(CGPoint)contentOffset animated:(BOOL)animated
{
if(_declineContentOffset) return;
[super setContentOffset:contentOffset];
}
- (void)setContentOffset:(CGPoint)contentOffset
{
if(_declineContentOffset) return;
[super setContentOffset:contentOffset];
}
- (void)setCollectionViewLayout:(UICollectionViewLayout *)layout animated:(BOOL)animated
{
_declineContentOffset ++;
[super setCollectionViewLayout:layout animated:animated];
_declineContentOffset --;
}
- (void)setContentSize:(CGSize)contentSize
{
_declineContentOffset ++;
[super setContentSize:contentSize];
_declineContentOffset --;
}
I'm not proud of it but the only workable solution seems to be completely to reject any attempt by the collection view to set its own content offset resulting from a call to setCollectionViewLayout:animated:. Empirically it looks like this change occurs directly in the immediate call, which obviously isn't guaranteed by the interface or the documentation but makes sense from a Core Animation point of view so I'm perhaps only 50% uncomfortable with the assumption.
However there was a second issue: UICollectionView was now adding a little jump to those views that were staying in the same place upon a new collection view layout — pushing them down about 240 points and then animating them back to the original position. I'm unclear why but I modified my code to deal with it nevertheless by severing the CAAnimations that had been added to any cells that, actually, weren't moving:
- (void)setCollectionViewLayout:(UICollectionViewLayout *)layout animated:(BOOL)animated
{
// collect up the positions of all existing subviews
NSMutableDictionary *positionsByViews = [NSMutableDictionary dictionary];
for(UIView *view in [self subviews])
{
positionsByViews[[NSValue valueWithNonretainedObject:view]] = [NSValue valueWithCGPoint:[[view layer] position]];
}
// apply the new layout, declining to allow the content offset to change
_declineContentOffset ++;
[super setCollectionViewLayout:layout animated:animated];
_declineContentOffset --;
// run through the subviews again...
for(UIView *view in [self subviews])
{
// if UIKit has inexplicably applied animations to these views to move them back to where
// they were in the first place, remove those animations
CABasicAnimation *positionAnimation = (CABasicAnimation *)[[view layer] animationForKey:#"position"];
NSValue *sourceValue = positionsByViews[[NSValue valueWithNonretainedObject:view]];
if([positionAnimation isKindOfClass:[CABasicAnimation class]] && sourceValue)
{
NSValue *targetValue = [NSValue valueWithCGPoint:[[view layer] position]];
if([targetValue isEqualToValue:sourceValue])
[[view layer] removeAnimationForKey:#"position"];
}
}
}
This appears not to inhibit views that actually do move, or to cause them to move incorrectly (as if they were expecting everything around them to be down about 240 points and to animate to the correct position with them).
So this is my current solution.
I've probably spent about two weeks now trying to get various layout to transition between one another smoothly. I've found that override the proposed offset is working in iOS 10.2, but in version prior to that I still get the issue. The thing that makes my situation a bit worse is I need to transition into another layout as a result of a scroll, so the view is both scrolling and transitioning at the same time.
Tommy's answer was the only thing that worked for me in pre 10.2 versions. I'm doing the following thing now.
class HackedCollectionView: UICollectionView {
var ignoreContentOffsetChanges = false
override func setContentOffset(_ contentOffset: CGPoint, animated: Bool) {
guard ignoreContentOffsetChanges == false else { return }
super.setContentOffset(contentOffset, animated: animated)
}
override var contentOffset: CGPoint {
get {
return super.contentOffset
}
set {
guard ignoreContentOffsetChanges == false else { return }
super.contentOffset = newValue
}
}
override func setCollectionViewLayout(_ layout: UICollectionViewLayout, animated: Bool) {
guard ignoreContentOffsetChanges == false else { return }
super.setCollectionViewLayout(layout, animated: animated)
}
override var contentSize: CGSize {
get {
return super.contentSize
}
set {
guard ignoreContentOffsetChanges == false else { return }
super.contentSize = newValue
}
}
}
Then when I set the layout I do this...
let theContentOffsetIActuallyWant = CGPoint(x: 0, y: 100)
UIView.animate(withDuration: animationDuration,
delay: 0, options: animationOptions,
animations: {
collectionView.setCollectionViewLayout(layout, animated: true, completion: { completed in
// I'm also doing something in my layout, but this may be redundant now
layout.overriddenContentOffset = nil
})
collectionView.ignoreContentOffsetChanges = true
}, completion: { _ in
collectionView.ignoreContentOffsetChanges = false
collectionView.setContentOffset(theContentOffsetIActuallyWant, animated: false)
})
This finally worked for me (Swift 3)
self.collectionView.collectionViewLayout = UICollectionViewFlowLayout()
self.collectionView.setContentOffset(CGPoint(x: 0, y: -118), animated: true)

UIButton interaction inside UIPageViewController

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

How to disable touch input to all views except the top-most view?

I have a view with multiple subviews. When a user taps a subview, the subview expands in size to cover most of the screen, but some of the other subviews are still visible underneath.
I want my app to ignore touches on the other subviews when one of the subviews is "expanded" like this. Is there a simple way to achieve this? I can write code to handle this, but I was hoping there's a simpler built-in way.
Hope this help...
[[yourSuperView subviews]
makeObjectsPerformSelector:#selector(setUserInteractionEnabled:)
withObject:[NSNumber numberWithBool:FALSE]];
which will disable userInteraction of a view's immediate subviews..Then give userInteraction to the only view you wanted
yourTouchableView.setUserInteraction = TRUE;
EDIT:
It seems in iOS disabling userInteraction on a parent view doesn't disable userInteraction on its childs.. So the code above (I mean the one with makeObjectsPerformSelector:)will only work to disable userInteraction of a parent's immediate subviews..
See user madewulf's answer which recursively get all subviews and disable user interaction of all of them. Or if you need to disable userInteraction of this view in many places in the project, You can categorize UIView to add that feature.. Something like this will do..
#interface UIView (UserInteractionFeatures)
-(void)setRecursiveUserInteraction:(BOOL)value;
#end
#implementation UIView(UserInteractionFeatures)
-(void)setRecursiveUserInteraction:(BOOL)value{
self.userInteractionEnabled = value;
for (UIView *view in [self subviews]) {
[view setRecursiveUserInteraction:value];
}
}
#end
Now you can call
[yourSuperView setRecursiveUserInteraction:NO];
Also user #lxt's suggestion of adding an invisible view on top of all view's is one other way of doing it..
There are a couple of ways of doing this. You could iterate through all your other subviews and set userInteractionEnabled = NO, but this is less than ideal if you have lots of other views (you would, after all, have to subsequently renable them all).
The way I do this is to create an invisible UIView that's the size of the entire screen that 'blocks' all the touches from going to the other views. Sometimes this is literally invisible, other times I may set it to black with an alpha value of 0.3 or so.
When you expand your main subview to fill the screen you can add this 'blocking' UIView behind it (using insertSubview: belowSubview:). When you minimize your expanded subview you can remove the invisible UIView from your hierarchy.
So not quite built-in, but I think the simplest approach. Not sure if that was what you were thinking of already, hopefully it was of some help.
Beware of the code given as solution here by Krishnabhadra:
[[yourSuperView subviews]makeObjectsPerformSelector:#selector(setUserInteractionEnabled:) withObject:[NSNumber numberWithBool:FALSE]];
This will not work in all cases because [yourSuperView subviews] only gives the direct subviews of the superview. To make it work, you will have to iterate recursively on all subviews:
-(void) disableRecursivelyAllSubviews:(UIView *) theView
{
theView.userInteractionEnabled = NO;
for(UIView* subview in [theView subviews])
{
[self disableRecursivelyAllSubviews:subview];
}
}
-(void) disableAllSubviewsOf:(UIView *) theView
{
for(UIView* subview in [theView subviews])
{
[self disableRecursivelyAllSubviews:subview];
}
}
Now a call to disableAllSubviewsOf will do what you wanted to do.
If you have a deep stack of views, the solution by lxt is probably better.
I would do this by putting a custom transparent button with the same frame as the superView. And then on top of that button I would put view that should accept user touches.
Button will swallow all touches and views behind it wouldn't receive any touch events, but view on top of the button will receive touches normally.
Something like this:
- (void)disableTouchesOnView:(UIView *)view {
UIButton *ghostButton = [[UIButton alloc] initWithFrame:CGRectMake(0, 0, view.frame.size.width, view.frame.size.height)];
[ghostButton setBackgroundColor:[UIColor clearColor]];
ghostButton.tag = 42; // Any random number. Use #define to avoid putting numbers in code.
[view addSubview:ghostButton];
}
And a method for enabling the parentView.
- (void)enableTouchesOnView:(UIView *)view {
[[view viewWithTag:42] removeFromSuperview];
}
So, to disable all views in the parentViev behind yourView, I would do this:
YourView *yourView = [[YourView alloc] initWithCustomInitializer];
// It is important to disable touches on the parent view before adding the top most view.
[self disableTouchesOnView:parentView];
[parentView addSubview:yourView];
Just parentView.UserInteractionEnabled = NO will do the work.
Parent view will disable user interaction on all the view's subviews. But enable it does not enable all subviews(by default UIImageView is not interactable). So an easy way is find the parent view and use the code above, and there is no need to iterate all subviews to perform a selector.
Add a TapGestureRecognizer to your "background view" (the translucent one which "grays out" your normal interface) and set it to "Cancels Touches In View", without adding an action.
let captureTaps = UITapGestureRecognizer()
captureTaps.cancelsTouchesInView = true
dimmedOverlay?.addGestureRecognizer(captureTaps)
I will give my 2 cents to this problem.
Iteratively run userInteractionEnabled = false it's one way.
Another way will be add a UIView like following.
EZEventEater.h
#import <UIKit/UIKit.h>
#interface EZEventEater : UIView
#end
EZEventEater.m
#import "EZEventEater.h"
#implementation EZEventEater
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
// Initialization code
self.backgroundColor = [UIColor clearColor];
self.userInteractionEnabled = false;
}
return self;
}
- (void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
//EZDEBUG(#"eater touched");
}
- (void) touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
}
- (void) touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
}
In your code you add the EZEventEater view to cover all the views that your may block the touch event.
Whenever you want to block the touch event to those views, simply call
eater.userInteractionEnabled = YES;
Hope this helpful.
In Swift 5, I achieved this behaviour by placing a view right on top(the highlighted one) and setting:
myView.isUserInteractionEnabled = true
This does not let the touches go through it, thus ignoring the taps.
For my app, I think it will be sufficient to disable navigation to other tabs of the app (for a limited duration, while I'm doing some processing):
self.tabBarController.view.userInteractionEnabled = NO;
Also, I disabled the current view controller--
self.view.userInteractionEnabled = NO;
(And, by the way, the recursive solutions proposed here had odd effects in my app. The disable seems to work fine, but the re-enable has odd effects-- some of the UI was not renabled).
Simple solution. Add a dummy gesture that does nothing. Make it reusable by adding it to an extension like this:
extension UIView {
func addNullGesture() {
let gesture = UITapGestureRecognizer(target: self,
action: #selector(nullGesture))
addGestureRecognizer(gesture)
}
#objc private func nullGesture() {}
}
setUserInteractionEnabled = NO on the view you want to disable
I had the same problem, but the above solutions did not help.
I then noticed that calling
super.touchesBegan(...) was the problem.
After removing this the event was only handled by the top-most view.
I hope this is of help to anybody.

UIButton Doesn't allow UIScrollView to scroll

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.