UIButton Press/Release Animation Oddly Inconsistent - ios

I'm trying to write a custom UIButton subclass that will "animate" during press and release.
When pressed, the button should "shrink" (toward its center) to 90% of its original size.
When released, the button should "expand" to 105%, shrink again to 95% and then return to its original size.
Here's the code I've got right now:
#pragma mark -
#pragma mark - Touch Handling
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
[super touchesBegan:touches withEvent:event];
[self animatePressedDown];
}
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
[super touchesCancelled:touches withEvent:event];
[self animateReleased];
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
[super touchesEnded:touches withEvent:event];
[self animateReleased];
}
- (void)animatePressedDown {
NSLog(#"button.frame before animatedPressedDown: %#", NSStringFromCGRect(self.frame));
[self addShadowLayer];
CATransform3D ninetyPercent = CATransform3DMakeScale(0.90f, 0.90f, 1.00f);
[UIView animateWithDuration:0.2f
animations:^{
self.layer.transform = ninetyPercent;
}
completion:^(BOOL finished) {
NSLog(#"button.frame after animatedPressedDown: %#", NSStringFromCGRect(self.frame));
}
];
}
- (void)animateReleased {
[self.shadowLayer removeFromSuperlayer];
CATransform3D oneHundredFivePercent = CATransform3DMakeScale(1.05f, 1.05f, 1.00f);
CATransform3D ninetyFivePercent = CATransform3DMakeScale(0.95f, 0.95f, 1.00f);
[UIView animateWithDuration:0.1f
animations:^{
self.layer.transform = oneHundredFivePercent;
}
completion:^(BOOL finished) {
NSLog(#"button.frame after animateReleased (Stage 1): %#", NSStringFromCGRect(self.frame));
[UIView animateWithDuration:0.1f
animations:^{
self.layer.transform = ninetyFivePercent;
}
completion:^(BOOL finished) {
NSLog(#"button.frame after animateReleased (Stage 2): %#", NSStringFromCGRect(self.frame));
[UIView animateWithDuration:0.1f
animations:^{
self.layer.transform = CATransform3DIdentity;
self.layer.frame = self.frame;
}
completion:^(BOOL finished) {
NSLog(#"button.frame after animateReleased (Stage 3): %#", NSStringFromCGRect(self.frame));
}
];
}
];
}
];
}
Anyway, the above code works perfectly... some of the time. At other times, the button animation works as expected, but after the "released" animation, the final frame of the button is "shifted" up and left from its original position. That's why I have those NSLog statements, to track exactly where the button's frame is during each stage of the animation. When the "shift" occurs, it happens somewhere between animatePressedDown and animateReleased. At least, the frame shown in animatePressedDown is ALWAYS what I expect it to be, but the first value for the frame in animateReleased is wrong frequently.
I can't see a pattern to the madness, though the same buttons in my app tend to behave correctly or incorrectly consistently from app run to app run.
I'm using auto layout for all of my buttons, so I can't figure out what the difference is to make one button behave correctly and another one change its position.

I really hate when I answer my own questions, but I figured it out.
(I'd been struggling with this issue for days, but it finally clicked after I asked the question. Isn't that how it always works?)
Apparently, the problem for me was the fact that (on the buttons that were "shifting") I was changing the button's title mid-animation.
Once I set up a blocks-based system to call the title change only after the button's "bounce"/"pop" animation was complete, the problems went away.
I still don't know all of why this works the way it does, as the titles I was setting in no way changed the overall size of the buttons, but setting the buttons up as described fixed my problem.
Here's the code from my custom UIButton subclass, with the block property added:
#interface MSSButton : UIButton {
}
#property (copy , nonatomic) void(^pressedCompletion)(void);
// Other, non-related stuff...
#end
#implementation MSSButton
#pragma mark -
#pragma mark - Touch Handling
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
[super touchesBegan:touches withEvent:event];
[self animatePressedDown];
}
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
[super touchesCancelled:touches withEvent:event];
[self animateReleased];
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
[super touchesEnded:touches withEvent:event];
[self animateReleased];
}
- (void)animatePressedDown {
[self addShadowLayer];
CATransform3D ninetyPercent = CATransform3DMakeScale(0.90f, 0.90f, 1.00f);
[UIView animateWithDuration:0.2f
animations:^{
self.layer.transform = ninetyPercent;
}
];
}
- (void)animateReleased {
[self.shadowLayer removeFromSuperlayer];
CATransform3D oneHundredFivePercent = CATransform3DMakeScale(1.05f, 1.05f, 1.00f);
CATransform3D ninetyFivePercent = CATransform3DMakeScale(0.95f, 0.95f, 1.00f);
[UIView animateWithDuration:0.1f
animations:^{
self.layer.transform = oneHundredFivePercent;
}
completion:^(BOOL finished) {
[UIView animateWithDuration:0.1f
animations:^{
self.layer.transform = ninetyFivePercent;
}
completion:^(BOOL finished) {
[UIView animateWithDuration:0.1f
animations:^{
self.layer.transform = CATransform3DIdentity;
}
completion:^(BOOL finished) {
if (self.pressedCompletion != nil) {
self.pressedCompletion();
self.pressedCompletion = nil;
}
}
];
}
];
}
];
}
#end
Since my IBAction fires before animateReleased (due to order of methods listed in my touch handling methods), I simply set the pressedCompletion block in my IBAction and the title is changed at the end of the animation sequence.

One likely problem is that you're trying to use the view's frame property after you've changed the transform property. The UIView class reference page specifically warns against that:
Warning: If the transform property is not the identity transform, the
value of this property is undefined and therefore should be ignored.
Specifically, you're changing the layer's transform, which probably impacts the view's transform, and then accessing self.frame. You might want to ensure that the view's transform is set to CGAffineTransformIdentity before accessing self.frame.

Related

MKAnnotationView drag state ending animation

I'm using the custom MKAnnotationView animations provided by Daniel here: Subclassing MKAnnotationView and overriding setDragState but I run into an issue.
After the pin drop animation, when I go to move the map the mkannotationview jumps back to its previous location before the final pin drop animation block is called.
It seems to me that dragState=MKAnnotationViewDragStateEnding is being called before the animation runs? How can I get around this issue and set the final point of the mkannotationview to be the point it's at when the animation ends?
#import "MapPin.h"
NSString *const DPAnnotationViewDidFinishDrag = #"DPAnnotationViewDidFinishDrag";
NSString *const DPAnnotationViewKey = #"DPAnnotationView";
// Estimate a finger size
// This is the amount of pixels I consider
// that the finger will block when the user
// is dragging the pin.
// We will use this to lift the pin even higher during dragging
#define kFingerSize 20.0
#interface MapPin()
#property (nonatomic) CGPoint fingerPoint;
#end
#implementation MapPin
#synthesize dragState, fingerPoint, mapView;
- (void)setDragState:(MKAnnotationViewDragState)newDragState animated:(BOOL)animated
{
if(mapView){
id<MKMapViewDelegate> mapDelegate = (id<MKMapViewDelegate>)mapView.delegate;
[mapDelegate mapView:mapView annotationView:self didChangeDragState:newDragState fromOldState:dragState];
}
// Calculate how much to life the pin, so that it's over the finger, no under.
CGFloat liftValue = -(fingerPoint.y - self.frame.size.height - kFingerSize);
if (newDragState == MKAnnotationViewDragStateStarting)
{
CGPoint endPoint = CGPointMake(self.center.x,self.center.y-liftValue);
[MapPin animateWithDuration:0.2
animations:^{
self.center = endPoint;
}
completion:^(BOOL finished){
dragState = MKAnnotationViewDragStateDragging;
}];
}
else if (newDragState == MKAnnotationViewDragStateEnding)
{
// lift the pin again, and drop it to current placement with faster animation.
__block CGPoint endPoint = CGPointMake(self.center.x,self.center.y-liftValue);
[MapPin animateWithDuration:0.2
animations:^{
self.center = endPoint;
}
completion:^(BOOL finished){
endPoint = CGPointMake(self.center.x,self.center.y+liftValue);
[MapPin animateWithDuration:0.1
animations:^{
self.center = endPoint;
}
completion:^(BOOL finished){
dragState = MKAnnotationViewDragStateNone;
if(!mapView)
[[NSNotificationCenter defaultCenter] postNotificationName:DPAnnotationViewDidFinishDrag object:nil userInfo:[NSDictionary dictionaryWithObject:self.annotation forKey:DPAnnotationViewKey]];
}];
}];
}
else if (newDragState == MKAnnotationViewDragStateCanceling)
{
// drop the pin and set the state to none
CGPoint endPoint = CGPointMake(self.center.x,self.center.y+liftValue);
[UIView animateWithDuration:0.2
animations:^{
self.center = endPoint;
}
completion:^(BOOL finished){
dragState = MKAnnotationViewDragStateNone;
}];
}
}
- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent*)event
{
// When the user touches the view, we need his point so we can calculate by how
// much we should life the annotation, this is so that we don't hide any part of
// the pin when the finger is down.
fingerPoint = point;
return [super hitTest:point withEvent:event];
}
#end
I had the same problem, especially under iOS 8. After many hours of testing I believe that iOS keeps track of where it thinks self.center of the annotation is during the time that the state is MKAnnotationViewDragStateDragging. You need to use extreme caution if you animate self.center when handling MKAnnotationViewDragStateEnding. Read that as "I couldn't get that to work, ever."
Instead, I kept Daniel's original code when handling states MKAnnotationViewDragStateStarting and MKAnnotationViewDragStateCanceling, I animated self.center. When handling MKAnnotationViewDragStateEnding I animated self.transform instead of self.center. This maintains the actual location of the annotation and just changes how it is rendered.
This works well for me running either iOS 7.1 and iOS 8.0. Also fixed a bug in hitTest, and added some code to reselect the annotation after dragging or canceling. I think that is the default behavior of MKPinAnnotationView.
- (void)setDragState:(MKAnnotationViewDragState)newDragState animated:(BOOL)animated
{
if(mapView){
id<MKMapViewDelegate> mapDelegate = (id<MKMapViewDelegate>)mapView.delegate;
[mapDelegate mapView:mapView annotationView:self didChangeDragState:newDragState fromOldState:dragState];
}
// Calculate how much to lift the pin, so that it's over the finger, not under.
CGFloat liftValue = -(fingerPoint.y - self.frame.size.height - kFingerSize);
if (newDragState == MKAnnotationViewDragStateStarting)
{
CGPoint endPoint = CGPointMake(self.center.x,self.center.y-liftValue);
[UIView animateWithDuration:0.2
animations:^{
self.center = endPoint;
}
completion:^(BOOL finished){
dragState = MKAnnotationViewDragStateDragging;
}];
}
else if (newDragState == MKAnnotationViewDragStateEnding)
{
CGAffineTransform theTransform = CGAffineTransformMakeTranslation(0, -liftValue);
[UIView animateWithDuration:0.2
animations:^{
self.transform = theTransform;
}
completion:^(BOOL finished){
CGAffineTransform theTransform2 = CGAffineTransformMakeTranslation(0, 0);
[UIView animateWithDuration:0.2
animations:^{
self.transform = theTransform2;
}
completion:^(BOOL finished){
dragState = MKAnnotationViewDragStateNone;
if(!mapView)
[[NSNotificationCenter defaultCenter] postNotificationName:DPAnnotationViewDidFinishDrag object:nil userInfo:[NSDictionary dictionaryWithObject:self.annotation forKey:DPAnnotationViewKey]];
// Added this to select the annotation after dragging.
// This is the behavior for MKPinAnnotationView
if (mapView)
[mapView selectAnnotation:self.annotation animated:YES];
}];
}];
}
else if (newDragState == MKAnnotationViewDragStateCanceling)
{
// drop the pin and set the state to none
CGPoint endPoint = CGPointMake(self.center.x,self.center.y+liftValue);
[UIView animateWithDuration:0.2
animations:^{
self.center = endPoint;
}
completion:^(BOOL finished){
dragState = MKAnnotationViewDragStateNone;
// Added this to select the annotation after canceling.
// This is the behavior for MKPinAnnotationView
if (mapView)
[mapView selectAnnotation:self.annotation animated:YES];
}];
}
}
- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent*)event
{
// When the user touches the view, we need his point so we can calculate by how
// much we should life the annotation, this is so that we don't hide any part of
// the pin when the finger is down.
// Fixed a bug here. If a touch happened while the annotation view was being dragged
// then it screwed up the animation when the annotation was dropped.
if (dragState == MKAnnotationViewDragStateNone)
{
fingerPoint = point;
}
return [super hitTest:point withEvent:event];
}

