I thought I was used to completion handlers but for some reason I think I have implemented this wrong.
The animation breaks if I tap the 'button' in quick succession. By breaks I mean seems to increase the X position.
I am looking for the animation to not be affected by the touch until its complete
if(self.segmentedView.selectedSegmentIndex == 0 && !animating){
//NORMAL CODE IS HERE
POPSpringAnimation *layerPositionAnimation = [POPSpringAnimation animationWithPropertyNamed:kPOPLayerPositionX];
animating = YES;
layerPositionAnimation.toValue = #(self.triangle.layer.position.x - secondSegmentWidth);
layerPositionAnimation.springBounciness = 10;
layerPositionAnimation.springSpeed = 20;
[layerPositionAnimation setCompletionBlock:^(POPAnimation *animation, BOOL finished) {
animating = NO;
}];
[self.triangle.layer pop_addAnimation:layerPositionAnimation forKey:#"layerPositionAnimation"];
}
Related
How can I modify Grant Pauls's cool Shimmer control to respond to accelerometer input? I am a novice with iOS dev and have isolated parts of the code, but I'm not sure how to go about "hooking it up" so it responds to accelerometer input like the sketch below:
(I made the sketch before finding the Shimmer control, the "White Stripe" here would be the non-masked layer of the control)
EDIT: I'm not looking for completed code, just ideas on how to approach creating the code. CAAnimation and Core Motion are totally new to me. "Make x a delegate of y and look at line z of the posted code" for instance would qualify as an answer to me.
Here is the code that I believe would be involved in "hooking up" the accelerometer:
BOOL disableActions = [CATransaction disableActions];
if (!_shimmering) {
if (disableActions) {
// simply remove mask
[self _clearMask];
} else {
// end slide
CFTimeInterval slideEndTime = 0;
CAAnimation *slideAnimation = [_maskLayer animationForKey:kFBShimmerSlideAnimationKey];
if (slideAnimation != nil) {
// determing total time sliding
CFTimeInterval now = CACurrentMediaTime();
CFTimeInterval slideTotalDuration = now - slideAnimation.beginTime;
// determine time offset into current slide
CFTimeInterval slideTimeOffset = fmod(slideTotalDuration, slideAnimation.duration);
// transition to non-repeating slide
CAAnimation *finishAnimation = shimmer_slide_finish(slideAnimation);
// adjust begin time to now - offset
finishAnimation.beginTime = now - slideTimeOffset;
// note slide end time and begin
slideEndTime = finishAnimation.beginTime + slideAnimation.duration;
[_maskLayer addAnimation:finishAnimation forKey:kFBShimmerSlideAnimationKey];
}
// fade in text at slideEndTime
CABasicAnimation *fadeInAnimation = shimmer_end_fade_animation(self, _maskLayer.fadeLayer, 1.0, _shimmeringEndFadeDuration);
fadeInAnimation.beginTime = slideEndTime;
[_maskLayer.fadeLayer addAnimation:fadeInAnimation forKey:kFBFadeAnimationKey];
// expose end time for synchronization
_shimmeringFadeTime = slideEndTime;
}
} else {
// fade out text, optionally animated
CABasicAnimation *fadeOutAnimation = nil;
if (_shimmeringBeginFadeDuration > 0.0 && !disableActions) {
fadeOutAnimation = shimmer_begin_fade_animation(self, _maskLayer.fadeLayer, 0.0, _shimmeringBeginFadeDuration);
[_maskLayer.fadeLayer addAnimation:fadeOutAnimation forKey:kFBFadeAnimationKey];
} else {
BOOL innerDisableActions = [CATransaction disableActions];
[CATransaction setDisableActions:YES];
_maskLayer.fadeLayer.opacity = 0.0;
[_maskLayer.fadeLayer removeAllAnimations];
[CATransaction setDisableActions:innerDisableActions];
}
// begin slide animation
CAAnimation *slideAnimation = [_maskLayer animationForKey:kFBShimmerSlideAnimationKey];
// compute shimmer duration
CGFloat length = 0.0f;
if (_shimmeringDirection == FBShimmerDirectionDown ||
_shimmeringDirection == FBShimmerDirectionUp) {
length = CGRectGetHeight(_contentLayer.bounds);
} else {
length = CGRectGetWidth(_contentLayer.bounds);
}
CFTimeInterval animationDuration = (length / _shimmeringSpeed) + _shimmeringPauseDuration;
if (slideAnimation != nil) {
// ensure existing slide animation repeats
[_maskLayer addAnimation:shimmer_slide_repeat(slideAnimation, animationDuration, _shimmeringDirection) forKey:kFBShimmerSlideAnimationKey];
} else {
// add slide animation
slideAnimation = shimmer_slide_animation(self, animationDuration, _shimmeringDirection);
slideAnimation.fillMode = kCAFillModeForwards;
slideAnimation.removedOnCompletion = NO;
slideAnimation.beginTime = CACurrentMediaTime() + fadeOutAnimation.duration;
[_maskLayer addAnimation:slideAnimation forKey:kFBShimmerSlideAnimationKey];
}
}
}
When you -init your view, you will want to create a MotionManager like so:
self.motionManager = [[CMMotionManager alloc] init];
[self.motionManager startAccelerometerUpdates];
On -dealloc in that view you will also want to get rid of it:
[self.motionManager stopAccelerometerUpdates];
self.motionManager = nil;
Here is the part that will matter most for you, you will need to query the motion manager for updates as to device orientation. I have always done this in the context of a SpriteKit scene in the update method, but I am pretty sure you can add a motion manager delegate (or something) and accomplish the same thing. The important bits are as follows:
CMAccelerometerData* data = self.motionManager.accelerometerData;
CGFloat xacceleration = data.acceleration.x;
CGFloat yacceleration = data.acceleration.y;
Using that x/y acceleration you can map that to a point in your view and map the shimmer effect to that point. I hope that helps.
I'm using the Facebook POP framework to animate UILabels and UITextFields when the user entered a wrong input. I want to simulate the kind of spring animation when you enter the wrong password in on a mac.
I've writting this method for doing the shake animation:
- (void)shakeAnimation:(UITextField *)textField :(UILabel *)label
{
//This method takes a textField and a label as input, and animates them accordingly.
//When the animation is done, the button is available for touch again, and the animation is removed.
POPSpringAnimation *positionAnimation = [POPSpringAnimation animationWithPropertyNamed:kPOPLayerPositionX];
positionAnimation.springBounciness = 20; //just parameters, nothing fancy
positionAnimation.velocity = #4000;
[positionAnimation setCompletionBlock:^(POPAnimation *animation, BOOL finished) {
self.signUpButton.userInteractionEnabled = YES;
}];
[textField.layer pop_addAnimation:positionAnimation forKey:#"positionAnimation"];
[label.layer pop_addAnimation:positionAnimation forKey:#"positionAnimation"];
[positionAnimation removedOnCompletion];
}
However, When I'm calling this method like so: [self shakeAnimation:self.emailField :self.emailLabel];
the UILabels only move about 50px to the right and then stop moving, whereas the UITextFields don't do anything. It doesn't seem like an animation at all, just an adjustment of the frame.
Instead of adding the same POPSpringAnimation to both the textField layer and the label layer, make two POPSpringAnimations and add one to each. Like this :
POPSpringAnimation *anim1 = [POPSpringAnimation animationWithPropertyNamed:kPOPLayerPositionX];
anim1.springBounciness = 20; //just parameters, nothing fancy
anim1.velocity = #400;
[anim1 setCompletionBlock:^(POPAnimation *animation, BOOL finished) {
self.button.userInteractionEnabled = YES;
}];
POPSpringAnimation *anim2 = [POPSpringAnimation animationWithPropertyNamed:kPOPLayerPositionX];
anim2.springBounciness = 20; //just parameters, nothing fancy
anim2.velocity = #400;
[textField.layer pop_addAnimation:anim1 forKey:#"positionAnimation"];
[label.layer pop_addAnimation:anim2 forKey:#"positionAnimation"];
[anim1 removedOnCompletion];
[anim2 removedOnCompletion];
I'd like to implement a custom UIDynamicBehavior that makes a view "burst". To do that I need to fade it out and scale it to 2x its size.
I do this by setting the view's alpha and bounds in the action block. However, how do I know how often the action block is called? The docs say "on each tick", but how many?
I added a counter. With no other animations, the block is called 30 times. With a few gravity and dynamic behaviors, it is called 500 times.
I also don't see how the UIDynamicAnimator knows when its behaviors are "done" moving stuff around. Can anyone shed some light on this?
The code below works sometimes, but other times the behavior stops before the view is animated completely (i.e. it is still visible).
self.action = ^{
static NSInteger count = 0;
NSLog(#"animation tick: %d", count);
count++;
UIView *view = (UIView*)[weakSelf.items lastObject];
view.alpha = view.alpha - 0.1;
CGRect bounds = view.bounds;
bounds.size.width += 1;
bounds.size.height += 1;
view.bounds = bounds;
};
For detecting when the animation has finished you could try this:
__weak Entity *weakSelf = self;
self.behavior.action = ^{
if (weakSelf.center.x == weakSelf.lastPosition.x && weakSelf.center.y == weakSelf.lastPosition.y) {
NSLog(#"end of dynamic movement");
}
weakSelf.lastPosition = weakSelf.center;
};
Or check another value that you are changing.
I'm simulating some effects, like leafs falling, snow and rain.
My first call was to use CAEmitterLayer, that works very well, but i need to render the layer in context to save as an image, and that seens impossible or, at least, very complicated.
So i am working with some view animations. I need to continuously animate my imageViews so it can fall through screen and reappear on top of it, each one with diffenrent speed.
So i relocate each imageView on screen with animateWithDuration and when animation is done i call the method that do that recursively from completion block so the imageview can make it's way to the end of screen. Once an imageView reachs the end of screen i relocate it on top.
The problem is when animation is over my imageView stops a little bit until the completion block calls the method recursively, i want it to be continuously.
My code generates random position for each imageView, and my animation duration is very short so i can always update imageView's location in case the user tap the button to save the view as an image.
Anyway, here is my code:
- (void)effectOnObject:(UIImageView *)object{
NSInteger initialX;
NSInteger initialy;
CGFloat duration;
//the object have reached the end of screen
if (object.frame.origin.y >= self.frame.size.height) {
//generate random position to relocate the object on x axis
initialX = arc4random() % 321;
//generate random position to relocate the object on y axis (little bit above top of screen)
initialy = ((NSInteger)-object.frame.size.height) - arc4random() % 11;
//duration 0 so the object can be replaced without animation
duration = 0.0;
}
else{
initialX = object.frame.origin.x;
//this change the speed of object
initialy = object.frame.origin.y + 10
//setted duration to short time
duration = 0.01;
}
[UIView animateWithDuration:duration delay:0 options:UIViewAnimationOptionAllowAnimatedContent
animations:^{
[object setFrame:CGRectMake(initialX, initialy, object.frame.size.width, object.frame.size.height)];
}
completion:^(BOOL completed){
//recursively call
[self effectOnObject:object];
}];
}
- (void)startEffect{
//initiate effect on each imageView
for (UIImageView *object in self.subviews) {
[self effectOnObject:object];
}
}
How can i make this animation repeatedly and continuously, without the gap when the method is called recursively from completion block and the animation restarts?
You don't need to do the animation with 0 duration when the object reaches the end of the screen. Just test for this in your completion block and relocate the view before calling recursively. Also, a duration of 0.01 seems a bit extreme (100 frames per second). Perhaps a more reasonable value like 0.1 would help.
Something like this:
- (void)effectOnObject:(UIImageView *)object{
//setted duration to short time
CGFloat duration = 0.1;
[UIView animateWithDuration:duration delay:0 options:UIViewAnimationOptionAllowAnimatedContent
animations:^{
NSInteger initialX = object.frame.origin.x;
//this change the speed of object
NSInteger initialy = object.frame.origin.y + 10;
[object setFrame:CGRectMake(initialX, initialy, object.frame.size.width, object.frame.size.height)];
}
completion:^(BOOL completed){
NSInteger initialX = arc4random() % 321;
//generate random position to relocate the object on y axis (little bit above top of screen)
NSInteger initialy = ((NSInteger)-object.frame.size.height) - arc4random() % 11;
[object setFrame:CGRectMake(initialX, initialy, object.frame.size.width, object.frame.size.height)];
//recursively call
[self effectOnObject:object];
}];
}
From what I read briefly you should check out CAReplicatorLayer. It is perfect for repeating stuff! Only issue I can think of is that the transform and values have to be incremental...
I'm currently testing new iOS 7 views controller transition.
What i want is a custom modal presenting transition that present your next view cut into several strip from top off screen. Each strip should appear after an incremental delay to give the desired effect.
So my code looks like this :
- (void)presentModalWithContext:(id<UIViewControllerContextTransitioning>)context
{
UIView *inView = [context containerView];
UIView *fromView = [context viewControllerForKey:UITransitionContextFromViewControllerKey].view;
UIView *toView = [context viewControllerForKey:UITransitionContextToViewControllerKey].view;
NSTimeInterval stripTime = 1.0;
NSTimeInterval stripDelay = 1.0;
NSInteger stripCount = 10;
CGFloat stripHeight = toView.frame.size.height / stripCount;
for (NSInteger i = 0; i < stripCount; i++)
{
CGFloat offsetY = i*stripHeight;
CGRect snapRect = CGRectMake(0, offsetY, toView.frame.size.width, stripHeight);
UIView *view = [toView resizableSnapshotViewFromRect:snapRect afterScreenUpdates:YES withCapInsets:UIEdgeInsetsZero];
CGRect stripRect = CGRectMake(0, -(stripCount-i)*stripHeight, snapRect.size.width, snapRect.size.height);
view.frame = stripRect;
[inView insertSubview:view aboveSubview:fromView];
NSTimeInterval interval = stripDelay*(stripCount-i);
[UIView animateWithDuration:stripTime delay:interval options:0 animations:^{
CGPoint center = view.center;
center.y += stripCount*stripHeight;
view.center = center;
} completion:^(BOOL finished) {
NSLog(#"complete");
if (i == stripCount-1)
[context completeTransition:YES];
}];
}
}
I've already checked initial and final position of each strip and already is OK. My interval variable is also properly set at each loop.
But it seems that this is not delayed at all. All strips are moving together, giving the impression that the complete view is moving.
A quick look to basic log shows that all animations are performed at the same time :
2013-09-20 01:11:32.908 test_transition[7451:a0b] complete
2013-09-20 01:11:32.909 test_transition[7451:a0b] complete
2013-09-20 01:11:32.910 test_transition[7451:a0b] complete
2013-09-20 01:11:32.910 test_transition[7451:a0b] complete
2013-09-20 01:11:32.911 test_transition[7451:a0b] complete
2013-09-20 01:11:32.911 test_transition[7451:a0b] complete
2013-09-20 01:11:32.912 test_transition[7451:a0b] complete
2013-09-20 01:11:32.912 test_transition[7451:a0b] complete
2013-09-20 01:11:32.913 test_transition[7451:a0b] complete
2013-09-20 01:11:32.913 test_transition[7451:a0b] complete
Do someone is able to spot what's wrong here ?
EDIT :
It seems this is the following line that cancel the delay of any animations, even if those are not concerning the view being snapshotted :
UIView *view = [toView resizableSnapshotViewFromRect:snapRect afterScreenUpdates:YES withCapInsets:UIEdgeInsetsZero];
If i set the parameter afterScreenUpdates to NO, the view snapshot is null and i get the following error log :
Snapshotting a view that has not been rendered results in an empty snapshot. Ensure your view has been rendered at least once before snapshotting or snapshot after screen updates.
How do i render the view before snapshotting ? I tried [toView setNeedsDisplay] but with no success ...
Here's a solution.
Although this question is 2 years old, it's still a pertinent one as it still exists on iOS9. I realize it miiight not be as much help to the asker seeing it's been 2 years, but I only just came across this. So here's a solution.
When you want to transition between view controllers, you're probably gonna be using an Animator Object to run your custom UIView block animation code. This might be sophisticated with multiple animation blocks and some using a delay value. But if during your transition you want to capture or screenshot a portion of something (whether it's by UIView.drawViewHierarchyInRect, UIView.snapshotViewAfterScreenUpdates, or UIView.resizableSnapshotViewFromRect), using any of these methods will disengage the delays in your animation. For the last 2 methods, if you pass false for afterScreenUpdates, then it won't disengage the delays, but it also won't capture anything; it has to be true to capture something, but setting it to true will disengage your delay.
Using any of these methods will disengage the delay in your animation block, and generally mess things up. If you tell UIKit the transition is gonna be 3 secs and you have an animation block (UIView.animateWithDuration...) that has a 2 sec delay and 1 sec animation, if your delay gets disengaged then the animation runs instantly and the transition lasts just 1 sec, which throws UIKit out of sync and stuff gets generally messed up cuz it was expecting it to be 3 secs.
Here's a solution:
Say you're transition from view controller A to view controller B. In your Animator Object's code file (an object that inherits from NSObject and conforms to UIViewControllerAnimatedTransitioning protocol), you put your animation code in the animateTransition(transitionContext: UIViewControllerContextTransitioning) { ...} delegate method. If you're transitioning from VC A to VC B, say you want to screenshot something from VC B, and show it and do something with it during the transition. One way that works perfectly, is to use the drawViewHierarchyInRect method in View Controller B's viewDidLoad method, to capture and store the image (or create a UIImageView from that image and store the UIImageView) in an instance property of View Controller B. You need to use the drawViewHierarchyInRect method and not any of the other two because those require the content to be visible on screen (i.e. already rendered); the drawViewHiearchyInRect method captures offScreen content, even if it's not added to the view.
As soon as you commence a transition, the 'to view controller' gets initialized and it's viewDidLoad gets called. So in your transition animation code, you can grab the image you screenshotted (or the imageView) by referring to the view controller's property and do whatever you want with it in your transition. Your delays will not be disengaged.
Main Point: Don't screenshot stuff during the transition. Instead, put the screenshot code in the viewDidLoad of the view controller, and store its output in an instance variable and refer to that in your transition.
Hope this helps, I only just came across this problem today and just came across a solution.
After working on your code for a bit, and comparing it to mine, where the delay parameter was honored correctly, I still can't figure out why yours doesn't work. In any case, I found another way that does work. I break the animation into two parts. In the first part, I create the slices of the view using your code, add them to the inView, and also to a mutable array. In the second part, I call the animation block recursively, with no delay, until the last strip is displayed. One limitation to this approach, is that each strip animation has to complete before the next one begins (since the next one is called from the completion block), so you don't have independent control over the duration and delay. Anyway, here is what I did. In the presenting view controller, I just do this:
-(IBAction)presntBlue:(id)sender {
BlueViewController *blue = [self.storyboard instantiateViewControllerWithIdentifier:#"Blue"];
blue.modalTransitionStyle = UIModalPresentationCustom;
blue.transitioningDelegate = self;
[self presentViewController:blue animated:YES completion:nil];
}
-(id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source {
RDPresentationAnimator *animator = [RDPresentationAnimator new];
animator.isPresenting = YES;
return animator;
}
And in the RDPresentationAnimator class, I have this:
#interface RDPresentationAnimator () {
NSInteger stripCount;
CGFloat stripHeight;
NSMutableArray *stripArray;
}
#end
#implementation RDPresentationAnimator
#define ANIMATION_TIME .3
- (NSTimeInterval)transitionDuration:(id <UIViewControllerContextTransitioning>)transitionContext {
return ANIMATION_TIME;
}
- (void)animateTransition:(id<UIViewControllerContextTransitioning>)context {
UIView *inView = [context containerView];
UIView *toView = [context viewControllerForKey:UITransitionContextToViewControllerKey].view;
stripCount = 10;
stripHeight = toView.frame.size.height / stripCount;
stripArray = [NSMutableArray new];
for (NSInteger i = 0; i < stripCount; i++)
{
CGFloat offsetY = i*stripHeight;
CGRect snapRect = CGRectMake(0, offsetY, toView.frame.size.width, stripHeight);
UIView *view = [toView resizableSnapshotViewFromRect:snapRect afterScreenUpdates:YES withCapInsets:UIEdgeInsetsZero];
CGRect stripRect = CGRectMake(0, -(stripCount-i)*stripHeight, snapRect.size.width, snapRect.size.height);
view.frame = stripRect;
[inView addSubview:view];
[stripArray addObject:view];
}
[self animateStrip:stripCount - 1 withContext:context];
}
-(void)animateStrip:(NSInteger) index withContext:(id <UIViewControllerContextTransitioning>) context{
[UIView animateWithDuration:ANIMATION_TIME animations:^{
UIView *view = stripArray[index];
CGPoint center = view.center;
center.y += stripCount*stripHeight;
view.center = center;
} completion:^(BOOL finished) {
if (index >0) {
[self animateStrip:index - 1 withContext:context];
}else{
[context completeTransition:YES];
};
}];
}
I thought I'd add another answer that does give you the independent control over the stripTime and stripDelay. I never did find a way to make it work using the new UIViewControllerContextTransitioning methods. This way uses normal UIView animations, followed by a no animation presentViewController. This method should work correctly in either portrait or landscape orientation (notice that I use self.view.bounds to calculate stripHeight and snapRect, so that those values will be correct for either orientation).
#interface ViewController () {
NSInteger stripCount;
CGFloat stripHeight;
NSMutableArray *stripArray;
}
#end
#implementation ViewController
-(IBAction)presntBlue:(id)sender {
BlueViewController *blue = [self.storyboard instantiateViewControllerWithIdentifier:#"Blue"];
[self animateView:blue];
}
-(void)animateView:(UIViewController *) toVC; {
UIView *toView = toVC.view;
toView.frame = self.view.bounds;
[self.view addSubview:toView];
NSTimeInterval stripDelay = 0.2;
NSTimeInterval stripTime = 1.0;
stripCount = 10;
stripHeight = self.view.bounds.size.height / stripCount;
stripArray = [NSMutableArray new];
for (NSInteger i = 0; i < stripCount; i++) {
CGFloat offsetY = i*stripHeight;
CGRect snapRect = CGRectMake(0, offsetY, self.view.bounds.size.width, stripHeight);
UIView *view = [toView resizableSnapshotViewFromRect:snapRect afterScreenUpdates:YES withCapInsets:UIEdgeInsetsZero];
CGRect stripRect = CGRectMake(0, -(stripCount-i)*stripHeight, snapRect.size.width, snapRect.size.height);
view.frame = stripRect;
[self.view addSubview:view];
[stripArray addObject:view];
}
[toView removeFromSuperview];
for (NSInteger i = 0; i < stripCount; i++) {
NSTimeInterval interval = stripDelay*(stripCount-i);
UIView *view = stripArray[i];
[UIView animateWithDuration:stripTime delay:interval options:0 animations:^{
CGPoint center = view.center;
center.y += stripCount*stripHeight;
view.center = center;
} completion:^(BOOL finished) {
if (i == 0){
[self presentViewController:toVC animated:NO completion:nil];
}
}];
}
}
Added note:
In the animateView: method, I add the toView to self.view,, and then remove it after making the strips. I do this to make sure it works correctly in portrait and landscape -- if I omit those two statements, there's a slight glitch in the landscape animation when the animation finishes. If I have those two lines in, I occasionally get a glitch at the beginning where you can see the whole toView for a brief flash. I don't know why this only happens occasionally, and I haven't updated my phone yet, so I don't know if this happens on the device as well.