dismissing the NumberPad keyboard from a UIScrollView - iphone

I have an application with UIScrollView added as a subview of UIView. This Scroll view has a textfield with keyboard type set to numberPad.
Now the problem is , i want to dismiss the keyboard when i tap anywhere else in the scroll view. how can i do this ... ?

Just call the textField's resignFirstResponder in the touch handler.
(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
[myTextField resignFirstResponder];
}

When I added the gesture to a subclass of UIScrollView, I was having problems with the various gestures in my view tree interfering with each other, such as being able to click on subviews, scroll the view, and have the keyboard dismiss in all cases. I came up with this solution, which can be setup from a superclass of UIScrollView or from a UIViewController.
The DismissKeyboardTapGesture class uses ARC, works with any text fields under the view, and doesn't take over any clicks from subviews like buttons. Also takes advantage of iOS7 scrolling effect to dismiss keyboard.
Setting up from UISScrollView superclass:
_dismissKeyboard = [[DismissKeyboardTapGesture alloc] initWithView:self];
or from UIViewController:
_dismissKeyboard = [[DismissKeyboardTapGesture alloc] initWithView:self.view];
Here is the class:
#interface DismissKeyboardTapGesture : NSObject <UIGestureRecognizerDelegate>
#end
#implementation DismissKeyboardTapGesture
- (id)initWithView:(UIView *)view
{
self = [super init];
if (self) {
UITapGestureRecognizer *singleTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(singleTap:)];
singleTap.cancelsTouchesInView = NO;
singleTap.delegate = self;
[view addGestureRecognizer:singleTap];
if ([view respondsToSelector:#selector(setKeyboardDismissMode:)]) {
// Bonus effect to dismiss keyboard by scrolling
((UIScrollView *)view).keyboardDismissMode = UIScrollViewKeyboardDismissModeInteractive;
}
}
return self;
}
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
// Don't stop any existing gestures in our view from working
if (otherGestureRecognizer.view == gestureRecognizer.view) {
return YES;
}
return NO;
}
- (void)singleTap:(UIGestureRecognizer*)gestureRecognizer
{
// Close keyboard for any text edit views that are children of the main view
[gestureRecognizer.view endEditing:YES];
}
#end

Related

How get UITableView IndexPath from UITableView iphone?

