I have a label, which I want to "swipe to delete". What I want is:
If the user touches the label, and starts dragging it to the right, the label is moving along with the finger
The more the label moves to the right, the less alpha it gets
When it reaches alpha 0, fire a method.
Any idea what the best way to implement this is?
Thanks in advance!
You'll want to use a UIPanGestureRecognizer to do this. When the gesture recognizer starts, you'll want to keep track of the starting point. You'll also want to define some amount to pan over that is considered all the way over. As the pan happens, you'll see how far over your touch has moved and move the label by that amount. You'll also determine what percentage of the way over that you've moved thru your "all the way over" distance, and set the alpha accordingly.
Once you reach the spot, you can cancel the gesture recognizer (setting its enabled property to NO) and do whatever you want to do at that point. If the user releases their touch (so the gesture recognizer ends before they drag all of the way) you'll obviously want to reset the label position and alpha at that point.
You may also want to take into account the velocity of the pan at the time it ends, and if it's over a certain velocity, go ahead and make it continue animating to the finished state at that velocity, otherwise if it's not fast enough, make it animate back to the starting state. But you may want to only bother with this after you initially implement it to see if you want this or not.
Put this in your UIViewController.
WARNING: I typed all this without XCode and never tested it. You may need to fix spelling errors and adjust numbers.
// Declare this in the anonymous category of your UIViewController
#property(nonatomic, strong) UILabel* label;
- (void)didSwipeLabel:(UISwipeGestureRecognizer*)swipe;
- (void)willRemoveLabel;
// Put the following in the usual places in .m file
- (void)viewDidLoad {
[super viewDidLoad];
self.label = [UILabel alloc] init];
self.label.font = [UIFont boldSystemFontOfSize:30.0];
self.label.text = #"SWIPE THIS LABEL TO CHANGE THE ALPHA";
[self.label sizeToFit];
[self.label addGestureRecognizer:[[UISwipeGestureRecognizer alloc] initWithTarget:self action:#selector(didSwipeLabel:)]];
[self.view addSubview:self.label];
}
- (void)didSwipeLabel:(UISwipeGestureRecognizer*)swipe
{
// The value of 0.1 needs to be adjusted. Most likely it needs
// to be decreased.
if (swipe.direction == UISwipeGestureRecognizerDirectionRight) {
self.label.alpha = self.label.alpha - 0.1;
} else if (swipe.direction == UISwipeGestureRecognizerDirectionLeft) {
self.label.alpha = self.label.alpha + 0.1;
}
if (self.label.alpha <= 0.0) {
[self willRemoveLabel];
}
}
- (void)willRemoveLabel
{
NSLog(#"label should be removed");
}
Related
Before any starts, I understand yourView.userInteractionEnabled = NO; is an option, but let me explain first the circumstance.
I have these 2 UIView objects, stoneOne and stoneTwo. I have 4 UISwipeGestureRecognizer objects attached to them for up, down, left and right. Imagine swiping these 'stones' around a 5x5 grid.
What I don't want is to be able to swipe both at the same time.
Currently, that bug is still an issue. I'll show you a method called swipeLeft: which represents the layout for all swipe directions.
- (IBAction)swipeLeft:(UISwipeGestureRecognizer *)recognizer {
_oldMove1 = _move1;
_oldMove2 = _move2;
if (recognizer.view == _oneStone
&& recognizer.direction == UISwipeGestureRecognizerDirectionLeft) {
_twoStone.userInteractionEnabled = NO;
_oneStone = recognizer.view;
[self moveOne:CGPointMake(-1, 0) withView:_oneStone];
self.move1++;
// 'causeADelay:' runs _twoStone.userInteractionEnabled = YES;
[self performSelector:#selector(causeADelay:) withObject:_twoStone afterDelay:1];
} else if (recognizer.view == _twoStone
&& recognizer.direction == UISwipeGestureRecognizerDirectionLeft) {
_oneStone.userInteractionEnabled = NO;
_twoStone = recognizer.view;
[self moveTwo:CGPointMake(-1, 0) withView:_twoStone];
self.move2++;
[self performSelector:#selector(causeADelay:) withObject:_oneStone afterDelay:1];
}
self.moveCount++;
}
One of the things I tried was creating a delay on when I can interact with my UIView objects. This worked ONLY IF I waited a split half-second to interact. The full delay would occur, and everything would work.
My bug is when you swipe them both at the same time. Is that because of the swipe gestures attached to them?
I also tried removing and reapplying the objects as subviews...didn't work, obviously. I really need this to work otherwise I have a dead-end game. I was very new to coding when I first started and never thought about Cocos2d or other game-driven platforms for development, so everything I have came from on-the-fly thinking.
There are several solutions, but here's a particularly easy one:
Remove the swipe gesture recognizers from the stones and attach them instead to the common superview of the stones. This solves the problem, because only one gesture recognizer on the same view will recognize at any one time.
Of course, you will now have to use hit-testing to find out which stone (if any) is being swiped. But that's an easy implementation detail, and is a small price to pay.
And of course another cool feature is that you now only need four gesture recognizers total!
iOS7 & iOS8
I need to disable 2 or three fingers scrolling in UIScrollview.
I tried :
[self.scrollView.panGestureRecognizer setMaximumNumberOfTouches:1];
[self.scrollView.panGestureRecognizer setMinimumNumberOfTouches:1];
But it has no effect. It is still possible to scroll with 2 fingers.
If i tried to set max and min to 2. One finger scrolling was disabled but 3 fingers scrolling possible :(
I tried this too, but without success:
for (UIGestureRecognizer* pan in self.scrollView.gestureRecognizers) {
OTTNSLog(#"touches: %ld", (unsigned long)pan.numberOfTouches);
if ([pan isKindOfClass:[UIPanGestureRecognizer class]])
{
UIPanGestureRecognizer *mpanGR = (UIPanGestureRecognizer *) pan;
mpanGR.minimumNumberOfTouches = 1;
mpanGR.maximumNumberOfTouches = 1;
}
if ([pan isKindOfClass:[UISwipeGestureRecognizer class]])
{
UISwipeGestureRecognizer *mswipeGR = (UISwipeGestureRecognizer *) pan;
mswipeGR.numberOfTouchesRequired = 1;
}
}
Does anybody know, how it solve this ?
Thanks.
PROBLEM:
When the UIPanGestureRecognizer is underlying a UIScrollView - which unfortunately does also effect UIPageViewController - the maximumNumberOfTouches is not behaving as expected, the minimumNumberOfTouches however always limits the lower end correctly.
When monitoring these parameters they report back correct values - they seem to do their job - it's just that UIScrollView itself doesn't honor them and ignores their settings!
SOLUTION:
Set the minimumNumberOfTouches to the desired value e.g. 1 and - very importantly - the maximumNumberOfTouches to 2 !!!
myScrollView.panGestureRecognizer.minimumNumberOfTouches = 1;
myScrollView.panGestureRecognizer.maximumNumberOfTouches = 2;
Conform to the UIGestureRecognizerDelegate protocol in your scrollView's #interface declaration. You don't have to set the panGestureRecognizer.delegate for a UIScrollView!!! The delegate is already set because UIScrollView requires to be the delegate of its own pan/pinchGestureRecognizer.
Then implement the UIGestureRecognizer delegate method:
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer {
NSLog(#"%d", gestureRecognizer.numberOfTouches);
NSLog(#"%#", gestureRecognizer.description);
if (gestureRecognizer == self.panGestureRecognizer) {
if (gestureRecognizer.numberOfTouches > 1) {
return NO;
} else {
return YES;
}
} else {
return YES;
}
}
}
AN EVEN SAFER VERSION:
If you have a custom scrollView class and wanna be on the VERY safe side you can also add one more line of code to disambiguate against other scrollViews:
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer {
NSLog(#"%d", gestureRecognizer.numberOfTouches);
NSLog(#"%#", gestureRecognizer.description);
if ([gestureRecognizer.view isMemberOfClass:[MY_CustomcrollView class]]) {
if (gestureRecognizer == self.panGestureRecognizer) {
if (gestureRecognizer.numberOfTouches > 1) {
return NO;
} else {
return YES;
}
} else {
return YES;
}
} else {
return YES;
}
}
ADDITIONAL INFORMATION:
The NSLogs tell you the number of touches. If you set both the min and max to the same value (like 1 in the example above) the if-loop would never be triggered... ;-)
That is why maximumNumberOfTouches has to be at least minimumNumberOfTouches + 1
panGestureRecognizer.minimumNumberOfTouches = 1;
panGestureRecognizer.maximumNumberOfTouches = minimumNumberOfTouches + 1;
GEEK SECTION:
for (UIView *view in self.view.subviews) {
if ([view isKindOfClass:[UIScrollView class]]) {
NSLog(#"myPageViewController - SCROLLVIEW GESTURE RECOGNIZERS: %#", view.gestureRecognizers.description);
((UIPanGestureRecognizer *)view.gestureRecognizers[1]).minimumNumberOfTouches = 1;
((UIPanGestureRecognizer *)view.gestureRecognizers[1]).maximumNumberOfTouches = 2;
}
}
This is the way to access the underlying scrollView that is responsible for the paging of a UIPageViewController. Put this code in the e.g. viewDidLoad: of the UIPageViewController (self).
If you don't have access to the scrollView at all - like for a UIPageViewController in a dynamic UITableViewCell where creation and cell reuse happens at runtime and no outlets can be set on its contentViews - put a category on UIScrollView and override the delegate method there. But be careful! This effects every scrollView in your application - so do proper introspection (class-checking) like in my 'EVEN SAFER' example above... ;-)
SIDENOTE:
Unfortunately the same trick doesn't work with the pinchGestureRecognizer on UIScrollView because it doesn't expose a min/maxNumberOfTouches property. When monitored it always reports 2 touches (which you obviously need to pinch) - so its internal min/maxNumberOfTouches seem to have been set both to 2 - even if UIScrollView isn't honoring its own settings and keeps happily pinching with any numbers of fingers (more than 2). So there is no way to restrict pinching to a limited amount of fingers...
I can confirm this is still an issue in iOS 8, but only when the UIPanGestureRecognizer is underlying a UIScrollView. Creating a UIView with a fresh UIPanGestureRecognizer and setting its maximumNumberOfTouches property works as expected.
Interesting note: if you query the UIScrollView's UIPanGestureRecognizer while you're scrolling, it reports the number of touches as less than or equal to the maximum. In other words, if you set the maximum to 2, and scroll with 3 fingers, it reports the gesture as a 2-finger scroll. Subsequently letting up fingers one at a time while continuing to scroll usually (but not consistently) reduces the reported number of touches as well — so if you go from 3 -> 2 -> 1 fingers, it will register 2 -> 1 -> 0 touches, and stop scrolling while you still have 1 finger on the device.
Submitted rdar://20890684 and copied to http://openradar.appspot.com/radar?id=6191825677189120. Please feel free to dupe.
There is no specific method available for this just do some tricks but results are only 75%.
Add swipe gestures(4 directions each) and double tap gesture to your UIScrollview..Then use below code..
[[yourScrollView panGestureRecognizer] requireGestureRecognizerToFail:swipeDown];
[[yourScrollView panGestureRecognizer] requireGestureRecognizerToFail:swipeLeft];
[[yourScrollView panGestureRecognizer] requireGestureRecognizerToFail:swipeRight];
[[yourScrollView panGestureRecognizer] requireGestureRecognizerToFail:swipeUp];
[[yourScrollView panGestureRecognizer] requireGestureRecognizerToFail:doubleTap];
This will do the job for you:
yourscrollView.multipleTouchEnabled=NO;
I found some questions and answers here on stackoverflow for that problem, but none of the solutions there solved my problem.
My iOS App has the ability to play some music with a nice music player. I designed it with Xcode's Interface Builder and dragged out a UIView and changed its class to MPVolumeView. Everything works fine when I'm debugging my app on my iPhone 6.
Here is my problem: I also dragged out a UITapGestureRecognizer on my whole view which contains my controls like
play/pause, next/previous track (...)
and also my MPVolumeView. When I tap on that view it should fade out and disappear. Then I added a UITapGestureRecognizer on my UIImageView which shows my artwork image of the song. When I tap this image view, it should fade in my view with all controls in int - that's working properly.
BUT: When I slide the knob of the volume slider just a little bit, or if I am just touching it, the view still disappears. It seems like my MPVolumeView is forwarding my touch or something like that. I tried setting userInteractionEnabled = false on my volume slider, but that didn't help. I also set the delegate of my gesture recognizer to self and added the
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
NSLog(#"tapped");
if([gestureRecognizer.view isMemberOfClass:[UIImageView class]]) {
return true;
}
return false;
}
function to my code, which returns true or false, depending on which view I'm tapping. When I'm accessing the gestureRecognizer.view property, it doesn't recognize my MPVolumeView, just the UIView in the background.
Here my two methods which are fired after when the TapGestureRecognizers are fired:
- (IBAction)overlayViewTapped:(UITapGestureRecognizer *)sender {
if(sender.state == UIGestureRecognizerStateEnded) {
[UIView animateWithDuration:0.3
delay:0.0
options:UIViewAnimationOptionAllowUserInteraction
animations:^{ self.blackOverlayView.alpha = 0.0; self.normalTimeLabel.alpha = 1.0; }
completion:nil];
}
}
- (IBAction)imageViewTapped:(UITapGestureRecognizer *)sender {
[UIView animateWithDuration:0.3
delay:0.0
options:UIViewAnimationOptionAllowUserInteraction
animations:^{ self.blackOverlayView.alpha = 1.0; self.normalTimeLabel.alpha = 0.0; }
completion:nil];
}
Please help me, I'm nearly going nuts with that ..
EDIT: My music player looks like this:
After I tap anywhere on the view (except the subviews), the view should fade out and hide everything, just show the artwork image of the song and the current elapsed time. This will look like this:
As I said - the problem is, if I just tap the volume slider or slide it just a little bit, my UITapGestureRecognizer fires and fades out my complete view. How can I prevent that?
It is behaving the way it is simply because you added the gesture recognizer to the entire UIView, which includes the volume slider and whatnot.
Instead of detecting the touch in the entire view, check to see if the touch is in the area you want it.
Create a CGRect property, I'll call it touchArea:
#property CGRect touchArea;
Then specify the size of the touchArea (you can do this in the viewDidLoad):
touchArea = CGRectMake(0.0, 240.0, 320.0, 240.0);
You will have to find out where you want this and how big it should be and replace my example values with the real ones. A simple way of cheating this is to take something like a UILabel in IB and positioning and sizing it to your desire, then go to the size inspector pane and get the x, y, width and height values.
Then, before you do your fade animation, check to see if the touch was in the touchArea:
- (void)handleGesture:(UIGestureRecognizer *)gestureRecognizer
{
CGPoint touchPoint = [gestureRecognizer locationInView:self.view];
if (CGRectContainsPoint(touchArea, touchPoint))
{
//do your animation here.
}
}
As a note, I would set a BOOL to check whether or not the view is faded in or out, so you can always check before animating.
I have multiple controllers in my PageViewController and in one controller I have a few sliders. Now there is a problem that user must touch exactly slider circle (I am not sure about right expression, thumb? - that moving part) and I would like to increase area in which reacts slider and not the whole PageViewController. I tried these solutions but it doesn't help:
thumbRectForBounds:
- (CGRect)thumbRectForBounds:(CGRect)bounds trackRect:(CGRect)rect value:(float)value
{
return CGRectInset ([super thumbRectForBounds:bounds trackRect:rect value:value], 15, 15);
}
Increase hitTest area:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
if (CGRectContainsPoint(CGRectInset(self.frame, 200, 200), point) || CGRectContainsPoint(CGRectInset(self.frame, 200, 200), point)) {
return self;
}
return [super hitTest:point withEvent:event];
}
I have these methods in my custom slider class because I would like to reuse this. Last thing what I found and not tried yet is create some object layer over slider which "takes" gesture and disable PageViewController but I am not sure how to do it and I am not sure if it's good/best solution.
I am not a big fan of the UISlider component because as you noticed, it is not trivial to increase the hit area of the actual slider. I would urge you to replicate the UISlider instead using a pan gesture for a much better user experience:
i. create a slider background with a seperate UIImageView with a slider image.
ii. create the PanGesture:
UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(handlePan:);
[imageView addGestureRecognizer:pan];
iii. implement handlePan Method:
- (IBAction)handlePan:(UIPanGestureRecognizer *)recognizer {
//pan (slide) begins
CGPoint translation = [recognizer locationInView:self.view];
translation.y = self.slideImage.center.y;
self.slideImage.center = translation;
if(recognizer.state == UIGestureRecognizerStateEnded) {
LTDebugLog(#"\n\n PAN, with spot: %f\n\n", self.slideImage.center.x);
//do something after user is done sliding
}
}
The big benefit of this method is that you will have a much better user experience as you can make the responsive UIImageView as big as you want.
Alternatively, you could subclass a UISlider and increase the hit space there, although in my experience this gives mixed results.
Hope this helps
In your CustomSlider class override thumbRectForBounds method:
Simply return rect value as you required:
- (CGRect)thumbRectForBounds:(CGRect)bounds trackRect:(CGRect)rect value:(float)value
{
return CGRectMake (bounds.origin.x, bounds.origin.y, yourWidthValue, yourHeightValue );
}
Change yourWidthValue and yourHeightValue as per your requirement. And then while using
Create object like below:
CustomSlider *slider = [[CustomSlider alloc] initWithFrame:CGRectMake(0, 0, 300, 20)];
[slider thumbRectForBounds: slider.bounds trackRect:slider.frame value:15.f]; // change values as per your requirements.
Hope this helps.
Create a custom thumb image which has a large empty margin and set that on your slider, like this:
[theSlider setThumbImage:[UIImage imageNamed:#"slider_thumb_with_margins"] forState:UIControlStateNormal];
To make the image, get a copy of the system thumb image using any one of a number of UIKit artwork extractors (just search the web for one). Open the thumb image in Photoshop and increase the canvas size by twice the margin you want to add. Make sure you change the canvas size and not the image size, as the image size will stretch the image to fill the new size. This will put empty space around the thumb which will be part of the hit-test area but since it is all transparent it won't change the look of the slider.
Right now my UIPanGestureRecognizer recognizes every single pan, which is great and necessary, but as I'm using it as a sliding gesture to increase and decrease a variable's value, within the method I only want to act every so often. If I increment by even 1 every time it's detected the value goes up far too fast.
Is there a way to do something like, every 10 pixels of panning do this, or something similar?
You're looking for translationInView:, which tells you how far the pan has progressed and can be tested against your minimum distance. This solution doesn't cover the case where you go back and forth in one direction in an amount equal to the minimum distance, but if that's important for your scenario it's not too hard to add.
#define kMinimumPanDistance 100.0f
UIPanGestureRecognizer *recognizer;
CGPoint lastRecognizedInterval;
- (void)viewDidLoad {
[super viewDidLoad];
recognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(didRecognizePan:)];
[self.view addGestureRecognizer:recognizer];
}
- (void)didRecognizePan:(UIPanGestureRecognizer*)sender {
CGPoint thisInterval = [recognizer translationInView:self.view];
if (abs(lastRecognizedInterval.x - thisInterval.x) > kMinimumPanDistance ||
abs(lastRecognizedInterval.y - thisInterval.y) > kMinimumPanDistance) {
lastRecognizedInterval = thisInterval;
// you would add your method call here
}
}