I have, for example, a UIButton that I placed in IB.
This UIButton has two functions depending on whether it is just touched up inside or dragged outside - it would be nice to at least keep it functioning for touch up inside.
I want to click on my button and 'drag outside' but use -touchesEnded over a rectangle when I finally release my click.
I know that one option is to ditch the UIButton and use -touchesBegan over a mask, but that is a lot more effort and requires a nice big rewrite (considering the 96 buttons).
So to rephrase slightly - I've clicked on a button and held down the mouse, while dragging the cursor (/finger) over to a different point in my view and I want my view to recognise a -touchesEnded when I finally let go. The problem is that the initial click is tied up in the button so -touchesEnded in my main view doesn't happen...
Am i trying to do this the wrong way? Also, am I being clear?
EDIT: #Heckman has phrased the question well below
If I understand your question correctly, you are touching one button, that is contained within some UIView, and want to be able to drag your finger out, and recognize the point in the buttons superview.
Sounds to me like you should detect the button press from the button, then make a call to the [UIButton superview], to tell the superview about the button pressing that started, and then handle the touch end within the view.
To notify the superview of the button that was pressed call something like (from your UIButton):
-(void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
[self.superview buttonPressed:self]
}
-(void) touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
[self.superview touchesEnded:touches withEvent:event];
}
In your superview code (if you have a property buttonPressed):
-(void) buttonPressed:(UIButton *) button {
_buttonPressed = button;
}
-(void) touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
if(self.buttonPressed) {
//Process the release from your drag here.
for(UITouch * touch in touches) {
//Use either of these to get the release position that you care about.
CGPoint releaseLocationInCurrentView = [touch locationInView:self];
CGPoint releaseLocationInButtonView = [touch locationInView:[self.buttonPressed]];
}
}
}
You might be able to use a UIPanGestureRecognizer on your UIView.
Here is some example code for how to implement gesture recognizers.
If you are looking for something like dragable items same as iphone homescreen icons then you can have a look to TTLauncherView sample from Three20 framework. http://three20.info/showcase/launcher
So, here's what I ended up doing, and it works quite nicely:
my UIButton on touchdragexit is assigned to an IBAction
-(IBAction)dragPoint:(id)sender {
//Do Something Useful in here - I assign the value to an Array that I can recall when I touch up inside one of 16 buttons
//Now pass the button touch on as a touch up outside
[myButton addTarget:self action:#selector(dragBegan:withEvent:) forControlEvents: UIControlEventTouchUpOutside];
}
so now I get my dragBegan like this:
- (void)dragBegan:(UIControl *)c withEvent:ev {
UITouch *touch = [[ev allTouches] anyObject];
CGPoint touchPoint = [touch locationInView:self.view]; // Get location of the moment I release the touch (touch up outside)
//now some long convoluted code to give me the location of a row of 16 buttons, of which I would like to know which one I touched up inside
UIButton *posButton;
int pos;
if(628.00<(touchPoint.y) && (touchPoint.y)<664.00){
if ((39.00<touchPoint.x && touchPoint.x<91.00) || (92.00<touchPoint.x && touchPoint.x<144.00) || (145.00<touchPoint.x && touchPoint.x<197.00) || (198.00<touchPoint.x && touchPoint.x<250.00)||(264.00<(touchPoint.x) && touchPoint.x<314.00)||(317.00<(touchPoint.x) && touchPoint.x<367.00)||(370.00<(touchPoint.x) && touchPoint.x<420.00)||(423.00<(touchPoint.x) && touchPoint.x<473.00)||(488.00<(touchPoint.x) && touchPoint.x<538.00) || (541.00<(touchPoint.x) && touchPoint.x<591.00) || (594.00<(touchPoint.x) && touchPoint.x<644.00) || (647.00<(touchPoint.x) && touchPoint.x<697.00)||(712.00<(touchPoint.x) && touchPoint.x<762.00)||(765.00<(touchPoint.x) && touchPoint.x<815.00)||(818.00<(touchPoint.x) && touchPoint.x<868.00)||(871.00<(touchPoint.x) && touchPoint.x<921.00)){
if(39.00<(touchPoint.x) && touchPoint.x<91.00){
posButton = SeqA;
pos=0;
}
else if(92.00<(touchPoint.x) && touchPoint.x<144.00){
posButton = SeqB;
pos=1;
}
else if(145.00<(touchPoint.x) && touchPoint.x<197.00){
posButton = SeqC;
pos=2;
}
else if(198.00<(touchPoint.x) && touchPoint.x<250.00){
posButton = SeqD;
pos=3;
}
else if(264.00<(touchPoint.x) && touchPoint.x<314.00){
posButton = SeqE;
pos=4;
}
else if(317.00<(touchPoint.x) && touchPoint.x<367.00){
posButton = SeqF;
pos=5;
}
else if(370.00<(touchPoint.x) && touchPoint.x<420.00){
posButton = SeqG;
pos=6;
}
else if(423.00<(touchPoint.x) && touchPoint.x<473.00){
posButton = SeqH;
pos=7;
}
else if(488.00<(touchPoint.x) && touchPoint.x<538.00){
posButton = SeqI;
pos=8;
}
else if(541.00<(touchPoint.x) && touchPoint.x<591.00){
posButton = SeqJ;
pos=9;
}
else if(594.00<(touchPoint.x) && touchPoint.x<644.00){
posButton = SeqK;
pos=10;
}
else if(647.00<(touchPoint.x) && touchPoint.x<697.00){
posButton = SeqL;
pos=11;
}
else if(712.00<(touchPoint.x) && touchPoint.x<762.00){
posButton = SeqM;
pos=12;
}
else if(765.00<(touchPoint.x) && touchPoint.x<815.00){
posButton = SeqN;
pos=13;
}
else if(818.00<(touchPoint.x) && touchPoint.x<868.00){
posButton = SeqO;
pos=14;
}
else if(871.00<(touchPoint.x) && touchPoint.x<921.00){
posButton = SeqP;
pos=15;
}
Now do something depending on the button you touched up inside
Other things I did - I created a mask of the original UIButton as a UIImageView that I made unhidden on a touchdragoutside event (and rebidden on the above touchupinside) so that I have a button I can drag
I have a custom UIButton with UILabel added as subview. Button perform given selector only when I touch it about 15points lower of top bound. And when I tap above that area nothing happens.
I found out that it hasn't caused by wrong creation of button and label, because after I shift the button lower at about 15 px it works correctly.
UPDATE I forgot to say that button located under the UINavigationBar and 1/3 of upper part of the button don't get touch events.
Image was here
View with 4 buttons is located under the NavigationBar. And when touch the "Basketball" in top, BackButton get touch event, and when touch "Piano" in top, then rightBarButton (if exists) get touch. If not exists, nothing happened.
I didn't find this documented feature in App docs.
Also I found this topic related to my problem, but there is no answer too.
I noticed that if you set userInteractionEnabled to OFF, the NavigationBar doesn't "steal" the touches anymore.
So you have to subclass your UINavigationBar and in your CustomNavigationBar do this:
-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
if ([self pointInside:point withEvent:event]) {
self.userInteractionEnabled = YES;
} else {
self.userInteractionEnabled = NO;
}
return [super hitTest:point withEvent:event];
}
Info about how to subclass UINavigationBar you can find here.
I found out the answer here(Apple Developer Forum).
Keith at Apple Developer Technical Support, on 18th May 2010 (iPhone OS 3):
I recommend that you avoid having touch-sensitive UI in such close proximity to the nav bar or toolbar. These areas are typically known as "slop factors" making it easier for users to perform touch events on buttons without the difficulty of performing precision touches. This is also the case for UIButtons for example.
But if you want to capture the touch event before the navigation bar or toolbar receives it, you can subclass UIWindow and override:
-(void)sendEvent:(UIEvent *)event;
Also I found out,that when I touch the area under the UINavigationBar, the location.y defined as 64,though it was not.
So I made this:
CustomWindow.h
#interface CustomWindow: UIWindow
#end
CustomWindow.m
#implementation CustomWindow
- (void) sendEvent:(UIEvent *)event
{
BOOL flag = YES;
switch ([event type])
{
case UIEventTypeTouches:
//[self catchUIEventTypeTouches: event]; perform if you need to do something with event
for (UITouch *touch in [event allTouches]) {
if ([touch phase] == UITouchPhaseBegan) {
for (int i=0; i<[self.subviews count]; i++) {
//GET THE FINGER LOCATION ON THE SCREEN
CGPoint location = [touch locationInView:[self.subviews objectAtIndex:i]];
//REPORT THE TOUCH
NSLog(#"[%#] touchesBegan (%i,%i)", [[self.subviews objectAtIndex:i] class],(NSInteger) location.x, (NSInteger) location.y);
if (((NSInteger)location.y) == 64) {
flag = NO;
}
}
}
}
break;
default:
break;
}
if(!flag) return; //to do nothing
/*IMPORTANT*/[super sendEvent:(UIEvent *)event];/*IMPORTANT*/
}
#end
In AppDelegate class I use CustomWindow instead of UIWindow.
Now when I touch area under navigation bar, nothing happens.
My buttons still don't get touch events,because I don't know how to send this event (and change coordinates) to my view with buttons.
Subclass UINavigationBar and add this method. It will cause taps to be passed through unless they are tapping a subview (such as a button).
-(UIView*) hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
UIView *v = [super hitTest:point withEvent:event];
return v == self? nil: v;
}
The solution for me was the following one:
First:
Add in your application (It doesn't matter where you enter this code) an extension for UINavigationBar like so:
The following code just dispatch a notification with the point and event when the navigationBar is being tapped.
extension UINavigationBar {
open override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "tapNavigationBar"), object: nil, userInfo: ["point": point, "event": event as Any])
return super.hitTest(point, with: event)
}
}
Then in your specific view controller you must listen to this notification by adding this line in your viewDidLoad:
NotificationCenter.default.addObserver(self, selector: #selector(tapNavigationBar), name: NSNotification.Name(rawValue: "tapNavigationBar"), object: nil)
Then you need to create the method tapNavigationBar in your view controller as so:
func tapNavigationBar(notification: Notification) {
let pointOpt = notification.userInfo?["point"] as? CGPoint
let eventOpt = notification.userInfo?["event"] as? UIEvent?
guard let point = pointOpt, let event = eventOpt else { return }
let convertedPoint = YOUR_VIEW_BEHIND_THE_NAVBAR.convert(point, from: self.navigationController?.navigationBar)
if YOUR_VIEW_BEHIND_THE_NAVBAR.point(inside: convertedPoint, with: event) {
//Dispatch whatever you wanted at the first place.
}
}
PD: Don't forget to remove the observation in the deinit like so:
deinit {
NotificationCenter.default.removeObserver(self)
}
That's it... That's a little bit 'tricky', but it's a good workaround for not subclassing and getting a notification anytime the navigationBar is being tapped.
I just wanted to share another prospective to solving this problem. This is not a problem by design, but it was meant to help user get back or navigate. But we need to put things tightly in or below nav bar and things look sad.
First lets look at the code.
class MyNavigationBar: UINavigationBar {
private var secondTap = false
private var firstTapPoint = CGPointZero
override func pointInside(point: CGPoint, withEvent event: UIEvent?) -> Bool {
if !self.secondTap{
self.firstTapPoint = point
}
defer{
self.secondTap = !self.secondTap
}
return super.pointInside(firstTapPoint, withEvent: event)
}
}
You might be see why am i doing second touch handling. There is the recipe to the solution.
Hit test is called twice for a call. The first time the actual point on the window is reported. Everything goes well. On the second pass, this happens.
If system sees a nav bar and the hit point is around 9 pixels more on Y side, it tries to decrease that gradually to below 44 points which is where the nav bar is.
Take a look at the screen to be clear.
So theres a mechanism that will use nearby logic to the second pass of hittest. If we can know its second pass and then call the super with first hit test point. Job done.
The above code does that exactly.
There are 2 things that might be causing problems.
Did you try setUserInteractionEnabled:NO for the label.
Second thing i think might work is apart from that after adding label on top of button you can send the label to back (it might work, not sure although)
[button sendSubviewToBack:label];
Please let me know if the code works :)
Your labels are huge. They start at {0,0} (the left top corner of the button), extend over the entire width of the button and have a height of the entire view. Check your frame data and try again.
Also, you have the option of using the UIButton property titleLabel. Maybe you are setting the title later and it goes into this label rather than your own UILabel. That would explain why the text (belonging to the button) would work, while the label would be covering the rest of the button (not letting the taps go through).
titleLabel is a read-only property, but you can customize it just as your own label (except perhaps the frame) including text color, font, shadow, etc.
This solved my problem..
I added hitTest:withEvent: code to my navbar subclass..
-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
int errorMargin = 5;// space left to decrease the click event area
CGRect smallerFrame = CGRectMake(0 , 0 - errorMargin, self.frame.size.width, self.frame.size.height);
BOOL isTouchAllowed = (CGRectContainsPoint(smallerFrame, point) == 1);
if (isTouchAllowed) {
self.userInteractionEnabled = YES;
} else {
self.userInteractionEnabled = NO;
}
return [super hitTest:point withEvent:event];
}
Extending Alexander's solution:
Step 1. Subclass UIWindow
#interface ChunyuWindow : UIWindow {
NSMutableArray * _views;
#private
UIView *_touchView;
}
- (void)addViewForTouchPriority:(UIView*)view;
- (void)removeViewForTouchPriority:(UIView*)view;
#end
// .m File
// #import "ChunyuWindow.h"
#implementation ChunyuWindow
- (void) dealloc {
TT_RELEASE_SAFELY(_views);
[super dealloc];
}
- (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event {
if (UIEventSubtypeMotionShake == motion
&& [TTNavigator navigator].supportsShakeToReload) {
// If you're going to use a custom navigator implementation, you need to ensure that you
// implement the reload method. If you're inheriting from TTNavigator, then you're fine.
TTDASSERT([[TTNavigator navigator] respondsToSelector:#selector(reload)]);
[(TTNavigator*)[TTNavigator navigator] reload];
}
}
- (void)addViewForTouchPriority:(UIView*)view {
if ( !_views ) {
_views = [[NSMutableArray alloc] init];
}
if (![_views containsObject: view]) {
[_views addObject:view];
}
}
- (void)removeViewForTouchPriority:(UIView*)view {
if ( !_views ) {
return;
}
if ([_views containsObject: view]) {
[_views removeObject:view];
}
}
- (void)sendEvent:(UIEvent *)event {
if ( !_views || _views.count == 0 ) {
[super sendEvent:event];
return;
}
UITouch *touch = [[event allTouches] anyObject];
switch (touch.phase) {
case UITouchPhaseBegan: {
for ( UIView *view in _views ) {
if ( CGRectContainsPoint(view.frame, [touch locationInView:[view superview]]) ) {
_touchView = view;
[_touchView touchesBegan:[event allTouches] withEvent:event];
return;
}
}
break;
}
case UITouchPhaseMoved: {
if ( _touchView ) {
[_touchView touchesMoved:[event allTouches] withEvent:event];
return;
}
break;
}
case UITouchPhaseCancelled: {
if ( _touchView ) {
[_touchView touchesCancelled:[event allTouches] withEvent:event];
_touchView = nil;
return;
}
break;
}
case UITouchPhaseEnded: {
if ( _touchView ) {
[_touchView touchesEnded:[event allTouches] withEvent:event];
_touchView = nil;
return;
}
break;
}
default: {
break;
}
}
[super sendEvent:event];
}
#end
Step 2: Assign ChunyuWindow instance to AppDelegate Instance
Step 3: Implement touchesEnded:widthEvent: for view with buttons, for example:
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
[super touchesEnded: touches withEvent: event];
UITouch *touch = [touches anyObject];
CGPoint point = [touch locationInView: _buttonsView]; // a subview contains buttons
for (UIButton* button in _buttons) {
if (CGRectContainsPoint(button.frame, point)) {
[self onTabButtonClicked: button];
break;
}
}
}
Step 4: call ChunyuWindow's addViewForTouchPriority when the view we care about appears, and call removeViewForTouchPriority when the view disappears or dealloc, in viewDidAppear/viewDidDisappear/dealloc of ViewControllers, so _touchView in ChunyuWindow is NULL, and it is the same as UIWindow, having no side effects.
An alternative solution that worked for me, based on the answer provided by Alexandar:
self.navigationController?.barHideOnTapGestureRecognizer.enabled = false
Instead of overriding the UIWindow, you can just disable the gesture recogniser responsible for the "slop area" on the UINavigationBar.
Give a extension version according to Bart Whiteley. No need to subclass.
#implementation UINavigationBar(Xxxxxx)
- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
UIView *v = [super hitTest:point withEvent:event];
return v == self ? nil: v;
}
#end
The following worked for me:
self.navigationController?.isNavigationBarHidden = true
guys, I want to set actions to touches. If people make single touch - one action, else - different.
I've written this code in my touchesBegan method:
UITouch *touch = [event.allTouches anyObject];
BOOL tappedTwice = NO;
if ([touch tapCount] == 2) {
tappedTwice = YES;
NSLog(#"double touch");
}
else if ([touch tapCount] == 1 && !tappedTwice) {
NSLog(#"single touch");
}
But it's detecting single touch, and after it double, but it's incorrect in my situation. Have you any ideas?
Check this link. Just Configure number of tap required by setting
[tapGestureRecognizer setNumberOfTapsRequired:2];
and then handle this method
- (void)handleTap:(UITapGestureRecognizer *)sender {
if (sender.state == UIGestureRecognizerStateEnded) {
// handling code
}
}
I want to make something happen with ccTouchesBegan but i want to restrict that action to a button, meaning that the action by the button should trigger ccTouchesBegan but only one time. After the code inside ccTouchesBegan has finished, the interface should go back to normal and wait for the button to be pressed to trigger the action again.
I have made the button trigger ccTouchesBegan when its pressed but the problem is that once the button is pressed, the code inside ccTouchesBegan keeps working and doing the same thing from then on when a touch action on the simulator is done.
This is the code that i have so far.
this is a flag method that i have created so that i know the button has been pressed and i can control the actions on ccTouchesBegan.
- (void) selector{
click = true;
}
this is the button which calls the method selector.
- (void) button{
CCMenuItemImage *touchesMovedButton = [CCMenuItemImage itemFromNormalImage:#"ActionButton-Normal.png"
selectedImage:#"ActionButton-Selected.png"
target:self
selector:#selector(selector)
];
CCMenu *selectorButton = [CCMenu menuWithItems: touchesMovedButton, nil];
selectorButton.position = ccp(64, 64);
[self addChild: selectorButton];
}
and this is the method ccTouchesBegan
- (void)ccTouchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [touches anyObject];
//this part doesn't let the interface start the ccTouchesBegan code until the button has been pressed.
if (click == false) {
return;
}
// the else code starts when the button has been pressed.
else{
CCLOG(#"you have touched the interface!!");
}
}
That's the code, the interface doesn't do any action until the button is pressed but after it's pressed it keeps printing the CCLOG each time i touch the interface. i just want it to do it once and then when i press the button again it should do it just one time again.
does anyone know how to do this? or maybe point out my mistake?
You simply have the logical mistake. Use the Code below in the ccTouchesBegan It would work perfectly as you need...
if (click == false)
{
return;
}
// the else code starts when the button has been pressed.
else if(click == true)
{
CCLOG(#"you have touched the interface!!");
click=false; // You need to make false if you want to make touch single time enabled
}
I am using - (void) touchesMoved to do stuff when ever I enter a specific frame, in this case the area of a button.
My problem is, I only want it to do stuff when I enter the frame - not when I am moving my finger inside the frame.
Does anyone know how I can call my methods only once while I am inside the frame, and still allow me to call it once again if I re-enter it in the same touchMove.
Thank you.
-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [[event touchesForView:self.view] anyObject];
CGPoint location = [touch locationInView:touch.view];
if(CGRectContainsPoint(p1.frame, location))
{
//I only want the below to me called
// once while I am inside this frame
[self pP01];
[p1 setHighlighted:YES];
}else {
[p1 setHighlighted:NO];
}
}
You can use some attribute to check if the code was already called when you were entering specific area. It looks like highlighted state of p1 object (not sure what it is) may be appropriate for that:
if(CGRectContainsPoint(p1.frame, location))
{
if (!p1.isHighlighted){ // We entered the area but have not run highlighting code yet
//I only want the below to me called
// once while I am inside this frame
[self pP01];
[p1 setHighlighted:YES];
}
}else { // We left the area - so we'll call highlighting code when we enter next time
[p1 setHighlighted:NO];
}
Simply add a BOOL that you check in touchesMoved and reset in touchesEnded
if( CGRectContainsPoint([p1 frame],[touch locationInView:self.view])) {
NSLog (#"Touch Moved over p1");
if (!p14.isHighlighted) {
[self action: p1];
p1.highlighted = YES;
}
}else {
p1.highlighted = NO;
}
try using a UIButton and use the 'touch drag enter' connection in Interface Builder.