In my iPhone app I have one messaging screen. I have added UITapGestureRecognizer on the UIViewController and also I have a UITableview on the screen. I want to select the UITableViewCell but I can't select the UITableView because of UITapGestureRecognizer. When I touch the screen, only the tap gesture action is called but UITableView delegate didSelectRowAtIndexPath: is not called. Could anyone please help me to work on both tap gesture and UITableView:didSelectRowAtIndexPath:. Thanks in advance.
While I prefer Matt Meyer's suggestion or my other suggestion of using a custom gesture recognizer, another solution, not involving custom gesture recognizers, would be to have your tap gesture recognizer identify whether you tapped on a cell in your tableview, and if so, manually invoke didSelectRowAtIndexPath, e.g.:
- (void)handleTap:(UITapGestureRecognizer *)sender
{
CGPoint location = [sender locationInView:self.view];
if (CGRectContainsPoint([self.view convertRect:self.tableView.frame fromView:self.tableView.superview], location))
{
CGPoint locationInTableview = [self.tableView convertPoint:location fromView:self.view];
NSIndexPath *indexPath = [self.tableView indexPathForRowAtPoint:locationInTableview];
if (indexPath)
[self tableView:self.tableView didSelectRowAtIndexPath:indexPath];
return;
}
// otherwise proceed with the rest of your tap handling logic
}
This is suboptimal because if you're doing anything sophisticated with your tableview (e.g. in cell editing, custom controls, etc.), you lose that behavior, but if you're just looking to receive the didSelectRowAtIndexPath, then this might do the job. The other two approaches (separate views or the custom gesture recognizer) let you retain the full tableview functionality, but this could work if you just need something simple and you don't need the rest of the tableview's built-in capabilities.
You can use the TagGesture delegate:
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
{
if ([touch.view isDescendantOfView:yourTableView]) {
return NO;
}
return YES;
}
Hope this helps.
An easier way to do this is to have two views: one containing the view that you want the tap gesture to be on, and one containing the tableview. You can attach the UITapGestureRecognizer to the view you want it to work on, and then it won't block your UITableView.
Assuming you want the tap gesture to work everywhere except over the tableview, you could subclass the tap gesture recognizer, creating a recognizer that will ignore any subviews included in an array of excludedViews, preventing them from generating a successful gesture (thus passing it on to didSelectRowAtIndexPath or whatever):
#import <UIKit/UIGestureRecognizerSubclass.h>
#interface MyTapGestureRecognizer : UITapGestureRecognizer
#property (nonatomic, strong) NSMutableArray *excludedViews;
#end
#implementation MyTapGestureRecognizer
#synthesize excludedViews = _excludedViews;
- (id)initWithTarget:(id)target action:(SEL)action
{
self = [super initWithTarget:target action:action];
if (self)
{
_excludedViews = [[NSMutableArray alloc] init];
}
return self;
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
[super touchesBegan:touches withEvent:event];
CGPoint location = [[touches anyObject] locationInView:self.view];
for (UIView *excludedView in self.excludedViews)
{
CGRect frame = [self.view convertRect:excludedView.frame fromView:excludedView.superview];
if (CGRectContainsPoint(frame, location))
self.state = UIGestureRecognizerStateFailed;
}
}
#end
And then, when you want to use it, just specify what controls you want to exclude:
MyTapGestureRecognizer *tap = [[MyTapGestureRecognizer alloc] initWithTarget:self action:#selector(handleTap:)];
[tap.excludedViews addObject:self.tableView];
[self.view addGestureRecognizer:tap];

Passing through touches to UIViews underneath

I have a UIView with 4 buttons on it and another UIView on top of the buttons view. The top most view contains a UIImageView with a UITapGestureRecognizer on it.
The behavoir I am trying to create is that when the user taps the UIImageView it toggles between being small in the bottom right hand corner of the screen and animating to become larger. When it is large I want the buttons on the bottom view to be disabled and when it is small and in the bottom right hand corner I want the touches to be passed through to the buttons and for them to work as normal. I am almost there but I cannot get the touches to pass through to the buttons unless I disable the UserInteractions of the top view.
I have this in my initWithFrame: of the top view:
// Add a gesture recognizer to the image view
UITapGestureRecognizer *tapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(imageTapped:)];
tapGestureRecognizer.cancelsTouchesInView = NO;
[imageView addGestureRecognizer:tapGestureRecognizer];
[tapGestureRecognizer release];
and I this is my imageTapped: method:
- (void) imageTapped:(UITapGestureRecognizer *) gestureRecognizer {
// Toggle between expanding and contracting the image
if (expanded) {
[self contractAnimated:YES];
expanded = NO;
gestureRecognizer.cancelsTouchesInView = NO;
self.userInteractionEnabled = NO;
self.exclusiveTouch = NO;
}
else {
[self expandAnimated:YES];
expanded = YES;
gestureRecognizer.cancelsTouchesInView = NO;
self.userInteractionEnabled = YES;
self.exclusiveTouch = YES;
}
}
With the above code, when the image is large the buttons are inactive, when I touch the image it shrinks and the buttons become active. However, the small image doesn't receive the touches and therefore wont expand.
If I set self.userInteractionEnabled = YES in both cases, then the image expands and contracts when touched but the buttons never receive touches and act as though disabled.
Is there away to get the image to expand and contract when touched but for the buttons underneath to only receive touches if the image is in its contracted state? Am I doing something stupid here and missing something obvious?
I am going absolutely mad trying to get this to work so any help would be appreciated,
Dave
UPDATE:
For further testing I overrode the touchesBegan: and touchesCancelled: methods and called their super implementations on my view containing the UIImageView. With the code above, the touchesCancelled: is never called and the touchesBegan: is always called.
So it would appear that the view is getting the touches, they are just not passed to the view underneath.
UPDATE
Is this because of the way the responder chain works? My view hierarchy looks like this:
VC - View1
-View2
-imageView1 (has tapGestureRecogniser)
-imageView2
-View3
-button1
-button2
I think the OS first does a hitTest as says View2 is in front so should get all the touches and these are never passed on to View3 unless userInteractions is set to NO for View2, in which case the imageView1 is also prevented from receiving touches. Is this how it works and is there a way for View2 to pass through it's touches to View3?
The UIGestureRecognizer is a red herring I think. In the end to solve this I overrode the pointInside:withEvent: method of my UIView:
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
BOOL pointInside = NO;
if (CGRectContainsPoint(imageView.frame, point) || expanded) pointInside = YES;
return pointInside;
}
This causes the view to trap all touches if you touch either the imageView or if its expanded flag is set. If it is not expanded then only trap the touches if they are on the imageView.
By returning NO, the top level VC's View queries the rest of its view hierarchy looking for a hit.
Select your View in Storyboard or XIB and...
Or in Swift
view.isUserInteractionEnabled = false
Look into the UIGestureRecognizerDelegate Protocol. Specifically, gestureRecognizer:shouldReceiveTouch:
You'll want to make each UIGestureRecognizer a property of your UIViewController,
// .h
#property (nonatomic, strong) UITapGestureRecognizer *lowerTap;
// .m
#synthesize lowerTap;
// When you are adding the gesture recognizer to the image view
self.lowerTap = tapGestureRecognizer
Make sure you make your UIViewController a delegate,
[self.lowerTap setDelegate: self];
Then, you'd have something like this,
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
if (expanded && gestureRecognizer == self.lowerTap) {
return NO;
}
else {
return YES;
}
}
Of course, this isn't exact code. But this is the general pattern you'd want to follow.
I have a another solution. I have two views, let's call them CustomSubView that were overlapping and they should both receive the touches. So I have a view controller and a custom UIView class, lets call it ViewControllerView that I set in interface builder, then I added the two views that should receive the touches to that view.
So I intercepted the touches in ViewControllerView by overwriting hitTest:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
return self;
}
Then I overwrote in ViewControllerView:
- (void)touchesBegan:(NSSet *)touches
withEvent:(UIEvent *)event
{
[super touchesBegan:touches withEvent:event];
for (UIView *subview in [self.subviews reverseObjectEnumerator])
{
if ([subview isKindOfClass:[CustomSubView class]])
{
[subview touchesBegan:touches withEvent:event];
}
}
}
Do the exact same with touchesMoved touchesEnded and touchesCancelled.
#Magic Bullet Dave's solution but in Swift
Swift 3
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
var pointInside = false
if commentTextField.frame.contains(point) {
pointInside = true
} else {
commentTextField.resignFirstResponder()
}
return pointInside
}
I use it in my CameraOverlayView for ImagePickerViewController.cameraOverlay to give user ability to comment while taking new photo