Setting UIView with animation to default state

I have an animating sine wave in one of my view controllers that appears every time an up swipe gesture is detected and disappears when a down swipe is detected. The sine wave is a UIView and the animation translates the sine wave across the screen. This works fine when I swipe up and down the first time. However, after the first swipe I get all kinds of issues. I noticed that if I reallocate and initialize the sine wave after every swipe down, I can get it to work properly, but I know thats not the correct fix. My assumption is that the frame of the sine wave has to be reset to what it was originally, but my attempt to do so didn't resolve the issue (my attempt is commented out in the code below). Any ideas?
View controller code:
- (void)viewDidLoad
{
[self setUpSine];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(appDidEnterForeground:) name:UIApplicationWillEnterForegroundNotification object:nil];
}
-(void) setUpSine
{
self.sineWave = [[OSDrawWave alloc] initWithFrame:CGRectMake(-1*self.view.bounds.size.width, self.view.bounds.size.height, 2*self.view.bounds.size.width, .3125*(2*self.view.bounds.size.width))];
[self.sineWave setBackgroundColor:[UIColor clearColor]];
[self.view insertSubview:self.sineWave belowSubview:self.panedView];
}
- (IBAction)handleUpSwipe:(UISwipeGestureRecognizer *)recognizer
{
[self.sineWave animateWave];
[self showSine];
[UIView animateWithDuration:.25 delay:0 options:UIViewAnimationOptionCurveLinear animations:^
{
self.sineWave.frame = CGRectOffset(self.sineWave.frame, 0, -246);
}
completion:^(BOOL finished)
{
NSLog(#"sine wave y position AFTER UP swipe %f", self.sineWave.frame.origin.y);
}];
}
- (IBAction)handleDownSwipe:(UISwipeGestureRecognizer *)recognizer
{
[self hideSine];
if(self.panedView.frame.origin.y <0)
{
[UIView animateWithDuration:.25 delay:0 options:UIViewAnimationOptionCurveLinear animations:^
{
self.sineWave.frame = CGRectOffset(self.sineWave.frame, 0, 246);
}
completion:^(BOOL finished)
{
}];
}
}
-(void) showSine
{
[self.sineWave setHidden:NO];
[self.sineWave fadeInAnimation];
}
-(void) hideSine
{
[self.sineWave fadeOutAnimation];
[self.sineWave setHidden:YES];
}
-(void) appDidEnterForeground:(NSNotification *)notification
{
[self.sineWave animateWave];
}
My sine wave is a subclass of UIView. This is the code within the sineWave class
- (void)animateWave {
[self.layer removeAllAnimations];
self.transform=CGAffineTransformIdentity;
[UIView animateWithDuration:3 delay:0.0 options: UIViewAnimationOptionRepeat | UIViewAnimationOptionCurveLinear | UIViewAnimationOptionAllowUserInteraction animations:^{
self.transform = CGAffineTransformMakeTranslation(self.frame.size.width/2,0);
} completion:^(BOOL finished) {
self.transform = CGAffineTransformMakeTranslation(-self.frame.size.width/2, 0);
}];
}
-(void) fadeInAnimation
{
[UIView animateWithDuration:0 animations:^{
self.alpha = 1.0;}];
}
-(void) fadeOutAnimation
{
[UIView animateWithDuration:3 animations:^{
self.alpha = 0.0;}];
}
- (void)drawRect:(CGRect)rect
{
self.yc = 30;//The height of a crest.
float w = 0;//starting x value.
float y = rect.size.height;
float width = rect.size.width;
int cycles = 6;//number of waves
self.x = width/cycles;
CGContextRef context = UIGraphicsGetCurrentContext();
CGMutablePathRef path = CGPathCreateMutable();
CGContextSetLineWidth(context, 2);
while (w <= width) {
CGPathMoveToPoint(path, NULL, w,y/2);
CGPathAddQuadCurveToPoint(path, NULL, w+self.x/4, y/2 - self.yc, w+self.x/2, y/2);
CGPathAddQuadCurveToPoint(path, NULL, w+3*self.x/4, y/2 + self.yc, w+self.x, y/2);
w+=self.x;
}
CGContextAddPath(context, path);
[[UIColor colorWithRed:107/255.0f green:212/255.0f blue:231/255.0f alpha:1.0f]setStroke];
CGContextDrawPath(context, kCGPathStroke);
self.alpha= 0.0;
}
I was able to get this working thanks to a clue from this answer
In the view controller you need to subscribe to UIApplicationWillEnterForegroundNotification to get a notification when you app is returning from the background. I changed the viewDidLoad in the view controller and added a method to handle the notification -
- (void)viewDidLoad
{
[super viewDidLoad];
[self setUpSine];
[self.sineWave animateWave];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(appDidEnterForeground:) name:UIApplicationWillEnterForegroundNotification object:nil];
}
-(void) appDidEnterForeground:(NSNotification *)notification
{
[self.sineWave animateWave];
}
I also removed the call to animateWave from the handleUpSwipe: method
- (IBAction)handleUpSwipe:(UISwipeGestureRecognizer *)recognizer
{
[self showSine];
[UIView animateWithDuration:.25 delay:0 options:UIViewAnimationOptionCurveLinear animations:^
{
self.sineWave.frame = CGRectOffset(self.sineWave.frame, 0, -246);
}
completion:^(BOOL finished)
{
NSLog(#"sine wave y position AFTER UP swipe %f", self.sineWave.frame.origin.y);
}];
}
You also need to make a change to animateWave to clear any previous animation before the animation is (re)started. You don't need anything in the completion block -
- (void)animateWave {
[self.layer removeAllAnimations];
self.transform=CGAffineTransformIdentity;
[UIView animateWithDuration:3 delay:0.0 options: UIViewAnimationOptionRepeat | UIViewAnimationOptionCurveLinear |UIViewAnimationOptionAllowUserInteraction animations:^{
self.transform = CGAffineTransformMakeTranslation(self.frame.size.width/2,0);
} completion:^(BOOL finished) {
}];
}

(iOS) Drag and drop, Snap to grid, Check proper area

I've read all the stuff about drag'n'drop and gestureRecognizer but haven't found the solution.
I got "gamefield" 10x10 and I want to implement drag'n'drop with snap to grid and check if element placed on "gamefield".
I got my 8 "TempButtons" added to UIView buttonsSuperView with action:#selector(imageMoved:withEvent:).
- (IBAction) imageMoved:(id) sender withEvent:(UIEvent *) event
{
UIControl *control = sender;
originalPosition = [self getButton:sender];
UITouch *touch = [[event allTouches] anyObject];
point = [touch locationInView:control];
float step = 31.0; // Grid step size.
CGPoint center = control.center;
center.x += step * floor((point.x / step));
center.y += step * floor((point.y / step));
control.center = center;
}
It works, I can move any button with grid step 31px. I can place it everywhere, but I have to put it on UIView fieldView. If it placed right there it's OK and I can replace it on another tile. But if I place it outside fieldView the button should return on its place. Something like this:
if ([self.fieldView pointInside:point withEvent:nil])
{
control.center = center;
} else {
[UIView animateWithDuration:0.4
delay:0.0
options:UIViewAnimationOptionCurveEaseInOut
animations:^ {
control.center = originalPosition;
// to
}
completion:^(BOOL finished) {}];
}
If this code placed on - (IBAction) imageMoved:(id) sender withEvent:(UIEvent *) event method it makes button get back to its "originalposition" all the time its moving. But I need to check if its placed properly only on 'touchesEnded'. But that's another method and I don't understand how to realize which button pressed and its original coords to get back if placed improperly.
Maybe there's a better way to use 'UIView' instead of 'UIButton' but I still don't understand how to do that.
Thanks in advance.
Kind Regards,
Nick
Since no one answered my question I've spent some time and found working solution.
- (IBAction) imageMoved:(id) sender withEvent:(UIEvent *) event
{
dragObject = sender;
originalPosition = [self getButton:sender];
UITouch *touch = [[event allTouches] anyObject];
point = [touch locationInView:dragObject];
float step = 31.0; // Grid step size.
CGPoint center = dragObject.center;
center.x += step * floor((point.x / step));
center.y += step * floor((point.y / step));
dragObject.center = center;
[sender addTarget:self action:#selector(touchesEnded:withEvent:) forControlEvents:UIControlEventTouchUpInside];
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
if (![fieldView pointInside:dragObject.center withEvent:nil])
{
[UIView animateWithDuration:0.4
delay:0.0
options:UIViewAnimationOptionCurveEaseInOut
animations:^ {
dragObject.center = CGPointMake(originalPosition.x + 15.5, originalPosition.y + 15.5);
}
completion:^(BOOL finished) {}];
}
}
Now it works like charm and I can move along. My buttons moving freely snapped to grid and when pleced outside game field they move back to their original position.
Regards,
Nick

How to properly implement a draggable "drawer" custom UIView

I have a custom UIView at the bottom on my view-controller that acts as a "drawer" control. About 90 pixels of the view is visible. When a user taps the view, it animates upward revealing other options, controls, etc. After another tap, the view animates back down to its original position. Effectively "closing the drawer".
I've also implemented code so that the view tracks with a dragging gesture:
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
if(self.frame.origin.y >= 80) {
UITouch *theTouch = [touches anyObject];
CGPoint location = [theTouch locationInView:self];
CGPoint previousLocation = [theTouch previousLocationInView:self];
CGFloat target = location.y - previousLocation.y;
self.frame = CGRectOffset(self.frame, 0, target);
}
}
All of this is working as expected. However, the view is not responding correctly to a quick "flick" gesture. In the touchesEnded:withEvent: method I'm animating the view from where the user lifted his/her finger to its destination. This works well if the user drags slowly then releases. But if they flick quickly, the view still has to process a 0.5 second duration which isn't smooth to the eye. Here's all of the custom view's code:
-(void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
if(self.frame.origin.y >= 80) {
UITouch *theTouch = [touches anyObject];
CGPoint location = [theTouch locationInView:self];
CGPoint previousLocation = [theTouch previousLocationInView:self];
CGFloat target = location.y - previousLocation.y;
self.frame = CGRectOffset(self.frame, 0, target);
}
}
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
if(self.frame.origin.y == kDefaultUpY) {
_comingFromUp = YES;
_comingFromDown = NO;
}
else if(self.frame.origin.y == kDefaultDownY) {
_comingFromUp = NO;
_comingFromDown = YES;
}
}
-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
//responding to tap
if(self.frame.origin.y == kDefaultUpY) {
[UIView animateWithDuration:0.5f delay:0.0f usingSpringWithDamping:0.75f initialSpringVelocity:1.0f options:UIViewAnimationOptionCurveEaseIn animations:^{
self.frame = CGRectMake(0, kDefaultDownY, self.frame.size.width, self.frame.size.height);
} completion:nil];
return;
}
else if(self.frame.origin.y == kDefaultDownY) {
[UIView animateWithDuration:0.5f delay:0.0f usingSpringWithDamping:0.75f initialSpringVelocity:1.0f options:UIViewAnimationOptionCurveEaseIn animations:^{
self.frame = CGRectMake(0, kDefaultUpY, self.frame.size.width, self.frame.size.height);
} completion:nil];
return;
}
//responding to drag
if(_comingFromDown) {
if(self.frame.origin.y < kDefaultDownY) {
//dragging up -- go up
[UIView animateWithDuration:0.5f delay:0.0f usingSpringWithDamping:0.75f initialSpringVelocity:1.0f options:UIViewAnimationOptionCurveEaseIn animations:^{
self.frame = CGRectMake(0, kDefaultUpY, self.frame.size.width, self.frame.size.height);
} completion:nil];
}
else if(self.frame.origin.y > kDefaultDownY) {
//dragging down -- stay down
[UIView animateWithDuration:0.5f delay:0.0f usingSpringWithDamping:0.75f initialSpringVelocity:1.0f options:UIViewAnimationOptionCurveEaseIn animations:^{
self.frame = CGRectMake(0, kDefaultDownY, self.frame.size.width, self.frame.size.height);
} completion:nil];
}
}
else if(_comingFromUp) {
if(self.frame.origin.y < kDefaultUpY) {
//dragging up -- stay up
[UIView animateWithDuration:0.5f delay:0.0f usingSpringWithDamping:0.75f initialSpringVelocity:1.0f options:UIViewAnimationOptionCurveEaseIn animations:^{
self.frame = CGRectMake(0, kDefaultUpY, self.frame.size.width, self.frame.size.height);
} completion:nil];
}
else if(self.frame.origin.y > kDefaultUpY) {
//dragging down -- go down
[UIView animateWithDuration:0.5f delay:0.0f usingSpringWithDamping:0.75f initialSpringVelocity:1.0f options:UIViewAnimationOptionCurveEaseIn animations:^{
self.frame = CGRectMake(0, kDefaultDownY, self.frame.size.width, self.frame.size.height);
} completion:nil];
}
}
}
How can I properly respond to the gestures associated with a "drawer" view? Is there a standard way of achieving this kind of functionality?
Use gesture recognizers rather than implementing touch handling code yourself. You can attach both a swipe gesture recognizer and a pan gesture recognize, and set up the pan gesture so the swipe has to fail before the pan is triggered. Make the animation for the swipe gesture be faster. Something like 0.1 or 0.2 seconds is probably right for a swipe gesture.
Something to watch out for, though: In iOS 7, a swipe gesture from the bottom of the screen triggers the system settings drawer to come up from the bottom of the screen. Our app Face Dancer has a controls drawer, and we recently discovered that under iOS 7 its hard to slid our controls drawer up without triggering the system settings drawer instead. We have a "thumb" region for dragging, and we are adding a tap gesture that will fully display it/hide it in one step.

