I'm building a swipeable cell adding a pan gesture to that cell. Basically, it has the same look and feel as the cells in Mailbox app, where you have a top view which you can swipe to the left or right to show another view (revealView) underneath.
I wanted to build this with the reactive approach so the way I am doing it is:
First, when I setup the view and the pan gesture, I'm filtering the rac_gestureSignal to get the current state of the gesture and update the top view position with bindings (some implementation details are simplified here) as well as hiding/showing the revealView when the gesture is ended/cancelled. I also call setNeedsLayout when either panDirection or revealView change (in order to update revealView frame accordingly) merging the signals from their values, as well as remove the reveal view on cell reusing:
- (void)setupView
{
UIPanGestureRecognizer *panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:nil];
panGesture.delegate = self;
RACSignal *gestureSignal = [panGesture rac_gestureSignal],
*beganOrChangeSignal = [gestureSignal filter:^BOOL(UIGestureRecognizer *gesture) {
return gesture.state == UIGestureRecognizerStateChanged || gesture.state == UIGestureRecognizerStateBegan;
}],
*endOrCancelSignal = [gestureSignal filter:^BOOL(UIGestureRecognizer *gesture) {
return gesture.state == UIGestureRecognizerStateEnded || gesture.state == UIGestureRecognizerStateCancelled;
}];
RAC(self, contentSnapshotView.center) = [beganOrChangedSignal map:^id(id value) {
return [NSValue valueWithCGPoint:[self centerPointForTranslation:[panGesture translationInView:self]]];
}];
[beganOrChangeSignal subscribeNext:^(UIPanGestureRecognizer *panGesture) {
[self updateTopViewFrame];
[panGesture setTranslation:CGPointZero inView:self];
}];
[[endOrCancelSignal filter:^BOOL(UIPanGestureRecognizer *gestureRecognizer) {
return [self shouldShowRevealView];
}] subscribeNext:^(id x) {
[self showRevealViewAnimated:YES];
}];
[[endOrCancelSignal filter:^BOOL(UIPanGestureRecognizer *gestureRecognizer) {
return [self shouldHideRevealView];
}] subscribeNext:^(id x) {
[self hideRevealViewAnimated:YES];
}];
[[RACSignal merge:#[RACObserve(self, panDirection), RACObserve(self, revealView)]] subscribeNext:^(id x) {
[self setNeedsLayout];
}];
[[self rac_prepareForReuseSignal] subscribeNext:^(id x) {
[self.revealView removeFromSuperview];
self.revealView = nil;
}];
[self addGestureRecognizer:panGesture];
}
Then, I'm exposing a signal property (revealViewSignal) which will send YES/NO values when the reveal view shows/hides. Thus, you can subscribe to this signal and consequently act when the view changes his state. Internally, this signal will be a RACSubject sending next events after each show/hide animation ends:
- (void)showRevealViewAnimated:(BOOL)animated
{
[UIView animateWithDuration:animated ? 0.1 : 0.0
animations:^{
// SHOW ANIMATIONS
}
completion:^(BOOL finished) {
[(RACSubject *)self.revealViewSignal sendNext:#(YES)];
}];
}
- (void)hideRevealViewAnimated:(BOOL)animated
{
[UIView animateWithDuration:animated ? 0.1 : 0.0
animations:^{
// HIDE ANIMATIONS
}
completion:^(BOOL finished) {
[(RACSubject *)self.revealViewSignal sendNext:#(NO)];
}];
}
Everything works as expected but I was just wondering if this is a correct approach to build this kind of view in a RAC way. Also, there are two gesture recognizer delegate methods that I would love to setup in the same setup method above, but I wasn't able to figure out whether it's possible to use the rac_signalForSelector:fromProtocol: method here, so I ended up implementing them as always:
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
{
return [self checkIfGestureShouldBegin:gestureRecognizer];
}
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
return [self checkIfGestureShouldRecognize:gestureRecognizer];
}
Any help would be highly appreaciated, thanks!
Unfortunately there's currently no way to use RAC to implement a protocol method that returns a value.
It's a tricky problem since signals aren't required to send values synchronously, but obviously you need to return something when the delegate method is called. You probably don't want to block on the signal because it'd be easy to dead or live lock.
Related
I have a custom subclass of MKPinAnnotationView that displays a custom call out. I want to handle touch events inside that annotation.
I have a working solution (below) but it just doesn't feel right. I have a rule of thumb that whenever I use performSelector: .. withDelay: I'm fighting the system rather than working with it.
Does anyone have a good, clean workaround for the aggressive event handling of MKMapView and annotation selection handling?
My current solution:
(All code from my annotation selection class)
I do my own hit testing (without this my gesture recognisers don't fire as the Map View consumes the events:
- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent*)event; {
// To enable our gesture recogniser to fire. we have to hit test and return the correct view for events inside the callout.
UIView* hitView = nil;
if (self.selected) {
// check if we tpped inside the custom view
if (CGRectContainsPoint(self.customView.frame, point))
hitView = self.customView;
}
if(hitView) {
// If we are performing a gesture recogniser (and hence want to consume the action)
// we need to turn off selections for the annotation temporarily
// the are re-enabled in the gesture recogniser.
self.selectionEnabled = NO;
// *1* The re-enable selection a moment later
[self performSelector:#selector(enableAnnotationSelection) withObject:nil afterDelay:kAnnotationSelectionDelay];
} else {
// We didn't hit test so pass up the chain.
hitView = [super hitTest:point withEvent:event];
}
return hitView;
}
Note that I also turn off selections so that in my overridden setSelected I can ignore the deselection.
- (void)setSelected:(BOOL)selected animated:(BOOL)animated; {
// If we have hit tested positive for one of our views with a gesture recogniser, temporarily
// disable selections through _selectionEnabled
if(!_selectionEnabled){
// Note that from here one, we are out of sync with the enclosing map view
// we're just displaying out callout even though it thinks we've been deselected
return;
}
if(selected) {
// deleted code to set up view here
[self addSubview:_customView];
_mainTapGestureRecogniser = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(calloutTapped:)];
[_customView addGestureRecognizer: _mainTapGestureRecogniser];
} else {
self.selected = NO;
[_customView removeFromSuperview];
}
}
It's the line commented 1 that I don't like but it's also pretty hairy at the end of theta delayed fire. I have to walk back up the superview chain to get to the mapView so that I can convince it the selection is still in place.
// Locate the mapview so that we can ensure it has the correct annotation selection state after we have ignored selections.
- (void)enableAnnotationSelection {
// This reenables the seelction state and resets the parent map view's idea of the
// correct selection i.e. us.
MKMapView* map = [self findMapView];
[map selectAnnotation:self.annotation animated:NO];
_selectionEnabled = YES;
}
with
-(MKMapView*)findMapView; {
UIView* view = [self superview];
while(!_mapView) {
if([view isKindOfClass:[MKMapView class]]) {
_mapView = (MKMapView*)view;
} else if ([view isKindOfClass:[UIWindow class]]){
return nil;
} else{
view = [view superview];
if(!view)
return nil;
}
}
return _mapView;
}
This all seems to work without and downside (like flicker I've seen from other solutions. It's relatively straightforward but it doesn't feel right.
Anyone have a better solution?
I don't think you need to monkey with the map view's selection tracking. If I'm correctly interpreting what you want, you should be able to accomplish it with just the hitTest:withEvent: override and canShowCallout set to NO. In setSelected: perform your callout appearance/disappearance animation accordingly. You should also override setHighlighted: and adjust the display of your custom callout if visible.
I added a swipe gesture recognizer and a pan gesture recognizer to the same view. These gestures should be exclusive to each other.
In order to do this I added the constraint on the swipe gesture
[swipeGesture requireGestureToFail:panGesture];
(because the pan gesture should get precedence)
Problem is that the pan gesture is always invoked - even during a very fast swipe.
In order to over come this I set myself as the pan gesture's delegate. In the delegate method I set up some code as follows:
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
{
// check if it is the relevant view
if (gestureRecognizer.view == self.myViewWithTwoGestures)
{
// check that it is the pan gesture
if ([gestureRecognizer isKindOfClass:[UIPanGestureRecognizer class]])
{
UIPanGestureRecognizer *pan = (UIPanGestureRecognizer *)gestureRecognizer;
CGPoint velocity = [pan velocityInView:gestureRecognizer.view];
// added an arbitrary velocity for failure
if (ABS(velocity.y) > 100)
{
// fail if the swipe was fast enough - this should allow the swipe gesture to be invoked
return NO;
}
}
}
return YES;
}
Is there a suggested velocity to ensure good behavior? Is there another way to force the pan gesture to fail?
According to Apple's documentation here (under Declaring a Specific Order for Two Gesture Recognizers) the way to get both UIPanGestureRecognizer and UISwipeGestureRecognizer to work on the same view is by requiring the UISwipeGesureRecognizer to fail before calling the UIPanGestureRecognizer (the opposite of what you wrote). This probably has something to do with the fact the a swipe gesture is also a pan gesture but the opposite is not necessarily true (see this SO question).
I wrote this little piece of code and it manages to recognize both pan and swipe gestures:
UIPanGestureRecognizer * pan = [[UIPanGestureRecognizer alloc]initWithTarget:self action:#selector(panned:)];
UISwipeGestureRecognizer * swipe = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:#selector(swiped:)];
[pan requireGestureRecognizerToFail:swipe];
swipe.direction = (UISwipeGestureRecognizerDirectionLeft | UISwipeGestureRecognizerDirectionRight);
-(void)panned:(UIPanGestureRecognizer *)gesture
{
NSLog(#"Pan");
}
-(void)swiped:(UISwipeGestureRecognizer *)gesture
{
NSLog(#"Swipe");
}
This doesn't work as well as you'd hope (since you need the swipe gesture to fail there's a small delay before the pan gesture starts) but it does work.
The code you posted however gives you the ability to fine tune the gestures to your liking.
Late response, but I was having a similar issue where I wanted to pan to be recognized before the swipe. The only way I could get it working was to use a long press (or something similar) to set a flag to use the pan gesture as a pan or a swipe. I ended up not using swipes at all. I.e.:
- (void) handleLongPress : (UILongPressGestureRecognizer *) gestureRecognizer
{
if (gestureRecognizer.state == UIGestureRecognizerStateBegan)
{
_canSwipe = YES;
}
else if (gestureRecognizer.state == UIGestureRecognizerStateEnded)
{
_canSwipe = NO;
}
}
- (void) handleDragging : (id) sender
{
UIPanGestureRecognizer *pan = (UIPanGestureRecognizer *)sender;
GLKVector2 dragDelta = GLKVector2Make(0., 0.);
if (pan.state == UIGestureRecognizerStateBegan || pan.state == UIGestureRecognizerStateChanged)
{
_mousePosition = [pan translationInView:self.view];
if (_beginDragging == NO)
{
_beginDragging = YES;
}
else
{
dragDelta = GLKVector2Make(_mousePosition.x - _prevMousePosition.x, _mousePosition.y - _prevMousePosition.y);
}
_prevMousePosition = _mousePosition;
}
else
{
_beginDragging = NO;
}
if (_canSwipe == YES)
{
if (dragDelta.x > 0)
{
_canSwipe = NO;
[self.navigationController popToRootViewControllerAnimated:YES];
NSLog(#"swipe right");
}
else if (dragDelta.x < 0)
{
_canSwipe = NO;
[self performSegueWithIdentifier:#"toTableSegue" sender:pan];
NSLog(#"swipe left");
}
}
else
{
_dragDeltaTranslation = GLKVector2Make(dragDelta.x/90, dragDelta.y/90);
_translationXY = GLKVector2Make(_translationXY.x + _dragDeltaTranslation.x, _translationXY.y - _dragDeltaTranslation.y);
}
}
So essentially:
Use long press (or some other mechanism) to activate a state of swiping (long press is nice because as soon as you release, the state goes to UIGestureRecognizerStateEnded)
Then use the pan direction to determine the direction of the swipe.
2.
I have a really interesting issue with UIPageViewController.
My project is set up very similarly to the example Page Based Application template.
Every now and then (but reproducible to a certain extent) a certain pan gesture will call out to -(UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerAfterViewController:(UIViewController *)viewController.
I return the viewcontroller for the next page, but a page flip animation is never ran and my delegate method is never called.
Here is the code for viewControllerAfterViewController
-(UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerAfterViewController:(UIViewController *)viewController
{
PageDisplayViewController *vc = (PageDisplayViewController *)viewController;
NSUInteger index = [self.pageFetchController.fetchedObjects indexOfObject:vc.page];
if(index == (self.pageFetchController.fetchedObjects.count - 1)) return nil;
return [self getViewControllerForIndex:(++index)];
}
Here is the getViewControllerForIndex:
-(PageDisplayViewController *)getViewControllerForIndex:(NSUInteger)index
{
PageDisplayViewController *newVC = [self.storyboard instantiateViewControllerWithIdentifier:#"PageDisplayController"];
newVC.page = [self.pageFetchController.fetchedObjects objectAtIndex:(index)];
newVC.view.frame = CGRectMake(0, 0, 1024, 604);
NSLog(#"%i", index);
if(index == 0)
{
//We're moving to the first, animate the back button to be hidden.
[UIView animateWithDuration:0.5 animations:^
{
self.backButton.alpha = 0.f;
} completion:^(BOOL finished){
self.backButton.hidden = YES;
}];
}
else if(index == (self.pageFetchController.fetchedObjects.count - 1))
{
[UIView animateWithDuration:0.5 animations:^{
self.nextButton.alpha = 0.f;
} completion:^(BOOL finished){
self.nextButton.hidden = YES;
}];
}
else
{
BOOL eitherIsHidden = self.nextButton.hidden || self.backButton.hidden;
if(eitherIsHidden)
{
[UIView animateWithDuration:0.5 animations:^{
if(self.nextButton.hidden)
{
self.nextButton.hidden = NO;
self.nextButton.alpha = 1.f;
}
if(self.backButton.hidden)
{
self.backButton.hidden = NO;
self.backButton.alpha = 1.f;
}
}];
}
}
return newVC;
}
Basically, I create the view controller, set it's data object, then fade a next/back button out depending on the index.
Delegate method
-(void)pageViewController:(UIPageViewController *)pageViewController didFinishAnimating:(BOOL)finished previousViewControllers:(NSArray *)previousViewControllers transitionCompleted:(BOOL)completed
{
PageDisplayViewController *vc = [previousViewControllers lastObject];
NSUInteger index = [self.pageFetchController.fetchedObjects indexOfObject:vc.page];
if (!completed)
{
[self.pagePreviewView setCurrentIndex:index];
NSLog(#"Animation Did not complete, reverting pagepreview");
}
else
{
PageDisplayViewController *curr = [pageViewController.viewControllers lastObject];
NSUInteger i = [self.pageFetchController.fetchedObjects indexOfObject:curr.page];
[self.pagePreviewView setCurrentIndex:i];
NSLog(#"Animation compeleted, updating pagepreview. Index: %u", i);
}
}
I only noticed this issue because randomly, my back button would reappear on screen. After tossing some NSLog() statements in there, I notice that my dataSource method gets called for an index of 1, but no animation ever plays or delegate gets called. Whats even scarier, is that if I try to pan the next page, index 1 gets called for AGAIN.
I fear this may be a bug with the UIPageViewController.
Since I was still receiving mysterious crashes with the implementation in my first answer, I kept searching for a "good enough" solution which depends less on personal assumptions about the page view controller's (PVC) underlying behavior. Here is what I managed to come up with.
My former approach was kind of intrusive and was more of a workaround than an acceptable solution. Instead of fighting the PVC to force it to do what I thought it was supposed to do, it seems that it's better accept the facts that:
the pageViewController:viewControllerBeforeViewController: and pageViewController:viewControllerAfterViewController: methods can be called an arbitrary number of times by UIKit, and
there is absolutely no guarantee that either of these correspond to a paging animation, nor that they will be followed by a call to pageViewController:didFinishAnimating:previousViewControllers:transitionCompleted:
That means we cannot use the before/after methods as "animation-begin" (note, however, that didFinishAnimating still serves as "animation-end" event). So how do we know an animation has indeed started?
Depending on our needs, we may be interested in the following events:
the user begins fiddling with the page:
A good indicator for this is the before/after callbacks, or more precisely the first of them.
first visual feedback of the page turning gesture:
We can use KVO on the state property of the tap and pan gesture recognizers of the PVC. When a UIGestureRecognizerStateBegan value is observed for panning, we can be pretty sure that visual feedback will follow.
the user finishes dragging the page around by releasing the touch:
Again, KVO. When the UIGestureRecognizerStateRecognized value is reported either for panning or tapping, it is when the PVC is actually going to turn the page, so this may be used as "animation-begin".
UIKit starts the paging animation:
I have no idea how to get a direct feedback for this.
UIKit concludes the paging animation:
Piece of cake, just listen to pageViewController:didFinishAnimating:previousViewControllers:transitionCompleted:.
For KVO, just grab the gesture recognizers of the PVC as below:
#interface MyClass () <UIGestureRecognizerDelegate>
{
UIPanGestureRecognizer* pvcPanGestureRecognizer;
UITapGestureRecognizer* pvcTapGestureRecognizer;
}
...
for ( UIGestureRecognizer* recognizer in pageViewController.gestureRecognizers )
{
if ( [recognizer isKindOfClass:[UIPanGestureRecognizer class]] )
{
pvcPanGestureRecognizer = (UIPanGestureRecognizer*)recognizer;
}
else if ( [recognizer isKindOfClass:[UITapGestureRecognizer class]] )
{
pvcTapGestureRecognizer = (UITapGestureRecognizer*)recognizer;
}
}
Then register your class as observer for the state property:
[pvcPanGestureRecognizer addObserver:self
forKeyPath:#"state"
options:NSKeyValueObservingOptionNew
context:NULL];
[pvcTapGestureRecognizer addObserver:self
forKeyPath:#"state"
options:NSKeyValueObservingOptionNew
context:NULL];
And implement the usual callback:
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
{
if ( [keyPath isEqualToString:#"state"] && (object == pvcPanGestureRecognizer || object == pvcTapGestureRecognizer) )
{
UIGestureRecognizerState state = [[change objectForKey:NSKeyValueChangeNewKey] intValue];
switch (state)
{
case UIGestureRecognizerStateBegan:
// trigger for visual feedback
break;
case UIGestureRecognizerStateRecognized:
// trigger for animation-begin
break;
// ...
}
}
}
When you are done, don't forget to unsubscribe from those notifications, otherwise you may get leaks and strange crashes in your app:
[pvcPanGestureRecognizer removeObserver:self
forKeyPath:#"state"];
[pvcTapGestureRecognizer removeObserver:self
forKeyPath:#"state"];
That's all folks!
Please look at my other answer in the first place, this one has serious flaws but I'm leaving it here as it might still help someone.
First off, a disclaimer: The following solution is a HACK. It does work in the environment I tested but there is no guarantee that it works in yours nor that it won't be broken by the next update. So proceed with care.
TL;DR: grab the UIPanGestureRecognizer of the UIPageViewController and hijack its delegate calls but keep forwarding them to the original target.
Longer version:
My findings on the issue: the UIPageViewController shipped in iOS 6 is different in behavior to the one in iOS 5 in that it may call the pageViewController:viewControllerBeforeViewController: on its datasource even if there is no page turning going on in any sense (read: no tap, swipe, or valid direction-matching panning has been recognized). This, of course, breaks our former assumption that the before/after calls are equivalent to an "animation begin" trigger and are consistently followed by a pageViewController:didFinishAnimating:previousViewControllers:transitionCompleted: call to the delegate. (Eventually this is a bold assumption to make but I guess I was not alone with that.)
I found out that the extra calls to the datasource are likely to happen when the default UIPanGestureRecognizer on the page view controller starts to recognize a pan gesture that in the end doesn't match the direction of the controller (e.g. vertical panning in a horizontally paging PVC). Interestingly enough, in my environment it was always the "before" method which got hit, never the "after". Others suggested interfereing with the gesture recognizer's delegate but that didn't work for me the way it was described there so I kept experimenting.
Finally I found a workaround. First we grab the pan gesture recognizer of the page view controller:
#interface MyClass () <UIGestureRecognizerDelegate>
{
UIPanGestureRecognizer* pvcPanGestureRecognizer;
id<UIGestureRecognizerDelegate> pvcPanGestureRecognizerDelegate;
}
...
for ( UIGestureRecognizer* recognizer in pageViewController.gestureRecognizers )
{
if ( [recognizer isKindOfClass:[UIPanGestureRecognizer class]] )
{
pvcPanGestureRecognizer = (UIPanGestureRecognizer*)recognizer;
pvcPanGestureRecognizerDelegate = pvcPanGestureRecognizer.delegate;
pvcPanGestureRecognizer.delegate = self;
break;
}
}
Then we implement the UIGestureRecognizerDelegate protocol in our class:
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
{
if ( gestureRecognizer == pvcPanGestureRecognizer &&
[pvcPanGestureRecognizerDelegate respondsToSelector:#selector(gestureRecognizer:shouldReceiveTouch:)] )
{
return [pvcPanGestureRecognizerDelegate gestureRecognizer:gestureRecognizer
shouldReceiveTouch:touch];
}
return YES;
}
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
if ( gestureRecognizer == pvcPanGestureRecognizer &&
[pvcPanGestureRecognizerDelegate respondsToSelector:#selector(gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer:)] )
{
return [pvcPanGestureRecognizerDelegate gestureRecognizer:gestureRecognizer
shouldRecognizeSimultaneouslyWithGestureRecognizer:otherGestureRecognizer];
}
return NO;
}
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
{
if ( gestureRecognizer == pvcPanGestureRecognizer &&
[pvcPanGestureRecognizerDelegate respondsToSelector:#selector(gestureRecognizerShouldBegin:)] )
{
return [pvcPanGestureRecognizerDelegate gestureRecognizerShouldBegin:gestureRecognizer];
}
return YES;
}
Apparently, the methods don't do anything sensible, they just forward the invocations to the original delegate (making sure that that actually implements them). Still, this forwarding seems to be sufficient for the PVC to behave and not call the datasource when there is no need to.
This workaround fixed the issue for me on devices running iOS 6. Code which was compiled with the iOS 6 SDK but with a deployment target of iOS 5 had already run flawlessly on 5.x devices, so the fix is not necessary there but according to my tests it doesn't do any harm either.
Hope someone finds this useful.
I have tried your solution and it came almost working, but still with some issues. The best solution came with adding method
- (void)pageViewController:(UIPageViewController *)pageViewController willTransitionToViewControllers:(NSArray *)pendingViewControllers
which is available starting from iOS 6 and it is required for it. If not to implement it, issues may occur with those gestures. Implementing it helped to solve major part of issues.
try this...
- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerBeforeViewController:(UIViewController *)viewController
{
for (UIGestureRecognizer *gr in pageViewController.gestureRecognizers) {
if([gr isKindOfClass:[UIPanGestureRecognizer class]])
{
UIPanGestureRecognizer *pgr = (UIPanGestureRecognizer*)gr;
CGPoint velocity = [pgr velocityInView:pageViewController.view];
BOOL verticalSwipe = fabs(velocity.y) > fabs(velocity.x);
if(verticalSwipe)
return nil;
}
}
....
}
I have a view with several UIButtons. I have successfully implemented using UILongPressGestureRecognizer with the following as the selector;
- (void)longPress:(UILongPressGestureRecognizer*)gesture {
if ( gesture.state == UIGestureRecognizerStateEnded ) {
NSLog(#"Long Press");
}
}
What I need to know within this method is which UIButton received the longpress since I need to do something different, depending on which button received the longpress.
Hopefully the answer is not some issue of mapping the coordinates of where the longpress occured to the bounds of the buttons - would rather not go there.
Any suggestions?
Thanks!
This is available in gesture.view.
Are you adding the long tap gesture controller to the UIView that has the UIButtons as subviews? If so, something along the lines of #Magic Bullet Dave's approach is probably the way to go.
An alternative is to subclass UIButton and add to each UIButton a longTapGestureRecogniser. You can then get your button to do what you like. For example, it could send a message identifying itself to a view controller. The following snippet illustrates methods for the subclass.
- (void) setupLongPressForTarget: (id) target;
{
[self setTarget: target]; // property used to hold target (add #property and #synthesise as appropriate)
UILongPressGestureRecognizer* longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:button action:#selector(longPress:)];
[self addGestureRecognizer:longPress];
[longPress release];
}
- (void) longPress: (UIGestureRecognizer*) recogniser;
{
if (![recogniser isEnabled]) return; // code to prevent multiple long press messages
[recogniser setEnabled:NO];
[recogniser performSelector:#selector(setEnabled:) withObject: [NSNumber numberWithBool:YES] afterDelay:0.2];
NSLog(#"long press detected on button");
if ([[self target] respondsToSelector:#selector(longPressOnButton:)])
{
[[self target] longPressOnButton: self];
}
}
In your view controller you might have code something like this:
- (void) viewDidLoad;
{
// set up buttons (if not already done in Interface Builder)
[buttonA setupLongPressForTarget: self];
[buttonB setupLongPressForTarget: self];
// finish any other set up
}
- (void) longPressOnButton: (id) sender;
{
if (sender = [self buttonA])
{
// handle button A long press
}
if (sender = [self buttonB])
{
// handle button B long press
}
// etc.
}
If your view contains multiple subViews (like lots of buttons) you can determine what was tapped:
// Get the position of the point tapped in the window co-ordinate system
CGPoint tapPoint = [gesture locationInView:nil];
UIView *viewAtBottomOfHeirachy = [self.window hitTest:tapPoint withEvent:nil];
if ([viewAtBottomOfHeirachy isKindOfClass:[UIButton class]])
I have to use swipe functionality in my view controller.
so, whenever Iam swiping, my swipe method is getting called twice and the NSlogs which I Wrote inside the (swipe:) method is displaying the content two times.
Here is the code which i have used.
UIView *swipeView=[[UIView alloc]initWithFrame:CGRectMake(405, 420, 265, 35)];
swipeView.backgroundColor=[UIColor clearColor];
[self.view addSubview:swipeView];
UISwipeGestureRecognizer *gesture;
gesture=[[UISwipeGestureRecognizer alloc]initWithTarget:self action:#selector(swipe:)];
[gesture setDirection:(UISwipeGestureRecognizerDirectionRight)];
[swipeView addGestureRecognizer:gesture];
[gesture release];
[swipeView release];
-(void)swipe:(UISwipeGestureRecognizer *)recognizer {
NSLog(#"Swipe received.");
NSLog(#"HIJ");
}
please tell me what i have to do for calling it only one time.
That's what's supposed to happen. You need to look at the state property where you'll find things like UIGestureRecognizerStateBegan and UIGestureRecognizerStateEnded.
Try this, recognizer has various state like
UIGestureRecognizerStatePossible,
UIGestureRecognizerStateBegan,
UIGestureRecognizerStateChanged,
UIGestureRecognizerStateEnded,
UIGestureRecognizerStateCancelled,
UIGestureRecognizerStateFailed,
UIGestureRecognizerStateRecognized = UIGestureRecognizerStateEnded
-(void)swipe:(UISwipeGestureRecognizer *)recognizer {
if (recognizer.state == UIGestureRecognizerStateEnded) {
NSLog(#"Swipe received.");
NSLog(#"HIJ");
}
}