How can I dismiss the keyboard if a user taps off the on-screen keyboard?

I want to be able to dismiss the iPhone keyboard when the user taps anywhere outside of the keyboard. How can I go about doing this? I know I need to dismiss the responder, but need to know how to implement it when a user taps out of the keyboard space.
You'll need to add an UITapGestureRecogniser and assign it to the view, and then call resign first responder on the textfield on it's selector.
The code:
In viewDidLoad
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self
action:#selector(dismissKeyboard)];
[self.view addGestureRecognizer:tap];
In dismissKeyboard:
-(void)dismissKeyboard {
[aTextField resignFirstResponder];
}
(Where aTextField is the textfield that is responsible for the keyboard)
OPTION 2
If you can't afford to add a gestureRecognizer then you can try this
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch * touch = [touches anyObject];
if(touch.phase == UITouchPhaseBegan) {
[aTextField resignFirstResponder];
}
}
The simplest solution I have used is this:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
[self.view endEditing:YES];
}
The endEditing command can be used on any view that contains your textfield as a subview. The other advantage of this method is that you don't need to know which textfield triggered the keyboard. So even if you have a multiple textfields, just add this line to the superview.
Based on the Apple documentation, I think this method exists specifically to solve this problem.
Add a tapGesture Recognizer but make sure cancelsTouchesInView = NO
UITapGestureRecognizer* tapGesture = [[UITapGestureRecognizer alloc]initWithTarget:self action:#selector(closeTextInput)];
tapGesture.cancelsTouchesInView = NO;
[self.view addGestureRecognizer:tapGesture];
[tapGesture release];
You'll need to add an UITapGestureRecogniser and assign it to the view, and then call resign first responder on the textfield on it's selector.
The code:
In viewDidLoad
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self
action:#selector(dismissKeyboard)];
[self.view addGestureRecognizer:tap];
In dismissKeyboard:
-(void)dismissKeyboard {
[self.view endEditing:true];
}
You need to add a transparent UIVIew as a subview below the keyboard and handle touches there, to dismiss the keyboard. Below code is for your reference.
UITapGestureRecognizer* gesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(overlayTouched:)];
gesture.delegate = self;
[(UITapGestureRecognizer *)gesture setNumberOfTouchesRequired:1];
UIView* trans = [[UIView alloc] initWithFrame:[[delegate view] bounds]];
[trans setOpaque:NO];
[trans setAutoresizingMask:UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin];
[trans setAlpha:0.3];
[trans setUserInteractionEnabled:YES];
trans.multipleTouchEnabled = YES;
[trans addGestureRecognizer:gesture];
[trans setBackgroundColor:[UIColor blackColor]];
[trans setTag:BLACK_SCREEN_VIEW];
This is a Swift 4 solution:
let tap = UITapGestureRecognizer(target: self, action: #selector(self.dismissKeyboard))
self.view.addGestureRecognizer(tap)
And the dismissKeyboard
#objc func dismissKeyboard() {
self.view.endEditing(true)
}
Other way to do is simple:
Makes your UIView as UIControl in custom class in the interface builder, then you can attach an IBAction method in Touch up inside event of your UIView : UIControl, then you put [yourTextField resignFirstResponder] inside the IBAction method, like this:
- (IBAction) hideKeyboard: (id) sender
{
// If you have more than one textfield to dismiss, of course only can be active 1, but here you can't know who is it, because sender will be the UIView : UIControl
[alias resignFirstResponder];
[password resignFirstResponder];
}
Then, you have other option and it's to put in your textfield the return key of the keyboard as Done (It can be any of those you want, but Done it's good for this, because return means to do an action with the form) in the interface builder, so you can press Done and hide the keyboard, but in that case you have to attach the previous IBAction method to the Did end on exit event.
And in this way the keyboard will hide touching outside or touching Done from the keyboard.
If you want to improve the code, if only will hide the keyboard touching Done from the keyboard the method should be:
// Attach all textFields here on Did end on exit event, will not work if touch outside the keyboard
- (IBAction) hideKeyboard: (id) sender
{
[sender resignFirstResponder];
}
If you are on a UITableViewController or have a UITableView that the user will be interacting with, in your ViewDidLoad you can simply do:
tableView.KeyboardDismissMode = UIScrollViewKeyboardDismissMode.OnDrag;
Note: this is Xamarin.iOS syntax.
If you don't want to add a gesture recognizer you can also use the touchesBegin method
Code in Swift 4
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
view.endEditing(true)
}
You don't even need a gesture recogniser. Views can detect touches without gesture recognisers. All you need is literally this in your view controller....
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
[self.view endEditing:YES];
}

