hitTest to dismiss keyboard causing weird behaviour - ios

I've googled on how to dismiss keyboard when touching a blank area in UITableView in iOS, and there're several ways to solve this. Like using delegate, UITapGestureRecognizer, - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event and - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event.
I decide to take hitTest by subclassing the corresponding UIView class and override this method like this:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
UIView *result = [super hitTest:point withEvent:event];
[self endEditing:YES];
return result;
}
This really works, will dismiss the virtual keyboard when I touch / scroll / swipe / pinch ... somewhere else, but another problem shows up.
The keyboard is active or shown when I touch one UITextField object, then I touch the same UITextField object, here is the problem, the keyboard try to dismiss but not completely, in the middle of somewhere it begin to show up, doing this kind of weird animation. Most of the case in our APPs, the keyboard should stay still when we touch the same UITextField object. Is there a nice and simple way to solve this problem?
Solved:
Finally, I figure it out myself. Thanks to #Wain, thanks to #Wain's hint. I do check the result before I invoke [self endEditing:YES];. Here is the modified code:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
UIView *result = [super hitTest:point withEvent:event];
// You can change the following condition to meet your own needs
if (![result isMemberOfClass:[UITextField class]] && ![result isMemberOfClass:[UITextView class]]) {
[self endEditing:YES];
}
return result;
}

As per #iBCode answer, if I select same text field multiple times then keyboard automatically hide and show immediately, to avoid that I have added one more condition and added code below in swift languagge
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if let resultView = super.hitTest(point, with: event) {
if resultView.isMember(of: UITextField.self) ||
resultView.isKind(of: UITextField.self) ||
resultView.isMember(of: UITextView.self) ||
resultView.isKind(of: UITextView.self) {
return resultView
}
if !resultView.isMember(of: UITextField.self) && !resultView.isMember(of: UITextView.self) {
endEditing(true)
}
return resultView
}
return nil
}

Check the class of the result so that you can limit when you end editing. If the hit target was an editable class (like a text field, or a switch) then don't end the editing session.

Related

Handling touchesBegan in the main view - how to get ahold of the touched subview?

