This has been a bit of a head scratcher for me. In an app I'm building I am using a UITextField, and adding a button as the leftView property. However, it seems that on an iPad (both sim and on device), the button is receiving touches that are well outside its bounds. This is interfering with the ability of the UITextField to become the first responder when the user touches the placeholder text. It seems that when touching the placeholder text, the events are being handled by the button instead of the text field itself. Strangely, this only appears to happen on iPad; it works as expected on the iPhone.
Here is some simple code demonstrating the problem:
- (void)viewWillLayoutSubviews {
[super viewWillLayoutSubviews];
UITextField *textField = [[UITextField alloc] initWithFrame:CGRectMake(10.0f,
10.0f,
(self.view.frame.size.width - 20.0f),
35.0f)];
textField.borderStyle = UITextBorderStyleRoundedRect;
textField.placeholder = #"Test text";
[self.view addSubview:textField];
UIButton *addButton = [UIButton buttonWithType:UIButtonTypeContactAdd];
[addButton addTarget:self action:#selector(touchDown:) forControlEvents:UIControlEventTouchDown];
[addButton addTarget:self action:#selector(touchUpInside) forControlEvents:UIControlEventTouchUpInside];
[addButton addTarget:self action:#selector(touchUpOutside) forControlEvents:UIControlEventTouchUpOutside];
textField.leftView = addButton;
textField.leftViewMode = UITextFieldViewModeAlways;
}
- (void)touchDown:(id)sender {
NSLog(#"touchDown");
}
- (void)touchUpInside {
NSLog(#"touchUpInside");
}
- (void)touchUpOutside {
NSLog(#"touchUpOutside");
}
It seems that sometimes touches are still perceived as being inside even though they appear to be outside the button's bounds. Then getting even further right, the button only receives UIControlEventTouchDown then UIControlEventTouchUpOutside.
2013-07-25 11:51:44.217 TestApp[22722:c07] touchDown
2013-07-25 11:51:44.306 TestApp[22722:c07] touchUpInside
2013-07-25 11:51:44.689 TestApp[22722:c07] touchDown
2013-07-25 11:51:44.801 TestApp[22722:c07] touchUpOutside
EDIT
Here is an example with the background color of the button changed as well as the approximate zones that trigger the events above. Also, I checked the frame of the button and it is less than 30px wide.
I sat down and spent some more time on this last night. I was already subclassing UITextField in my real application, so I ended up overriding -(id)hitTest:withEvent: as seen below. This has been working fine so far.
- (id)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
if ([[super hitTest:point withEvent:event] isEqual:self.leftView]) {
if (CGRectContainsPoint(self.leftView.frame, point)) {
return self.leftView;
} else {
return self;
}
}
return [super hitTest:point withEvent:event];
}
I verified your results. I think it's most likely a bug in UITextView. Unfortunately, for some reason if you set leftView, events in that area are being sent to the wrong view (on the iPad).
I don't have a simple workaround for you since adjusting the frame of the leftView has no effect. I think it needs to be fixed at a lower level than we have access to. Maybe report the bug to Apple and live with it for now?
You could track the location of the touch and ignore it if it's out of bounds, but it seems like a lot of work for a minor bug?
Just met this question too, for some other solution I found is adding a check whether the event point located inside the button.
- (void)touchUpInside:(UIButton *)sender event:(UIEvent *)event
{
CGPoint location = [[[event allTouches] anyObject] locationInView:sender];
if (!CGRectContainsPoint(sender.bounds, location)) {
// Outside of bounds, so ignore:
return;
}
// Inside our bounds, so continue as normal:
}
Related
I'm building out a UITableView that has a button in the tableview's section header, so that as the user scrolls down the screen, the button is pinned to the top of the screen.
The problem is that the touch events for the section header's button don't register when the UITableView is moving. The button only gets the touch event if the tableview is not scrolling.
-(UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section
{
if (!_sectionHeaderView) {
_sectionHeaderView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width, 200)];
UIButton *backButton = [UIButton buttonWithType:UIButtonTypeCustom];
backButton.frame = CGRectMake(0, 0, 50, 50);
[backButton setImage:[UIImage imageNamed:#"upArrow"] forState:UIControlStateNormal];
[backButton addTarget:self action:#selector(scrollToTop:) forControlEvents:UIControlEventTouchUpInside];
[_sectionHeaderView addSubview:backButton];
}
return _sectionHeaderView;
}
Currently, what happens is:
- If the tableview is not scrolling, the button performs as you'd expect: tap it, and the button gets the TouchUpInside gesture.
- If the tableview is scrolling, the button doesn't get the TouchUpInside gesture - instead, the tableview just stops in place.
I've tried subclassing the UITableView and looking at the touchesBegan: methods, etc and that didn't work. I see the same pattern: the gestures only show up if/when the tableview isn't moving.
Update:
This isn't a usability issue - while I can't show screenshots of the design I'm working on, it's a valid use case to use buttons & controls in the section header.
Here's a quick sketch to explain why:
Figured it out! All you need to do is subclass UITableView, and override hitTest:withEvent:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
if ([[super hitTest:point withEvent:event] isKindOfClass:[UIButton class]]) {
UIButton *buttonThatWasTapped = (UIButton *)[super hitTest:point withEvent:event];
[buttonThatWasTapped sendActionsForControlEvents:UIControlEventTouchUpInside];
}
return [super hitTest:point withEvent:event];
}
Here's what this code does: if the tableview is touched, and the touch occurred in the bounds of a UIButton, the tableview will forward the touch event to the UIButton.
There's a caveat here: the touch event may get sent to the UIButton several times (I'm seeing the call being made 3 times for each touch). This code was sufficient for my needs, but if you're using this code in your app, watch out for that issue - you may have to modify the above code to account for this.
This is what's supposed to happen.
The user is used to this behavior - it allows them to stop the table view's scrolling when they see what they wanted to touch.
Imagine I was scrolling through a table looking for the row 'unicorns', and I was scrolling really fast (because it's a big table, and I'm not sure where unicorns is). When 'unicorns' crosses my vision, I will instinctively stop the table to select it.
With the current system, I can tap anywhere on the table view and it will stop. If table views were implemented your way, I would tap 'underground rockchucks' to stop the table, and all of a sudden I am looking at information about rockchucks - and I want information about Unicorns.
I now become a disgruntled user, return your app to Apple, and get my $0.99 back.
You don't want users doing that.
I've got a strange bug whereby two UIButtons inside a UIView, which is in turn inside a UIScrollView view are not clickable on iOS 5, but work perfectly fine on iOS 6 (screenshot shows scroller and map underneath)
The only other detail is the scroller view 'slides up' to reveal buttons when a station is selected. I've tried selecting the buttons on iOS 5, and they do get hit (visually), but the event isn't fired.
Edit: If I tap and hold on the button in the simulator, then move the cursor up the screen and release (e.g. to the part of the UIView that was always visible), the event fires.
The scroll view itself has the following settings, the latter two have only been added in order to try and make the buttons work on iOS 5 (tried various combinations):
self.scrollView.clipsToBounds = YES;
self.scrollView.scrollEnabled = YES;
self.scrollView.pagingEnabled = YES;
self.scrollView.delaysContentTouches = NO;
self.scrollView.canCancelContentTouches = NO;
The button events are all wired up correctly, with suitable targets etc:
[_updatePriceButton addTarget:self action:#selector(showPriceUpdateBoxWithPrice:) forControlEvents:UIControlEventTouchUpInside];
[_stationDetailsButton addTarget:self action:#selector(stationDetailsSelected:) forControlEvents:UIControlEventTouchUpInside];
And handlers (here's one as a sample):
- (void)stationDetailsSelected:(id)sender {
if ([self.delegate respondsToSelector:#selector(stationDetailsSelected:)]) {
[self.delegate stationDetailsSelected:_station];
}
}
Any help would be great!
Found the culprit - a misconfigured UITapGestureRecognizer on the UIView the UIButtons sit on:
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(stationInfoViewTapped:)];
[infoController.view addGestureRecognizer:tap];
Simply adding the following after init of 'tap' solves the issue in iOS 5:
[tap setCancelsTouchesInView:NO];
I had a project which contains lots of UIButton, using xcode 4.5 and storyboard and ARC. after testing in ios6, everything goes fine. But in ios5, UIButton touch up inside event not working, the action not called. I tried to use touch down and it works. However, I have a lot of UIButton, I cannot change that one by one. what's more, the touch down event does give a good experience.
I used code below in many of my view controllers:
in viewDidLoad:
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self
action:#selector(dismissKeyboard)];
[self.view addGestureRecognizer:tap];
-(void)dismissKeyboard {
[aTextField resignFirstResponder];
}
this might be the reason. I will check it out later. But it works in ios6.
Do you know what's wrong ? Thanks very much!
I had the same problem to you.
That's because the touch event in iOS5 is prevented when the gestureRecognizer you registered captures the event.
There are two solutions.
One:
1) Add a new view inside your view. It should have the same level to the buttons. The priority of the buttons should be higher than the new view.
2) change the call of 'addGestureRecognizer' to the new view.
[self.newView addGestureRecognizer:tap];
Two:
1) Implement the code below. You should return NO when the point is in the buttons.
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
{
CGPoint point = [gestureRecognizer locationInView:self.view];
NSLog(#"x = %.0f, y = %.0f", point.x, point.y);
if (CGRectContainsPoint(self.testBtn.layer.frame, point))
return NO;
return YES;
}
ps.
you should import QuartzCore.h to access layer's attributes.
#import <QuartzCore/QuartzCore.h>
Just use the property "cancelsTouchesInView" (NO) on UITapGestureRecognizer tap
Example:
UITapGestureRecognizer *gesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(didTap)];
gesture.cancelsTouchesInView = NO;
[self.view addGestureRecognizer:gesture];
Override your button parent view pointinside method, setting a new custom frame size that contains your button.
I have a uiview at the top of the interface (below the status bar) that only the bottom part of it is shown.
Actually, I want to make the red uiview to slide down to be entirely shown by drag such as the notificationcenter in the native iOS and not just by taping a button.
What should I use to "touch and pull down" the uiview so it could be shown entirely ?
No needs to find a workaround of drag-n-drop. An UIScrollView can do it without any performance loss brought by listening on touches.
#interface PulldownView : UIScrollView
#end
#implementation PulldownView
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (!self) {
return self;
}
self.pagingEnabled = YES;
self.bounces = NO;
self.showsVerticalScrollIndicator = NO;
[self setBackgroundColor:[UIColor clearColor]];
double pixelsOutside = 20;// How many pixels left outside.
self.contentSize = CGSizeMake(320, frame.size.height * 2 - pixelsOutside);
// redArea is the draggable area in red.
UIView *redArea = [[UIView alloc] initWithFrame:frame];
redArea.backgroundColor = [UIColor redColor];
[self addSubview:redArea];
return self;
}
// What this method does is to make sure that the user can only drag the view from inside the area in red.
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
if (point.y > height)
{
// Leaving useless touches to the views under it.
return nil;
}
return [super hitTest:point withEvent:event];
}
#end
How to use:
1. Initialize an instance of PulldownView.
2. Add any content you want to display to the instance using [addSubview:].
3. Hide the area in red.
[pulldownView setContentOffset:CGPointMake(0, heightOfTheView - pixelsOutside)];
This is a simple example. You can add any features to it like adding a titled button bar on the bottom of the draggable area to implement click-n-drop, or adding some method to the interface to reposition it by the caller.
Make a subclass of UIView.
Override touchesBegan:withEvent and touchesMoved:withEvent.
In the touchesBegan perhaps make a visual change so the user knows they are touching the view.
In the touchesMoved use
[[touches anyObject] locationInView:self]
and
[[touches anyObject] previousLocationInView:self]
to calculate the difference between the current touch position and the last touch position (detect drag down or drag back up).
Then if you're custom drawing, call [self setNeedsDisplay] to tell your view to redraw in it's drawRect:(CGRect)rect method.
Note: this assumes multiple touch is not used by this view.
Refer to my answer in iPhone App: implementation of Drag and drop images in UIView
You just need to use TouchesBegin and TouchesEnded methods. In that example, I have shown how to use CGPoint, Instead of that you have to try to use setFrame or drawRect for your view.
As soon as TouchesMoved method is called you have to use setFrame or drawRect (not sure but which ever works, mostly setFrame) also take the height from CGPoint.
For some reason, the button initialized in the addBook method of my viewController won't respond to touches. The selector I've assigned to it never triggers, nor does the UIControlStateHighlighted image ever appear when tapping on the image.
Is there something intercepting touches before they get to the UIButton, or is its interactivity somehow disabled by what I'm doing to it?
- (void)viewDidLoad {
...
_scrollView.contentSize = CGSizeMake(currentPageSize.width, currentPageSize.height);
_scrollView.showsHorizontalScrollIndicator = NO;
_scrollView.showsVerticalScrollIndicator = NO;
_scrollView.scrollsToTop = NO;
_scrollView.pagingEnabled = YES;
...
}
- (void)addBook {
// Make a view to anchor the UIButton
CGRect frame = CGRectMake(0, 0, currentPageSize.width, currentPageSize.height);
UIImageView* bookView = [[UIImageView alloc] initWithFrame:frame];
// Make the button
frame = CGRectMake(100, 50, 184, 157);
UIButton* button = [[UIButton alloc] initWithFrame:frame];
UIImage* bookImage = [UIImage imageNamed:kBookImage0];
// THIS SECTION NOT WORKING!
[button setBackgroundImage:bookImage forState:UIControlStateNormal];
UIImage* bookHighlight = [UIImage imageNamed:kBookImage1];
[button setBackgroundImage:bookHighlight forState:UIControlStateHighlighted];
[button addTarget:self action:#selector(removeBook) forControlEvents:UIControlEventTouchUpInside];
[bookView addSubview:button];
[button release];
[bookView autorelease];
// Add the new view/button combo to the scrollview.
// THIS WORKS VISUALLY, BUT THE BUTTON IS UNRESPONSIVE :(
[_scrollView addSubview:bookView];
}
- (void)removeBook {
NSLog(#"in removeBook");
}
The view hierarchy looks like this in Interface Builder:
UIWindow
UINavigationController
RootViewController
UIView
UIScrollView
UIPageControl
and presumably like this once the addBook method runs:
UIWindow
UINavigationController
RootViewController
UIView
UIScrollView
UIView
UIButton
UIPageControl
THe UIScrollView might be catching all the touch events.
Maybe try a combination of the following:
_scrollView.delaysContentTouches = NO;
_scrollView.canCancelContentTouches = NO;
and
bookView.userInteractionEnabled = YES;
try to remove the [bookView autorelease]; and do it like this:
// Add the new view/button combo to the scrollview.
// THIS WORKS VISUALLY, BUT THE BUTTON IS UNRESPONSIVE :(
[_scrollView addSubview:bookView];
[bookView release];
_scrollView.canCancelContentTouches = YES; should do the trick
delaysContentTouches - is a Boolean value that determines whether the scroll view delays the handling of touch-down gestures. If the value of this property is YES, the scroll view delays handling the touch-down gesture until it can determine if scrolling is the intent. If the value is NO , the scroll view immediately calls touchesShouldBegin:withEvent:inContentView:. The default value is YES.
canCancelContentTouches - is a Boolean value that controls whether touches in the content view always lead to tracking. If the value of this property is YES and a view in the content has begun tracking a finger touching it, and if the user drags the finger enough to initiate a scroll, the view receives a touchesCancelled:withEvent: message and the scroll view handles the touch as a scroll. If the value of this property is NO, the scroll view does not scroll regardless of finger movement once the content view starts tracking.