In iOS, I would like to receive touchesMoved events in a subview even when the touch began i the super view! Ideally, I would also like to receive touchesBegan when the moving finger reaches the subview.
What is the best way to achieve this?
I guess I could override touchesMoved:withEvent: in the superview, do a hit-test and then call the subview, but is there a better solution? (I have tried to resign first responder in the superview but that did not help)
I had to solve this exact same problem today, and I just did.
Say, you have the following -- this is an abstract description of exactly my case:
Main view containing a button and a sub view
When touches begin on the main view you want to receive the touch events on the sub view
One exception is that button: if you tap the button in the main view, then the button should handle that
The solution is to override the -hitTest:withEvent: method. The purpose of that method is to return the view that should handle the touches.
You have two options: override it in the sub view or override it in the main view. If you want ALL touches to be handled by the sub view, you can just have the -hitTest:withEvent on the sub view return self and be done with it. In my case I still needed the button on the main view to have its own touch detection, so my fix was to override the -hitTest:withEvent: method on the main view instead, as follows:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
if (CGRectContainsPoint(self.myButton.frame, point))
{
return self.myButton;
}
else
{
return self.mySubView;
}
}
This works like an absolute charm! Hope this works for you as well :-)
You should of course modify the details inside -hitTest:withEvent: to suit your specific needs -- maybe you want to limit this override to a specific region or a specific set of controls -- but the point is that the answer to the main question on this thread lies in the override of this -hitTest:withEvent: method.
Erik
Related
I have a scrollview and a subview inside it. If the user does a certain touch in that subview, I want to prevent the scrollview to move, in order to let the user do their stuff in the subview without being bothered by the scrollview movement. If the certain touch is not detected, then the scrollview should move normally .
I intercept these gestures with raw touch events touchesBegan, touchesMoved. I don't use gestureRecognizers because the gestures I want to recognise are very specifics and I feel more confortable using no abstraction layer to recognise them.
I know after seeing many answers on SO, I could just hold a reference of the scrollview behind and stop its movement if I detect the gesture. I'm looking for a more stable solution. I want to stop the propagation of the event (to any view behind), if I detect the gesture, without having to hold the references of any of these views behind.
As I understood, view in iOS are subclasses of UIResponder. When UIKit detects a touch on the screen, it gives the event to the first responder, which is generally the top most subview. My question is : in touchesBegan how to tell UIKit : "Do not send the event to any other following view in the responder chain". If I can see the scrollview moving behind, UIKit must have forwarded the event to it (despite I'm not calling super in touchesBegan)
In Android for example, onTouchEvent function of View class returns a bool. false tells android to continue propagating the event, true tells to stop propagating. I'm looking for the same mechanism in iOS:
#Override
public boolean onTouchEvent(MotionEvent e)
{
return true ; // stops propagation
}
In Javascript (jQuery), there's quite the same mechanism :
$('#myview').bind('click', function(e) {
e.stopPropagation()
})
How to do that in Swift ?
Found the solution. This was tricky to understand so I'll try to make it easy if anyone has the same problem :
As stated in another SO answer, the "raw touch system" ( touchesBegen, touchesMoved ..) and the "gesture recognizer system" are mutually exclusive, both of them are actually at the "raw" view level, and there are independent.
This means that when you have a view and you touch it, you have a chance that your touch is handled by the gestureRecognizer system instead of the raw touch system. overriding next UIResponder property by override var next:UIResponder? { get { return nil }} only force UIKit not to forward the event in the raw touch system, the gesture recognition of views behind are still fired, because it's a system completely apart.
In my case, I tried to override var next:UIResponder? { get { return nil }} : the touchesBegan of the views behind remained quiet as expected, but I could still recognize gesture there.
So, it appears that UIScrollView uses gestureRecognizer to handle user touches. The solution is to shut down the gesture recognizer system from your top most view so the gesture is not forwarded : this can be done using :
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool
{
return !yourConditionToShutDownGR
}
I was confused because the Android gesture "detector" system is built on top of the raw touch system. When you catch a touch event in public boolean onTouchEvent(MotionEvent event) you can pass it as a parameter of a gestureDetector, which returns if it found a gesture or not. That's not the same approach for iOS in which the gestureRecognizer system is built aside of the raw touch system.
I have a UIViewController and I am adding subviews to the main view. The main view has a UIGestureRecogniser and each subview also has a UIGestureRecognizer. In interface builder I have checked canceled in view in the attributes inspector so I would expect taps on the subviews to only fire my gestureRecognizerShouldBegin in the subview handler but they are also firing the handler attached to the view they are place on.
Does anyone have any idea why this might be the case?
I guess I could keep a handle to the particular instance of the GestureRecogniser attached to each subview and compare if the event being fired has come from the correct recogniser but this does not seem like the correct solution. Any help would be much appricated.
Edit: This second solution doesn't even work seems like every UIGestureRecogniser conforming to the current gesture is being fired!
Edit:
The best solution I have come up with is,
(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch{
if([view isEqual:[touch view]] && self.editing)
return YES;
return NO;
}
I would like the Gesture not to receive the touch at all though
It appears that all the touch methods of a UIView are only called if the touches began within the bounds of that view. Is there a way to have a view respond to a user who has touched outside the view, but then dragged his fingers into the view?
In case it matters, my specific application is for dragging a MKPinAnnotationView (using built-in 4.0 dragging). I want something to happen if the user drags a pin onto another view (which happens to be an AnnotationView as well, but it could be anything). No method for dragging is called until I let go of the pin; and no method no the UIView that's being dragged to seems to be called unless I started by touching from within the view.
Because the superview is a MKMapView, it is difficult to just use the touchesMoved event of that and check if the user is in the right location or not. Thanks!
So after playing around with it for a while, I found that the answer given here actually gave me what I needed, even though the question being asked was different.
It turns out you can subclass UIGestureRecognizer; and have it handle all the touches for the view that it has been added to (including an MKMapView). This allows all the normal MKMapView interactions to still behave without any problem; but also alerts me of the touches. In touchesMoved, I just check the location of the touch; and see if it is within the bounds of my other view.
From everything I tried; this seems to be the only way to intercept touchesMoved while the user is dragging an MKAnnotation.
You sure can:
(HitstateView.h)
#import <UIKit/UIKit.h>
#interface HitstateView : UIView {
id overrideObject;
}
#property (nonatomic, retain) id overrideObject;
#end
(HitstateView.m)
#import "HitstateView.h"
#implementation HitstateView
#synthesize overrideObject;
- (void)dealloc {
self.overrideObject = nil;
[super dealloc];
}
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
UIView *hitView = [super hitTest:point withEvent:event];
if (hitView == self) {
return overrideObject;
}
return hitView;
}
#end
Make this view the size of your touch area. Set the overideObject to the view you want the touches to go. IIRC it ought to be a subview of the HitstateView.
Every view inherits UIResponder so every view gets touchesBegan/Moved/Ended - I do not think starting the touch outside the view means the view gets no event when the touch moves over the view. If you want to get a notification that something has been dragged onto your MKMapView you should make a subclass that handles the touch but then passes the event to super, allowing the hierarchy to do whatever it needs to do with the touch. You don't need to capture or modify the event just observe it.
It depends on how your views are set up. Generally leveraging the responder chain is the best way to go. It allows you to play tricks, though it may be too specific to address your particular needs.
You can also play tricks with forward events by override hit testing:
http://developer.apple.com/library/ios/#documentation/EventHandling/Conceptual/EventHandlingiPhoneOS/MultitouchEvents/MultitouchEvents.html%23//apple_ref/doc/uid/TP40009541-CH3-SW3
Your particular case sounds pretty exotic, so you may have to play tricks like having a parent view whose frame is large enough to contain both views in question.
So when I see ccTouchesBegan (or touchesBegan for that fact of the matter) I see something like this usually:
- (void)ccTouchesBegan:(NSSet*)touches withEvent:(UIEvent*)event
{
UITouch* touch = [touches anyObject];
}
But, what I am not getting is how do you just detect if one object has been touched? For example how would I check if a specific CCSprite I declared has been touched?
Sorry if this is a newbish question but, I just don't understand and if you need more clarification just ask how I can clarify myself more.
I'm not familiar with cocoas2d but in the standard API it sends the touches first to the view touched and then up the view responder chain to a view that has a controller. If that controller does not handle the touch then it goes up to the next view until it ends up at the Window object.
See Responder Objects in the Responder Chain
The best place to trap touches for specific objects is in the object themselves. In the case of sprite-like view, the sprite itself most likely needs to respond to the touch e.g. by moving itself. If you need the touch to be communicated to another object, you should use the delegate pattern so that the sprite can tell its delegate how its been touched.
That last sentence sounded weird.
I don't have the samples in front of me but there should be an example in the Cocos2D download package which demonstrates a touch event and how it propagates down to sprites.
I'm not sure why this is so hard. I have a UISearchBar at the top of my page. When the user types into that UISearchBar, it will automatically filter the results in the UITableView that is below the search bar. There is no need for a Search button since I search when keys are pressed.
The flow of the application is that the user types a search in the search bar (with the keyboard being displayed) and then will scroll the results in the table view - at which point the keyboard needs to disappear.
I'm having a hard time getting the keyboard to disappear.
I know I need to do:
[searchBar resignFirstResponder];
to get the keyboard to disappear but I can't find what delegate I need to perform this on. I want to do this as soon as the user touches the table view.
Any ideas?
Try this:
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
[searchBar resignFirstResponder];
}
It worked for me
I think the easiest (and best) way to do this is to subclass your global view and use hitTest:withEvent method to listen to any touch. Touches on keyboard aren't registered, so hitTest:withEvent is only called when you touch/scroll/swipe/pinch... somewhere else, then call [self endEditing:YES].
This is better than using touchesBegan because touchesBegan are not called if you click on a button on top of the view. It is also better than using a dim screen because in a complexe and dynamic user interface, you can't put dim screen every where. Also, it's better than calling [searchBar resignFirstResponder], because you may have many text fields on screen, so this works for all of them.
You might want to do it as Cydia (jailbroken packaging UI) does it - there is a search button, and when you press the search button, it closes the keyboard. The results are still filtered as you type for a preview.
Since the results are going to be in a table view, use UIScrollView's delegate (to which UITableView responds) method - (void)scrollViewDidScroll:(UIScrollView *)scrollView.
You'll need to have your view controller respond to the UITableView's data source and delegate (in the .h file):
<UITableViewDataSource, UITableViewDelegate>
Than in your initialization method in the .m file add:
self.tableView.dataSource = self;
self.tableView.delegate = self;
Finally, add this method:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
[self.view endEditing:YES];
}
This will make the UISearchBar lose focus, and the keyboard will hide.
One possibility is that the UISearchBar belongs to a controller, that is displayed in UIModalPresentationFormSheet.
I had the same problem and found out that this is related to the method "disablesAutomaticKeyboardDismissal" of UIViewController.
The Apple documentation states the following:
Override this method in a subclass to allow or disallow the dismissal
of the current input view (usually the system keyboard) when changing
from a control that wants the input view to one that does not. Under
normal circumstances, when the user taps a control that requires an
input view, the system automatically displays that view. Tapping in a
control that does not want an input view subsequently causes the
current input view to be dismissed but may not in all cases. You can
override this method in those outstanding cases to allow the input
view to be dismissed or use this method to prevent the view from being
dismissed in other cases.
The default implementation of this method returns YES when the modal
presentation style of the view controller is set to
UIModalPresentationFormSheet and returns NO for other presentation
styles. Thus, the system normally does not allow the keyboard to be
dismissed for modal forms.
In my case, I displayed the search bar within the navigation bar, so I had to create a subclass of UINavigationViewController that overrides the method as follows:
- (BOOL)disablesAutomaticKeyboardDismissal {
return NO;
}
After that resignFirstResponder would finally make the keyboard disappear.
Might be super late to answer but this might help anyone in future. This particular piece of code worked for me. P.S. I don't own this solution, found some time ago on the net itself.
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [[event allTouches] anyObject];
if ([self.srchBar isFirstResponder] && [touch view] != self.srchBar)
{
[self.srchBar resignFirstResponder];
}
[super touchesBegan:touches withEvent:event];
}
Given a view with a searchBar variable, add this method to the view:
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView?
{
if searchBar.isFirstResponder && !searchBar.point(inside: point, with: event){
searchBar.resignFirstResponder()
}
return super.hitTest(point, with: event)
}