In an single view iPhone app I have the following structure: scrollView -> contentView -> imageView and underneath (at the main view) there are 7 draggable custom views - which can be dragged from/to the contenView.
Initially dragging was handled in the custom views (see Tile.m#2eb672523a) and while this worked well it required sending notifications to the main view.
So I'm trying to move touchesBegan and friends to the main app view (see ViewController.m).
My problem is:
Initially I had this code in the contentView (see GameBoard.m) to enable subview dragging at the scrollView:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
UIView* result = [super hitTest:point withEvent:event];
return result == self ? nil : result;
}
However when trying to drag a tile back from contentView to the main view (in the direction shown in the below screenshot) this wouldn't deliver touchesBegan to the main view.
So I have changed this method to return the main view instead of the tiles:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
UIView* result = [super hitTest:point withEvent:event];
return result == self ? nil : self.window.rootViewController.view;
}
Now touchesBegan is called on the main view (as I can see in the debugger), but the touches.view is pointing to the main view too.
My question is:
How to find the touched tile in the new touchesBegan method?
Nevermind, I've solved my problem.
In ViewController.m I've added a _draggedTile ivar and set it in touchesBegan by using the following method to find the touched tile:
- (SmallTile*) findTileAtPoint:(CGPoint)point withEvent:(UIEvent*)event
{
NSArray* children = [self.view.subviews arrayByAddingObjectsFromArray:_contentView.subviews];
for (UIView* child in children) {
CGPoint localPoint = [child convertPoint:point fromView:self.view];
if ([child isKindOfClass:[SmallTile class]] &&
[child pointInside:localPoint withEvent:event]) {
return (SmallTile*)child;
}
}
return nil;
}
And in touchedMoved, touchesEnded, touchesCancelled I just use that ivar (initially I was using the above findTileAtPoint:withEvent: method there as well, but sometimes the touch wouldn't be exactly over the dragged tile and the movement was jerky or cancelled).

how to disregard touch events in topmost uiview when it is clear and a different uiview can handle them

I have a clear UIView which has gesture recognizers attached to it.
This clear uiview covers the entire super view to allow for the gestures to be invoked from anywhere on it.
Under this clear UIView sit different components such as tables,buttons,collectionview etc.
The clear UIView has no idea what is under it any time.
What I want - if a view which is under the clear uiview can handle a touch event (or any type of gesture) - the clear view should disregard that event - and the event will pass through to the underlying view which could handle it.
I tried
-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
but I don't know how to make sure the underlying view can handle it.
-(id)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
id hitView = [super hitTest:point withEvent:event];
if (hitView == self)
{
return nil;
}
else
{
return hitView;
}
}
Add this to your to clear view.
If the hit on clear view means just return nil.
You can override pointInside: withEvent: method. This method returns a boolean value indicating whether the receiver contains the specified point. So if we return NO then your upper clear view will become transparent for touch events and they will be passed to underlying views.
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
// Clear UIView will now respond to touch events if return NO:
return NO;
}
use below code for your case->
-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
UIView *hitTestView = [super hitTest:point withEvent:event];
if(hitTestView!=nil){
//check for gesture
if([hitTestView.gestureRecognizers count]>0)
return hitTestView;
//if it is subclass of UIControl like UIButton etc
else if([hitTestView isKindOfClass:[UIControl class]])
return hitTestView;
//if can handle touches
else if([hitTestView respondsToSelector:#selector(touchesBegan:withEvent:)])
return hitTestView;
else
return nil;
}
else{
return self;
}
}
In the above code if the subView which is hitView can anyway handle touch ,we return that object to handle that touch. If there is no such hitTest view, then we return the view itself.
I used some of these suggestions and used the following solution:
I added the gesture recognizer to the bottom most superview in the heirarchy (and not the top most)
Then in that class over rid
-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
UIView *v = [super hitTest:point withEvent:event];
// if v is nil then touch wasn't in this view or its subviews
if (v == nil)
{
return nil;
}
// in any case if the topview was hidden than return the default value
if (self.myTopView.hidden)
{
return v;
}
// if the view isn't hidden but the touch returned a control - than we can pass the touch to the control
if ([v isKindOfClass:[UIControl class]])
{
return v;
}
// decide on what threshold to decide is a touch
CGFloat threshHold = 40;
// if the touch wasn't on a control but could initiate a gesture than that view should get the touch
if (v.gestureRecognizers)
{
threshHold = 30;
// return v;
}
// check if the threshold should be bigger
if ([self someCondition])
{
threshHold = 100;
}
// threshold according to its position - this is the dynamic part
if (point.y > (self.myTopView.frame.origin.y - threshold))
{
return self.handleBarView;
}
return v;
}

not getting touches from UIScrollView [duplicate]

This question already has answers here:
UIScrollView touchesBegan
(8 answers)
Closed 9 years ago.
I have subclassed UIScrollView and implemented
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
method in it.
But Nothing happens when click or hold the scroll view!
Edit: I also need to use touchesEnded method
I think UIScrollView has two gesture recognizer. They are responsible for handling touch sequences, so they swallow the touch events.
Use scrollView delegate methods to handle drag gestures or the
touchesShouldBegin:withEvent:inContentView:
method to handle scrollview content touches and
touchesShouldCancelInContentView:
to cancel it.
As alternative you can manipulate the panGesture recognizer of the scrollView to pass the event to the next responser.
In your subclass of UIScrollView override the hitTest method like this:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
UIView *result = nil;
for (UIView *child in self.subviews)
if ([child pointInside:point withEvent:event])
if ((result = [child hitTest:point withEvent:event]) != nil)
break;
return result;
}
I think you are using scrollview as a subview.So in that case you can use gesture coz same problem i've face it.
You can do using UITapGestureRecognizer like this ...
-(void)viewDidLoad
{
UITapGestureRecognizer *gr = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(handleGesture:)];
gr.delegate = self;
[self.scrollView addGestureRecognizer:gr];
}
- (void)handleGesture:(UIGestureRecognizer *)gestureRecognizer
{
// do here whatever you wanna ..........
}
Solved!
Used LongPressedGestureRecognizer. The action method keeps getting called till the user holds the view. From that i can figure out the state of the gestureRecognizer (UIGestureRecognizerStateBegan, ...Ended, ...Cancelled)