UIScrollView with UIImageView as subviews, taping UIImageView

I need to do something like this:
UISCrollView with UIImageViews as its subviews. When user taps on UIImageViews an action occurs. But when user want to scroll UIScrollView should scroll even if scrolling started at UIImageView (at location where UIImage of UIImageView is displayed).
Basicly I can get one of two scenarios:
I can get that if user taps on UIImageView (which is subview of UIScrollView) an action occur, but when you try to scroll by draging finger from UIImageView the action also occurs (and I want a scroll to occur).
I can make that regardles where user taps the view will scroll but if user taps UIImageView the action will NOT occur.
I can't get you any of my code because I'm testing a lot of aprroches here and there and it's bit messy so it would be no use at all (without a tons of commenting).
Is there a clean and simple solution for doing this?
Ok here is some code:
-(UIView*) hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
if(isDragging == NO)
{
return [super hitTest:point withEvent:event];
}
NSLog(#"dragging ><><><><><>>><><><><>");
return nil;
}
Now if I return nil then I can scroll but I can't get to tap my UIImageView for an action. If I return [super hitTest:point withEvent:event] I can't scroll over my UIIMageView.
isDragging is test code to determine if I'm trying to scroll or just tap. But hit test occurs before I can set isDragging property accordingly to event that is happening.
Here is my init
-(id) initWithCoder:(NSCoder *)aDecoder
{
if(self = [super initWithCoder:aDecoder])
{
[self setUserInteractionEnabled:YES];
UISwipeGestureRecognizer *swipeRecLeft = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:#selector(swipe)];
swipeRecLeft.direction = UISwipeGestureRecognizerDirectionDown;
UISwipeGestureRecognizer *swipeRecRight = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:#selector(swipe)];
swipeRecRight.direction = UISwipeGestureRecognizerDirectionUp;
UITapGestureRecognizer *singleTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(tapSingle)];
[self addGestureRecognizer:swipeRecRight];
[self addGestureRecognizer:swipeRecLeft];
[self addGestureRecognizer:singleTap];
[swipeRecLeft release];
[swipeRecRight release];
[singleTap release];
isDragging = NO;
}
else {
self = nil;
}
return self;
}
Here are the rest of actions
-(void) tapSingle
{
[self.delegate hitOccur];
}
-(void) swipe
{
isDragging = YES;
}
And on top of it in my UIScrollView I've setted delegate and when scrolling ends I set isDragging property manualy to NO on each subview of my UIScrollView.
It's working... but it's not perfect. To actually scroll content I must swipe TWICE in UIImageView (first one is to set isDragging to YES and then we can scroll...). How to this right and proper?
LATEST UPDATE:
Ok I've managed to solve this problem. However I'm dead sure my way isn't clean or good one (but regardles it works).
In my UIScrollView subclass I've overided hitTest method with this:
-(UIView*) hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
if(!self.dragging)
{
if([[super hitTest:point withEvent:event] class] == [ResponsiveBookView class])
{
container = [super hitTest:point withEvent:event];
}
}
return self;
}
Where the container is id container and it holds my UIView subclass. So I can recognize if the touch was on my image or on scroll view itself. Now I need to detec if it is scrolling or just touching and I do this here:
-(void) touchesEnded: (NSSet *) touches withEvent: (UIEvent *) event
{
if (!self.dragging) {
NSLog(#"touch touch touch");
[container tapSingle];
[self.nextResponder touchesEnded: touches withEvent:event];
}
[super touchesEnded: touches withEvent: event];
}
As you see if self.dragging (scrolling) the default behaviour will apply. If !self.dragging I will manually call tapSingle on my container (which will then make an action "occur"). It works!
Yeah—use gesture recognizers. If you add a UITapGestureRecognizer to each UIImageView (using the UIVIew -addGestureRecognizer: method), you should get both recognition of the taps and the default scrolling behavior.

Iphone SDK dismissing Modal ViewControllers on ipad by clicking outside of it

I want to dismiss a FormSheetPresentation modal view controller when the user taps outside the modal view...I have seen a bunch of apps doing this (ebay on ipad for example) but i cant figure out how since the underneath views are disabled from touches when modal views are displayed like this (are they presenting it as a popover perhaps?)...anyone have any suggestions?
I'm a year late, but this is pretty straightforward to do.
Have your modal view controller attach a gesture recognizer to the view's window:
UITapGestureRecognizer *recognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(handleTapBehind:)];
[recognizer setNumberOfTapsRequired:1];
recognizer.cancelsTouchesInView = NO; //So the user can still interact with controls in the modal view
[self.view.window addGestureRecognizer:recognizer];
[recognizer release];
The handling code:
- (void)handleTapBehind:(UITapGestureRecognizer *)sender
{
if (sender.state == UIGestureRecognizerStateEnded)
{
CGPoint location = [sender locationInView:nil]; //Passing nil gives us coordinates in the window
//Then we convert the tap's location into the local view's coordinate system, and test to see if it's in or outside. If outside, dismiss the view.
if (![self.view pointInside:[self.view convertPoint:location fromView:self.view.window] withEvent:nil])
{
// Remove the recognizer first so it's view.window is valid.
[self.view.window removeGestureRecognizer:sender];
[self dismissModalViewControllerAnimated:YES];
}
}
}
That's about it. HIG be damned, this is a useful and often intuitive behavior.
For iOS 8, you must both implement the UIGestureRecognizer, and swap the (x,y) coordinates of the tapped location when in landscape orientation. Not sure if this is due to an iOS 8 bug.
- (void) viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
// add gesture recognizer to window
UITapGestureRecognizer *recognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(handleTapBehind:)];
[recognizer setNumberOfTapsRequired:1];
recognizer.cancelsTouchesInView = NO; //So the user can still interact with controls in the modal view
[self.view.window addGestureRecognizer:recognizer];
recognizer.delegate = self;
}
- (void)handleTapBehind:(UITapGestureRecognizer *)sender
{
if (sender.state == UIGestureRecognizerStateEnded) {
// passing nil gives us coordinates in the window
CGPoint location = [sender locationInView:nil];
// swap (x,y) on iOS 8 in landscape
if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(#"8.0")) {
if (UIInterfaceOrientationIsLandscape([UIApplication sharedApplication].statusBarOrientation)) {
location = CGPointMake(location.y, location.x);
}
}
// convert the tap's location into the local view's coordinate system, and test to see if it's in or outside. If outside, dismiss the view.
if (![self.view pointInside:[self.view convertPoint:location fromView:self.view.window] withEvent:nil]) {
// remove the recognizer first so it's view.window is valid
[self.view.window removeGestureRecognizer:sender];
[self dismissViewControllerAnimated:YES completion:nil];
}
}
}
#pragma mark - UIGestureRecognizer Delegate
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
{
return YES;
}
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
return YES;
}
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
{
return YES;
}
The other apps are not using Modal Views if they allow the view to be dismissed by clicking outside of it. UIModalPresentationFormSheets cannot be dismissed this way. (nor, indeed can any UIModal in SDK3.2). Only the UIPopoverController can be dismissed by clicking outside of the area. It is very possible (though against Apple's iPad HIG) for the app developer to have shaded out the background screen and then displayed the UIPopoverController so that it looks like a UIModalPresentationFormSheets (or other UIModal View).
[...] UIModalPresentationCurrentContext style lets a view controller adopt the presentation style of its parent. In each modal view, the dimmed areas show the underlying content but do not allow taps in that content. Therefore, unlike a popover, your modal views must still have controls that allow the user to dismiss the modal view.
See the iPadProgrammingGuide on the developer site for more information (Page 46 -- "Configuring the Presentation Style for Modal Views")
The code above works great, but I would change the if statement to,
if (!([self.view pointInside:[self.view convertPoint:location fromView:self.view.window] withEvent:nil] || [self.navigationController.view pointInside:[self.navigationController.view convertPoint:location fromView:self.navigationController.view.window] withEvent:nil]))
{
// Remove the recognizer first so it's view.window is valid.
[self.view.window removeGestureRecognizer:sender];
[self dismissModalViewControllerAnimated:YES];
}
This makes sure you can still interact with the navigation bar, otherwise tapping in it dismisses the modal view.
Answer updated for iOS 8
Apparently, in iOS 8, the UIDimmingView has a tap gesture recognizer, which interferes with the initial implementation, so we ignore it and don't require it to fail.
This is the age of speed, so most are probably just copying the code above.. But, I suffer from OCD when it comes to code, unfortunately.
Here is a modular solution that uses Danilo Campos's answer with categories. It also solves an important bug that may occur if you are dismissing your modal through other means, as mentioned.
NOTE: The if statements are there because I use the view controller for both iPhone and iPad, and only the iPad needs to register/unregister.
UPDATE: The gist has been updated, since it didn't work properly with the awesome FCOverlay code, and it didn't allow gestures to be recognized in the presented view. Those issues are fixed.
Using the category is as easy as:
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
if (self.presentingViewController) {
[self registerForDismissOnTapOutside];
}
}
- (void)viewWillDisappear:(BOOL)animated
{
if (self.presentingViewController) {
[self unregisterForDismissOnTapOutside];
}
[super viewWillDisappear:animated];
}
Copy paste this code in your ModalViewController :
- (void) viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
//Code for dissmissing this viewController by clicking outside it
UITapGestureRecognizer *recognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(handleTapBehind:)];
[recognizer setNumberOfTapsRequired:1];
recognizer.cancelsTouchesInView = NO; //So the user can still interact with controls in the modal view
[self.view.window addGestureRecognizer:recognizer];
}
- (void)handleTapBehind:(UITapGestureRecognizer *)sender
{
if (sender.state == UIGestureRecognizerStateEnded)
{
CGPoint location = [sender locationInView:nil]; //Passing nil gives us coordinates in the window
//Then we convert the tap's location into the local view's coordinate system, and test to see if it's in or outside. If outside, dismiss the view.
if (![self.view pointInside:[self.view convertPoint:location fromView:self.view.window] withEvent:nil])
{
// Remove the recognizer first so it's view.window is valid.
[self.view.window removeGestureRecognizer:sender];
[self dismissModalViewControllerAnimated:YES];
}
}
}
Very important: If you have any other way to close your modal popup window, don't forget to remove the tap gesture recognizer!
I forgot this, and got crazy crashes later on, since the tap recognizer was still firing events.
Accoring to Apple's iOS HIG, 1. the modal view doesn't have that ability to be dismissed without any input on itself; 2. use modal view in the situation that a user input is required.
Use UIPresentationController instead:
- (void)presentationTransitionWillBegin
{
[super presentationTransitionWillBegin];
UITapGestureRecognizer *dismissGesture=[[UITapGestureRecognizer alloc]initWithTarget:self action:#selector(dismissGestureTapped:)];
[self.containerView addGestureRecognizer:dismissGesture];
[[[self presentedViewController] transitionCoordinator] animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> context) {
} completion:nil];
}
- (void) dismissGestureTapped:(UITapGestureRecognizer *)sender{
if (sender.state==UIGestureRecognizerStateEnded&&!CGRectContainsPoint([self frameOfPresentedViewInContainerView], [sender locationInView:sender.view])) {
[self.presentingViewController dismissViewControllerAnimated:YES completion:nil];
}
}
Modified from LookInside example
This works for me for ios7 an 8 and navigation bar.
If you don't need the nav bar just remove location2 and second condition in the if statement after the pipes.
#MiQUEL this should work for you too
- (void)handleTapBehind:(UITapGestureRecognizer *)sender
{
if (sender.state == UIGestureRecognizerStateEnded)
{
CGPoint location1 = [sender locationInView:self.view];
CGPoint location2 = [sender locationInView:self.navigationController.view];
if (!([self.view pointInside:location1 withEvent:nil] || [self.navigationController.view pointInside:location2 withEvent:nil])) {
[self.view.window removeGestureRecognizer:self.recognizer];
[self dismissViewControllerAnimated:YES completion:nil];
}
}
}
Edit: You may also need to be a gesture recognizer delegate for this and other above solutions to work. Do it like so:
#interface CommentTableViewController () <UIGestureRecognizerDelegate>
set yourself as the delegate for the recognizer:
self.recognizer.delegate = self;
and implement this delegate method:
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer{
return YES;
}
It is pretty doable.
Take a look here
https://stackoverflow.com/a/26016458/4074557
It is a NavigationController (modal) that auto dismiss for ipad (when you tap outside)
Use your viewcontroller inside of it.
Hope it helps.
I know it's late but consider using CleanModal (tested with iOS 7 and 8).
https://github.com/orafaelreis/CleanModal
You can use MZFormSheetController like this:
MZFormSheetController *formSheet = [[MZFormSheetController alloc] initWithSize:customSize viewController:presentedViewController];
formSheet.shouldDismissOnBackgroundViewTap = YES;
[presentingViewController mz_presentFormSheetController:formSheet animated:YES completionHandler:nil];
In Swift 2/Xcode Version 7.2 (7C68) the following code worked for me.
Attention: this code should be put in the ViewController.swift file of the presented FormSheet or Page Sheet, here: "PageSheetViewController.swift"
class PageSheetViewController: UIViewController, UIGestureRecognizerDelegate {
override func viewDidAppear(animated: Bool) {
let recognizer = UITapGestureRecognizer(target: self, action:Selector("handleTapBehind:"))
recognizer.delegate = self
recognizer.numberOfTapsRequired = 1
recognizer.cancelsTouchesInView = false
self.view.window?.addGestureRecognizer(recognizer)
}
func gestureRecognizer(sender: UIGestureRecognizer,
shouldRecognizeSimultaneouslyWithGestureRecognizer:UIGestureRecognizer) -> Bool {
return true
}
func handleTapBehind(sender:UIGestureRecognizer) {
if(sender.state == UIGestureRecognizerState.Ended){
var location:CGPoint = sender.locationInView(nil)
// detect iOS Version 8.0 or greater
let Device = UIDevice.currentDevice()
let iosVersion = Double(Device.systemVersion) ?? 0
let iOS8 = iosVersion >= 8
if (iOS8) {
// in landscape view you will have to swap the location coordinates
if(UIInterfaceOrientationIsLandscape(UIApplication.sharedApplication().statusBarOrientation)){
location = CGPointMake(location.y, location.x);
}
}
if(!self.view.pointInside(self.view.convertPoint(location, fromView: self.view.window), withEvent: nil)){
self.view.window?.removeGestureRecognizer(sender)
self.dismissViewControllerAnimated(true, completion: nil)
}
}
}
}