How to create a delayed CAAnimation that can be cancelled and/or reverted while playing?

I have a layer that I want to show when the user does an action, and hide after he is done. The layer is then shown again if the user does the action again.
In order for the UI to not go berserk with animations, I want:
that the fadeout animation only starts after one second (and is cancelled if the user does the action before it has started)
the the fadein animation to show the layer starts from the current fadeout opacity if the user is doing action before the layer has disappeared
what is the best way to do this?
I tried this, but this won't work properly (lot of noise with the layer blinking ):
- (void)hideHintLayer:(bool)hide
{
if(hide)
{
CABasicAnimation *animation = [CABasicAnimation animation];
animation.beginTime = CACurrentMediaTime() + 1.0f;
animation.duration = 1.0f;
animation.fillMode = kCAFillModeForwards;
animation.removedOnCompletion = NO;
animation.keyPath = #"opacity";
animation.fromValue = #(1.0f);
animation.toValue = #(0.0f);
[layer addAnimation:animation forKey:nil];
}
else
{
layer.opacity = 1.0f;
}
}
If you want to stop an animation, you can just do
[layer removeAllAnimations];
If you want to know the current alpha during the animated hiding of the view (so that you can reverse the animation, starting from the right place, you can do:
CALayer *presentationLayer = layer.presentationLayer;
CGFloat startingAlpha = presentationLayer.opacity;
You can then set the alpha to go from startingAlpha to 1.0 to animate the unhide without flickering the screen.
You can do the actual animations using block based animation, or I guess you could use CABasicAnimation, though I'm not sure why you would.
So, for example, you could do something like the following (in my example, I have a "show" button). I'm using block animations, but I suspect it would work fine for CABasicAnimation, too:
- (IBAction)onPressShowButton:(id)sender
{
[self showAndScheduleHide];
}
- (void)showAndScheduleHide
{
[UIView animateWithDuration:1.0
animations:^{
self.containerView.alpha = 1.0;
}
completion:^(BOOL finished) {
[self scheduleHide];
}];
}
- (void)show
{
[UIView animateWithDuration:1.0
animations:^{
self.containerView.alpha = 1.0;
}
completion:nil];
}
- (void)scheduleHide
{
self.timer = [NSTimer scheduledTimerWithTimeInterval:5.0
target:self
selector:#selector(startToHide)
userInfo:nil
repeats:NO];
}
- (void)startToHide
{
self.timer = nil;
self.hiding = YES;
[UIView animateWithDuration:5.0
delay:0.0
options:UIViewAnimationOptionAllowUserInteraction
animations:^{
self.containerView.alpha = 0.0;
}
completion:^(BOOL finished) {
self.hiding = NO;
}];
}
You can then have some utility method for reversing it or rescheduling a hide in progress:
- (void)reverseAndPauseHide
{
// if we have a "hide" scheduled, then cancel that
if (self.timer)
{
[self.timer invalidate];
self.timer = nil;
}
// if we have a hide in progress, then reverse it
if (self.hiding)
{
[self.containerView.layer removeAllAnimations];
CALayer *layer = self.containerView.layer.presentationLayer;
CGFloat currentAlpha = layer.opacity;
self.containerView.alpha = currentAlpha;
[self show];
}
}
Then, the question is when you know to call this reverseAndPauseHide and when to scheduleHide again. So, for example, you could handle touches:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
[super touchesBegan:touches withEvent:event];
[self reverseAndPauseHide];
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
[super touchesEnded:touches withEvent:event];
[self scheduleHide];
}

Resources