Problems getting touch event on UIImageView in iOS 5

I have problems detecting touch events on my UIImageView. I have sub classed UIImageView and overrided canBecomeFirstResponder, touchedBegan and touchesEnded:
#implementation ImageViewNext
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
// Initialization code
self.userInteractionEnabled = YES;
}
return self;
}
-(BOOL)canBecomeFirstResponder
{
return YES;
}
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
NSLog(#"Touches Began event");
}
-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
NSLog(#"Touches Ended");
}
I have ensured that all the parent views have User Interaction Enabled.
But I don't get my events?
I have tried to find the solution here but without any luck. But in my search I found that some people use "UIGestureRecognizer" to get the same effect. So as a side question, what is the "right" solution use the override method or use UIGestureRecognizer? I don't like the solution where a normal button is given an image, so please keep those post away :)
Are you sure your initWithFrame: is called? UIViews have two designated initializers, initWithFrame: and initWithCoder, depending on how they are created.
I don't know why you're not getting the touch events, but to answer the other part of your question using a UIGestureRecognizer will significantly simplify your touch handling for any of the gestures for which there's a recognizer available. It's much simpler to use and you'll need less code.
Furthermore, if in the future you need to negotiate between different touch events (is it a tap of a drag type questions) then all that logic is handled trivially.

How can I respond to touch events on a UITextField?

I want to respond to touch events (specifically UIControlEventTouchUpInside and UIControlEventTouchUpOutside) on a UITextField, but the only event that comes through is UIControlEventTouchCancel.
I can't use UIControlEventEditingDidBegin, because it only occurs when field gains focus, which is not what I'm trying to detect. I want to know when the user lifts their finger, and whether it was inside or outside the text field, regardless of current focus state.
Does anyone know a way to accomplish this?
To be clear I've tried using addTarget:action:forControlEvents and it does not work. The touch up event is prevented by the cancel. I want a way to detect the touch up before the UITextField consumes it and produces the Cancel event.
Also, I'd like to avoid creating any extra views if possible.
Well a round about way of doing it would be to create a UIButton with the fill set to clear to cover all of the space, and a UITextField on top of it. If the user touches in the text field, the text field should send an event, and if the user touches outside of the text field, the UIButton will send the event.
Good answer here:
Change default touch events for UITextField
Use the text field's delegate and do what you want to do in textFieldShouldBeginEditing:
You can return NO to prevent the default behavior.
You can create category for the UiView and override the touchesBegan meathod as follows.
It is working fine for me.And it is centralize solution for this problem.
#import "UIView+Keyboard.h"
#implementation UIView(Keyboard)
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
[self.window endEditing:true];
[super touchesBegan:touches withEvent:event];
}
#end
I have faced this problem just today. My goal was to change textfield's background color on touch down and revert it back on touch up/cancel.
It is weird that touch down is sent to target from UITextField, but touch up inside, touch up outside and even touch cancel are not.
I found the solution. It is simple. Just create a subclass of UITextField and override touchesEnded: and touchesCancelled:.
#interface TextField : UITextField
#end
#implementation CuteTextField
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
// Initialization code
}
return self;
}
-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event{
[super touchesEnded:touches withEvent:event];
self.backgroundColor=GetDefaultBackgroundColor();
}
-(void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event{
[super touchesCancelled:touches withEvent:event];
self.backgroundColor=GetDefaultBackgroundColor();
}
#end
Add a target for your desired event programatically, or, use IB.
UITextField * touchyTextField = [[UITextField alloc] initWithFrame:CGRectMake(0.0, 0.0, 200.0, 21.0)];
[touchyTextField addTarget:self
action:#selector(yourDesiredMethod)
forControlEvents:UIControlEventTouchUpInside];

Resources