I have been trying to implement a UITapGestureRecognizer that will dismiss a modal view controller by detecting a tap outside the modal view controller on iOS 8.
The issue I am seeing is that in landscape mode on iPad that the tap gesture is only recognised inside parts of the view controller. My modal view is 380 width x 550 height. When orientation is in landscape (with the home button at the right) the tap gesture is detected inside the bottom half of the modal view. When orientation is in landscape but with the home button at the left hand side the tap gesture is detected inside the top half of the modal view.
Problem 1: The tap gesture is only detected inside the modal view when it should be detected outside.
Problem 2: No detection in some areas of the view in landscape orientation.
Here is the code I used:
UIViewController+DismissOnTapOutside.h
#import <UIKit/UIKit.h>
#interface UIViewController (DismissOnTapOutside)
- (void)registerForDismissOnTapOutside; // Call in viewDidAppear
- (void)unregisterForDismissOnTapOutside; // Call in viewWillDisappear
#end
UIViewController+DismissOnTapOutside.m
#import "UIViewController+DismissOnTapOutside.h"
#import <objc/runtime.h>
static char gestureRecognizerKey;
static char gestureRecognizerDelegateKey;
#interface SimpleGestureRecognizerDelegate : NSObject <UIGestureRecognizerDelegate>
#end
#implementation SimpleGestureRecognizerDelegate
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
return YES;
}
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRequireFailureOfGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
return [otherGestureRecognizer isMemberOfClass:[UITapGestureRecognizer class]];
}
#end
#interface UIViewController ()
#property (assign, nonatomic) UIGestureRecognizer *gestureRecognizer;
#property (strong, nonatomic) SimpleGestureRecognizerDelegate *gestureRecognizerDelegate;
#end
#implementation UIViewController (DismissOnTapOutside)
- (void)setGestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
{
objc_setAssociatedObject(self, &gestureRecognizerKey, gestureRecognizer, OBJC_ASSOCIATION_ASSIGN);
}
- (UIGestureRecognizer *)gestureRecognizer
{
return objc_getAssociatedObject(self, &gestureRecognizerKey);
}
- (void)setGestureRecognizerDelegate:(SimpleGestureRecognizerDelegate *)delegate
{
objc_setAssociatedObject(self, &gestureRecognizerDelegateKey, delegate, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (UIGestureRecognizer *)gestureRecognizerDelegate
{
id delegate = objc_getAssociatedObject(self, &gestureRecognizerDelegateKey);
if (!delegate)
{
delegate = [[SimpleGestureRecognizerDelegate alloc] init];
self.gestureRecognizerDelegate = delegate;
}
return delegate;
}
- (void)handleDismissTap:(UIGestureRecognizer *)gesture
{
if (gesture.state == UIGestureRecognizerStateEnded)
{
UIView *view = self.navigationController.view ?: self.view;
// Passing nil gives us coordinates in the window
CGPoint location = [gesture locationInView:nil];
if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(#"8.0")) {
if (UIInterfaceOrientationIsLandscape([UIApplication sharedApplication].statusBarOrientation)) {
location = CGPointMake(location.y, location.x);
}
}
// Then we convert the tap's location into the local view's coordinate system
location = [view convertPoint:location fromView:self.view.window];
if (![view pointInside:location withEvent:nil])
{
[self.presentingViewController dismissViewControllerAnimated:YES completion:nil];
}
}
}
- (void)registerForDismissOnTapOutside
{
// This approach is attributed to Danilo Campos:
// http://stackoverflow.com/a/6180584/456434
UITapGestureRecognizer *recognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(handleDismissTap:)];
recognizer.delegate = self.gestureRecognizerDelegate;
recognizer.numberOfTapsRequired = 1;
recognizer.cancelsTouchesInView = NO;
self.gestureRecognizer = recognizer;
[self.view.window addGestureRecognizer:recognizer];
}
- (void)unregisterForDismissOnTapOutside
{
[self.view.window removeGestureRecognizer:self.gestureRecognizer];
self.gestureRecognizer = nil;
}
#end
The key method that should set the correct location for tap detection does not seem to be setting the location correctly in landscape orientation in iOS 8:
- (void)handleDismissTap:(UIGestureRecognizer *)gesture
{
if (gesture.state == UIGestureRecognizerStateEnded)
{
UIView *view = self.navigationController.view ?: self.view;
// Passing nil gives us coordinates in the window
CGPoint location = [gesture locationInView:nil];
if (SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(#"8.0")) {
if (UIInterfaceOrientationIsLandscape([UIApplication sharedApplication].statusBarOrientation)) {
location = CGPointMake(location.y, location.x);
}
}
// Then we convert the tap's location into the local view's coordinate system
location = [view convertPoint:location fromView:self.view.window];
if (![view pointInside:location withEvent:nil])
{
[self.presentingViewController dismissViewControllerAnimated:YES completion:nil];
}
}
}
All of the examples I have looked at on StackOverflow in relation to this issue are adamant that the above code works. But in my case it simply does not detect taps outside the modal view controller.
How can a tap outside a modal be detected in iOS 8 on iPad in landscape orientation?
Related
I am moving items in the view by touching them to the place where i leave it
i am using touch events
touchesBegin , touchesMoved , touchesEnd
and in touchesMoved i move the item.frame to the new location and it works with me
but then i found a code that use panGestureRecognizer
and then i cant determine what to use
the code to handle pan was
- (IBAction)handlePan:(UIPanGestureRecognizer *)recognizer {
if (recognizer.state == UIGestureRecognizerStateBegan || recognizer.state == UIGestureRecognizerStateChanged) {
CGPoint translation = [recognizer translationInView:self.superview];
CGPoint translatedCenter = CGPointMake(self.center.x + translation.x, self.center.y + translation.y);
[self setCenter:translatedCenter];
[recognizer setTranslation:CGPointZero inView:self];
}
given that i need the exact coordinates of the point i am touching
The code you have is OK but seems to be running in a custom subclass of UIView (from references to self). Ideally a UIView should not be handling this it should be dealt with in the View Controller to adhere to the MVC design pattern.
Something like this:
#import "ViewController.h"
#interface ViewController ()
#property (nonatomic, strong) UIPanGestureRecognizer *pan;
#property (nonatomic, strong) IBOutletCollection(UIView) NSArray<UIView *> *touchableViews;
#property (nonatomic, weak) UIView *currentMovingView;
#end
#implementation ViewController
#pragma mark - Lifecyle
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
if (!self.pan) {
self.pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(didPan:)];
}
[self.view addGestureRecognizer:self.pan];
}
- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
[self.view removeGestureRecognizer:self.pan];
}
#pragma mark - Gestures
- (void)didPan:(UIPanGestureRecognizer *)gesture
{
switch (gesture.state) {
case UIGestureRecognizerStateBegan:
{
CGPoint loc = [gesture locationInView:self.view];
self.currentMovingView = [self viewForLocation:loc];
}
break;
case UIGestureRecognizerStateChanged:
{
if (self.currentMovingView == nil) {
return;
}
self.currentMovingView.center = [gesture locationInView:self.view];
}
break;
case UIGestureRecognizerStateEnded:
{
self.currentMovingView = nil;
}
break;
default:
break;
}
}
- (UIView *)viewForLocation:(CGPoint)loc
{
for (UIView *v in self.touchableViews) {
if (CGRectContainsPoint(v.frame, loc)) {
return v;
}
}
return nil;
}
#end
Bear in mind that this is untested code and will almost certainly need to be tweaked for different use cases but this is neat and follows good practices.
i found a code that use panGestureRecognizer
and then i cant determine what to use
Unless you need something than cannot be achieved with UIPanGestureRecogniser, use it.
Can anyone give me an example about Touch drag enter to drag from a button to another that triggering both button's event.
And how does it work?
For example, I want to drag from Do to Fa that event of Do, Re, Mi, Fa are triggered.
Here is my code:
- (void) setupVC {
soundBankPlayer = [[SoundBankPlayer alloc] init];
[soundBankPlayer setSoundBank:#"Piano"];
arrMusicalNotes = [NSArray arrayWithObjects:#"60", #"62", #"64", #"65", #"67", #"69", #"71", #"72", nil];
}
#pragma mark - Setup Musical Note
- (IBAction)btnMusicalNoteclick:(id)sender {
int numOfNote = [[arrMusicalNotes objectAtIndex:((UIButton*)sender).tag] intValue];
NSLog(#"%i", numOfNote);
[soundBankPlayer queueNote:numOfNote gain:1.0f];
[soundBankPlayer playQueuedNotes];
}
- (IBAction)btnDragOut:(id)sender {
NSLog(#"Out");
}
Oh I've seen that when i hold click out of Simulator, the method btnDragOut is triggered. And when i drag from out of Simulator to the button, the method of this button is triggered.
Now I want the method btnDragOut is triggered when i drag out of a button (finger is still in Simulator). Anyone know that?
You can add UIPanGestureRecognizer to your view of your UIViewController subclass via Storyboard or via code in viewDidLoad method:
UIPanGestureRecognizer *gestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(handleDrag:)];
[self.view addGestureRecognizer:gestureRecognizer];
Then you can add property in implementation file of your UIViewController subclass of current UIButton being dragged:
#interface YourViewController ()
#property (weak, nonatomic) UIButton *currentButton;
#end
Now in action method you can detect UIControlEventTouchDragEnter and UIControlEventTouchDragExit events as follows:
- (void)handleDrag:(UIPanGestureRecognizer *)gestureRecognizer
{
CGPoint point = [gestureRecognizer locationInView:self.view];
UIView *draggedView = [self.view hitTest:point withEvent:nil];
if (gestureRecognizer.state == UIGestureRecognizerStateChanged) {
if ([draggedView isKindOfClass:[UIButton class]] && !self.currentButton) {
self.currentButton = (UIButton *)draggedView;
NSLog(#"Enter: %ld", (long)self.currentButton.tag);
// send enter event to your button
[self.currentButton sendActionsForControlEvents:UIControlEventTouchDragEnter];
}
if (self.currentButton && ![self.currentButton isEqual:draggedView]) {
NSLog(#"Out: %ld", (long)self.currentButton.tag);
// send exit event to your button
[self.currentButton sendActionsForControlEvents:UIControlEventTouchDragExit];
self.currentButton = nil;
}
} else if (gestureRecognizer.state == UIGestureRecognizerStateEnded) {
self.currentButton = nil;
}
}
I've tried to implement the UIGestureRecognizerDelegate in my ViewController and somehow the methods aren't called. This is the controller:
#import "DiaryEntryViewController.h"
#import "UINavigationController+BarManagement.h"
#interface DiaryEntryViewController ()<UIGestureRecognizerDelegate>
#property (strong, nonatomic) NSManagedObjectContext *managedObjectContext;
#property (weak, nonatomic) IBOutlet UITextView *textView;
#end
#implementation DiaryEntryViewController
-(void)viewWillAppear:(BOOL)inAnimated{
[super viewWillAppear:inAnimated];
self.navigationController.barsHidden = NO;
}
-(void)viewDidAppear:(BOOL)inAnimated{
[super viewDidAppear:inAnimated];
[self.navigationController hideBarsWithDelay:2.0];
}
-(void)viewWillDisappear:(BOOL)inAnimated{
[self.navigationController setBarsHidden:NO animated:YES];
[super viewWillDisappear:inAnimated];
}
-(NSManagedObjectContext *)managedObjectContext{
return self.diaryEntry.managedObjectContext;
}
-(BOOL)saveDiaryEntry{
BOOL theResult = NO;
NSError *theError = nil;
theResult = [self.managedObjectContext save:&theError];
if(!theResult){
NSLog(#"saveItem %#", theError);
}
return theResult;
}
-(CGRect)visibleBounds{
CGRect theBounds = self.view.bounds;
if([self respondsToSelector:#selector(topLayoutGuide)] && [self respondsToSelector:#selector(bottomLayoutGuide)]){
theBounds.origin.y = [self.topLayoutGuide length];
theBounds.size.height -= [self.topLayoutGuide length] + [self.bottomLayoutGuide length];
}
return theBounds;
}
-(IBAction)toogleBars:(id)sender{
NSLog(#"toogleBars");
UINavigationController *theController = self.navigationController;
BOOL theHiddenFlag = theController.barsHidden;
[theController setBarsHidden:!theHiddenFlag animated:YES];
if(theHiddenFlag){
[theController hideBarsWithDelay:2.0];
}
}
-(BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)inRecognizer{
NSLog(#"gestureRecognizerShouldBegin");
UIView *theView = self.textView;
CGPoint thePoint = [inRecognizer locationInView:theView];
return !CGRectContainsPoint(theView.bounds, thePoint);
}
-(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer{
NSLog(#"bla");
return YES;
}
-(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRequireFailureOfGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer{
NSLog(#"ble");
return YES;
}
-(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer{
NSLog(#"blä");
return YES;
}
#end
It does call toogleBars methods, but none of the recognizer methods.
Do not forget to declare and add the recognizer to the view you want to detect that tap or swipe in
example:
Add a property like "theTapRecognizer" to the VC.
Alloc and init that recognizer:
self.theTapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget: self action: #selector(someMethod:)];
self.theTapRecognizer.delegate = self;
[someView addGestureRecognizer: selftheTapRecognizer];
someView is the placeholder text to the view you want to init that recognizer in, it can be the whole self.view or some subview,
you can listen for any interaction with that gesture recognizer with the following delegate method
-(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
Assuming you have added the UIGestureRecognizer from the storyboard, you need to remember to CTRL + Drag the gesture object to the UIViewController yellow circle image and set the delegate.
Your UIGestureRecognizer object will be on the top of the UIViewController view. CTL Drag this to the yellow circle ViewController icon
Then this pop-up will show and select 'Delegate' so that the gesutureRecognizer will use the delegate methods.
I wanted to create a draggable view which has many action elements. To accomplish this I replicated the code through Apple Documentation for creating a draggable view from here.
The view gets panned as expected but when an action element is clicked the view shifts to some other location. Here is the sample code and a screenshot of Main.storyboard to replicate the issue.
ViewController.h file
#import <UIKit/UIKit.h>
#interface ViewController : UIViewController
- (IBAction)segmentedAction:(id)sender;
#property (weak, nonatomic) IBOutlet UISegmentedControl *segmentOutlet;
#property (weak, nonatomic) IBOutlet UILabel *label;
#end
And this is the code in the ViewController.m file
#import "ViewController.h"
#interface ViewController ()
#end
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
- (void)adjustAnchorPointForGestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
{
if (gestureRecognizer.state == UIGestureRecognizerStateBegan) {
UIView *piece = gestureRecognizer.view;
CGPoint locationInView = [gestureRecognizer locationInView:piece];
CGPoint locationInSuperview = [gestureRecognizer locationInView:piece.superview];
piece.layer.anchorPoint = CGPointMake(locationInView.x / piece.bounds.size.width, locationInView.y / piece.bounds.size.height);
piece.center = locationInSuperview;
}
}
- (IBAction)panPiece:(UIPanGestureRecognizer *)gestureRecognizer
{
UIView *piece = [gestureRecognizer view];
[self adjustAnchorPointForGestureRecognizer:gestureRecognizer];
if ([gestureRecognizer state] == UIGestureRecognizerStateBegan || [gestureRecognizer state] == UIGestureRecognizerStateChanged) {
CGPoint translation = [gestureRecognizer translationInView:self.view];
[piece setCenter:CGPointMake([piece center].x + translation.x, [piece center].y + translation.y)];
[gestureRecognizer setTranslation:CGPointZero inView:self.view];
}
}
- (IBAction)segmentedAction:(id)sender {
self.label.text = [NSString stringWithFormat:#"%ld",self.segmentOutlet.selectedSegmentIndex];
}
#end
Can anybody guide me as to what I'm doing wrong here.
Thanks in advance
Okay, finally got the problem. This is happening because of the Autolayout. When you try to set the text of the label, the autolayout is forcing the view frame to reset to the initial values. Since you have changed the anchorpoint, it will appear to be moved to some random location.
To fix the issue, simply use the following line in viewDidLoad method;
self.label.translatesAutoresizingMaskIntoConstraints = YES;
OR add the following line below UIView *piece = gestureRecognizer.view; in adjustAnchorPointForGestureRecognizer:(UIGestureRecognizer *)gestureRecognizer method:-
piece.translatesAutoresizingMaskIntoConstraints = YES;
Cheers!:-)
I have added 20 subviews to scrollview line by line as rows
yPos=0;
for (int i=0; i<24; i++) {
UIView *timeView=[[UIView alloc]initWithFrame:CGRectMake(71, yPos, 909, 60)];
timeView.userInteractionEnabled=TRUE;
timeView.exclusiveTouch=YES;
timeView.tag=i;
NSLog(#"sub vieww tag=:%d",timeView.tag);
timeView.backgroundColor=[UIColor whiteColor];
UILabel *lbltime=[[UILabel alloc]initWithFrame:CGRectMake(0, 0, 70, 60)];
lbltime.text=#"VIEW HERE";
lbltime.textColor=[UIColor grayColor];
// [timeView addSubview:lbltime];
[scrlView addSubview:timeView];
yPos=yPos+61;
}
Now when ever I tap on a subview I am not getting the tapped subview properties.
like coordinates. It is giving parent view coordinates
I enabled subview UserInteractionEnabled to Yes.
Can any one tell me how to get tapped subview coordinate and tag value?
DO NOT subclass from UIScrollView, that's exactly why there are gesture recognizers. Also, DO NOT add a separate gesture recognizer to each view.
Add one gesture recognizer to your scroll view, and when it's clicked use the x,y values of the touch to calculate which view was clicked.
You'll need to do a small calculation: (y value of the click + UIScrollView y offset) / 60.
60 is the height of each view. This should return the index of the clicked view.
EDIT:
Code example:
- (void)viewTapped:(UIGestureRecognizer*)recognizer
{
CGPoint coords = [recognizer locationInView:recognizer.view];
int clickedViewIndex = (self.offset.y + coords.y) / 60;
// now clickedViewIndex contains the index of the clicked view
}
UIView *v = recognizer.view;
int tagNum = [v tag];
Using the tagNum you can do your further operatins.
Or v is your object of tapped view.
UITapGestureRecognizer* tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(tap:)];
tap.numberOfTapsRequired = 1;
[timeview addGestureRecognizer:tap];
Add this in for loop only.
Make a class extending UIScrollView :
For example :
.h file :
#protocol CustomScrollViewDelegate <NSObject>
#optional
// optional becuase we always don't want to interact with ScrollView
- (void) customScrollViewTouchesEnded :(NSSet *)touches withEvent:(UIEvent *)event;
- (void) customScrollViewDidScroll;
#end
#interface CustomScrollView : UIScrollView
#property (weak, nonatomic) id<CustomScrollViewDelegate> touchDelegate;
// delegate was giving error becuase name is already there in UIScrollView
#end
Code for .m file :
#import "CustomScrollView.h"
#implementation CustomScrollView
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
// Initialization code
}
return self;
}
- (void) touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
NSLog(#"touchesEnded");
if (!self.dragging) {
//NSLog(#"touchesEnded in custom scroll view");
if (_touchDelegate != nil) {
if ([_touchDelegate respondsToSelector:#selector(customScrollViewTouchesEnded:withEvent:)]) {
[_touchDelegate customScrollViewTouchesEnded:touches withEvent:event];
}
}
}
else {
// it wouldn't be called becuase at the time of dragging touches responding stops.
[super touchesEnded:touches withEvent:event];
}
}
#end
Implementing this use subview of scroll view, it will work
for (UILabel *label in [customScrollView subviews]) { // change name of table here
if ([label isKindOfClass:[UILabel class]]) {
if (label.tag == savedtag) { // use your tag
// write the code as desired
}
}
}