In my UIView, I've created a UIImageView called targetView where I have set it to a series of animations:
[UIView animateWithDuration:1.0
animations:^{self.targetView.center = CGPointMake(220,20);}
completion:^(BOOL finished){
//another animation will begin;
}];
I also created another UIImageView called shootView which will move to the target direction upon a finger swipe. Its movement is also implemented as an animation. At the end of its animation, detect the intersect with the targetView:
[UIView animateWithDuration:1.0
animations:^{
self.shootView.center = CGPointMake(destX, destY);}
completion:^(BOOL finished){
if (CGRectIntersectsRect(self.targetView.frame, self.shootView.frame)) {
//do something
}];
Now there is a problem: the intersect command only works fine if the targetView has reached the end point of the current animation and the shootView happens to be there. While the targetView is on the move somewhere in the middle of an animation, no intersect can be detected even if visually it is very obvious that the two frames are intersecting.
This approach might be tricky since you can only get callbacks on the start and end of UIView animations. In your example the CGRectIntersectRect method is only being called after the animation has completed.
One solution may be to use an NSTimer to animate the position of the shootview, and with each change of position do the collision detection. i.e
[NSTimer timerWithTimeInterval: 0.03 target: self selector: #selector(timerTicked:) userInfo: nil repeats: YES];
-(void) timerTicked: (NSTimer*) timer {
// do move
// check for colisions
// optional invalidation of timer
}
Related
I have a Mac app that uses a NSAnimationContext animation grouping to animate one NSView offscreen and another NSView onscreen. Prior to beginning the animation grouping I position the offscreen NSView in the position that I want it to originate from when it animates onscreen.
Under Yosemite and earlier versions this worked perfectly but under El Capitan it is as if the NSView never gets positioned in the start position that I specify so it animates onscreen from the wrong direction.
//Position offscreen view at correct starting point.
offscreenView.frame = STARTING_OFFSCREEN_RECT;
//Create animation grouping
[NSAnimationContext beginGrouping];
[[NSAnimationContext currentContext] setDuration:animationDuration];
[[NSAnimationContext currentContext] setTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]];
[[NSAnimationContext currentContext] setCompletionHandler:^{
/*
Do cleanup stuff here
*/
}];
//Move the views
onscreenView.frame = ENDING_OFFSCREEN_RECT:
offscreenView.frame = ENDING_ONSCREEN_RECT;
//End Grouping
[NSAnimationContext endGrouping];
I've debugged this to the best of my ability and it appears to me that the setting of offscreenView's frame at the very beginning is not actually occurring.
Does anybody know what I'm doing wrong?
I had very similar problem - offscreenView sometimes starts from the wrong position.
The offscreenView.layer appears to be messed up.
I fixed it by adding the following to my clean-up code:
onscreenView.layer = nil;
so that the next time the offscreenView is animated it will start with clean layer.
Or maybe in your case reset the layer before starting the animation:
offscreenView.layer = nil;
//Position offscreen view at correct starting point.
offscreenView.frame = STARTING_OFFSCREEN_RECT;
//Create animation grouping
...
NOTE:
In my animation I add the offscreenView every time to the superView:
//Position offscreen view at correct starting point.
offscreenView.frame = STARTING_OFFSCREEN_RECT;
[superView addSubview:offscreenView];
//Create animation grouping
...
And in the clean-up code I remove the onscreenView as well:
[onscreenView removeFromSuperview];
onscreenView.layer = nil;
I have a simple method for animate view.
-(void)animateSelf
{
CABasicAnimation * animation;
animation = [CABasicAnimation animationWithKeyPath:#"position.y"];
// settings ...
[self.view.layer addAnimation:animation forKey:#"position.y"];
animation = [CABasicAnimation animationWithKeyPath:#"transform.rotation.z"];
// settings ...
[self.view.layer addAnimation:animation forKey:#"transform.rotation.z"];
[UIView animateWithDuration: 1.0 animations:^{
CGRect rect = self.view.frame;
rect.origin.y += 800;
self.view.frame = rect;
} completion:nil];
}
For iOS 7 it worked well. But for iOS 8 animation behaves unpredictably. Is there a way to combine these animations for iOS 8?
I tried to replace animateWithDuration: by another CABasicAnimation, but it did not help.
The view.frame logs are correct, but the animation of view.frame jumps out of obscure origin.
After removing CABasicAnimation for position.y (the first one) bug is gone.
You have three animations:
You animate the layer's position
You animate the layer's transform
You animate the view's frame which in turn animates the layer's position.
Animations 1 and 3 collide.
On iOS 7 the behavior is that animation 3 cancels animation 1.
On iOS 8 the behavior is that animations 1 and 3 run concurrently ("Additive Animations").
I suggest you just remove animation 1 and also check out the related WWDC 2014 video (I think it was Building Interruptible and Responsive Interactions).
What worked for me was disabling autolayouts on the view and THE SUBVIEWS of the view I was animating at some point before doing the animation. Disabling autolayouts from the storyboard was not enough in iOS8.
[viewToAnimate removeFromSuperview];
[viewToAnimate setTranslatesAutoresizingMaskIntoConstraints:YES];
//addSubview again at original index
[superView insertSubview:viewToAnimate atIndex:index];
This example might help you, I wish I had discovered it before wasting hours. Rather than animate the frame, animates its contraints. Click on the auto layout constraint you would like to adjust (in interface builder e.g top constraint). Next make this an IBOutlet;
#property (strong, nonatomic) IBOutlet NSLayoutConstraint *topConstraint;
Animate upwards;
self.topConstraint.constant = -100;
[self.viewToAnimate setNeedsUpdateConstraints];
[UIView animateWithDuration:1.5 animations:^{
[self.viewToAnimate layoutIfNeeded];
}];
Animate back to original place
self.topConstraint.constant = 0;
[self.viewToAnimate setNeedsUpdateConstraints];
[UIView animateWithDuration:1.5 animations:^{
[self.viewToAnimate layoutIfNeeded];
}];
Originally posted by me here
So you would adjust the constraint for self.view.frame in your example.
I also got an issue of little nasty differences between iOS7 and iOS8 animation.
In most cases it was broken it was either:
single combination of Scale, Transform and Rotate CGAffineTransforms - the result was dependant on iOS version
or complex sequence of animations on different views - some views were 'reseting' their positions before commencing a new piece of animations. About 5% of animation pieces were affected.
I'm pretty sure there were no simultaneous animations on the problematic views.
Autolayout and constraints suggestions did not help (moreover, all animated views were create in code as autolayout interfered with animation a lot even before iOS8).
What turned out to be a universal solution for both problems is to put the problematic views into a wrapper view and use it to split-off Rotation animation or to do the animation that causes 'reset' effect. Now it functions the same in 7.1.1 and 8.1.1.
I've implemented some code, which is working nicely, to scroll a UIScrollview automatically based on a timer being triggered.
Here it is:
....
CGPoint offset;
....
offset = scroller.contentOffset;
....
- (void) scrollWords: (NSTimer *) theTimer
{
offset.y = offset.y+300;
[UIScrollView beginAnimations:#"scrollAnimation" context:nil];
[UIScrollView setAnimationDuration:50.0f];
[scroller setContentOffset:offset];
[UIScrollView commitAnimations];
}
However, I've noticed that the while the scrolling is happening, the scroll rate varies; half way through it scrolls 2 or 3 lines of text per second, but at the start and finish it's much slower, perhaps only 0.5 lines per second. Is there any way of controlling the scroll rate?
Thanks in advance.
Paul.
You're looking for setAnimationCurve:. Specifically what you're describing is the effect of UIViewAnimationCurveEaseInOut. Try adding [UIScrollView setAnimationCurve:UIAnimationCurveLinear];
Also, you're using old style animation code. If you're targeting iOS 4 or above, check out this new style that's much more friendly (in my opinion):
- (void) scrollWords: (NSTimer *) theTimer
{
offset.y = offset.y+300;
[UIScrollView animateWithDuration:50.0f delay:0 options:UIViewAnimationOptionCurveLinear animations:^{
[scroller setContentOffset:offset];
}];
}
Using the delay parameter, you can probably even get rid of your NSTimer. With this code you can scroll the table view after 5 seconds.
- (void) scrollWordsLater
{
offset.y = offset.y+300;
[UIScrollView animateWithDuration:50.0f delay:5.0 options:UIViewAnimationOptionCurveLinear animations:^{
[scroller setContentOffset:offset];
}];
}
I'm trying to slide in a NSView using core animation. I think I need to use explicit animation rather than relying on something like [[view animator] setFrame:newFrame]. This is mainly because I need to set the animation delegate in order to take action after the animation is finished.
I have it working just fine using the animator, but as I said, I need to be notified when the animation finishes. My code currently looks like:
// Animate the controlView
NSRect viewRect = [controlView frame];
NSPoint startingPoint = viewRect.origin;
NSPoint endingPoint = startingPoint;
endingPoint.x += viewRect.size.width;
[[controlView layer] setPosition:NSPointToCGPoint(endingPoint)];
CABasicAnimation *controlPosAnim = [CABasicAnimation animationWithKeyPath:#"position"];
[controlPosAnim setFromValue:[NSValue valueWithPoint:startingPoint]];
[controlPosAnim setToValue:[NSValue valueWithPoint:endingPoint]];
[controlPosAnim setDelegate:self];
[[controlView layer] addAnimation:controlPosAnim forKey:#"controlViewPosition"];
This visually works (and I get notified at the end) but it looks like the actual controlView doesn't get moved. If I cause the window to refresh, the controlView disappears. I tried replacing
[[controlView layer] setPosition:NSPointToCGPoint(endingPoint)];
with
[controlView setFrame:newFrame];
and that does cause the view (and layer) to move, but it is corrupting something such that my app dies with a seg fault soon afterwards.
Most of the examples of explicit animation seem to only be moving a CALayer. There must be a way to moving the NSView and also being able to set a delegate. Any help would be appreciated.
Changes made to views take effect at the end of the current run loop. The same goes for any animations applied to layers.
If you animate a view's layer, the view itself is unaffected which is why the view appears to jump back to its original position when the animation completes.
With these two things in mind, you can get the effect you want by setting the view's frame to what you want it to be when the animation is done and then adding an explicit animation to the view's layer.
When the animation begins, it moves the view to the starting position, animates it to the end position and when the animation is done, the view has the frame you specified.
- (IBAction)animateTheView:(id)sender
{
// Calculate start and end points.
NSPoint startPoint = theView.frame.origin;
NSPoint endPoint = <Some other point>;
// We can set the frame here because the changes we make aren't actually
// visible until this pass through the run loop is done.
// Furthermore, this change to the view's frame won't be visible until
// after the animation below is finished.
NSRect frame = theView.frame;
frame.origin = endPoint;
theView.frame = frame;
// Add explicit animation from start point to end point.
// Again, the animation doesn't start immediately. It starts when this
// pass through the run loop is done.
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"position"];
[animation setFromValue:[NSValue valueWithPoint:startPoint]];
[animation setToValue:[NSValue valueWithPoint:endPoint]];
// Set any other properties you want, such as the delegate.
[theView.layer addAnimation:animation forKey:#"position"];
}
Of course, for this code to work you need to make sure both your view and its superview have layers. If the superview doesn't have a layer, you'll get corrupted graphics.
I think you need to call the setPosition at the end (after setting the animation).
Also, I don't think you should animate explicitely the layer of the view, but instead the view itself by using animator and setting the animations. You can use delegates too with animator :)
// create controlPosAnim
[controlView setAnimations:[NSDictionary dictionaryWithObjectsAndKeys:controlPosAnim, #"frameOrigin", nil]];
[[controlView animator] setFrame:newFrame];
I'm hoping someone can help. I have a fairly standard UIView animation where the only real animation is a CGAffineTransformMakeScale that is set on a loop. I want to have two buttons, one that increases the rate (or more accurately reduces the animation duration) of scale and one that decreases the rate (or more accurately increases the animation duration) of the scale.
Is this even possible? I apologise if this is obvious but I am a novice and looking for advice - even if it's directed reading. Let me know if I should supply any further info to help.
Many thanks in advance!
I don't know of a way to modify an in-progress animations.
Here's code that I think does what you want. It uses the increase/decrease buttons to change an ivar animationDuration, and manually loops the animation as two halves (a forward half and a reverse half). Each time the new animation (forwards or reverse) starts, it gets the value for animationDuration at that point in time, so for short animations it will appear to change pretty much instantly. It would not work well for long duration animations however, as it actually only changes the animation speed at the max/min points of the animation.
You could break the animation up even smaller if you wanted to update more frequently - e.g. split it into 4 instead of 2 (1 -> 1.5, 1.5 -> 2.0, 2.0 -> 1.5, 1.5 -> 1.0) or even more if you need it.
Header (MyViewController.h):
// Define some constants to be edited to suit our needs
#define kAnimationDurationMin 0.1
#define kAnimationDurationDefault 0.4
#define kAnimationDurationMax 2.0
#define kAnimationDurationStep 0.05
#interface MyViewController : UIViewController {
// The variable to store the target animation duration in
double animationDuration;
// Whether the next animation should be a reverse animation or not
BOOL reverse;
// The view to be animated, this should be connected in Interface Builder
IBOutlet UIView *ball;
}
//The method to actually do the animation
-(void)doAnimation;
// These 2 methods should be connected to the 'touch up inside' event of the relevant buttons in Interface Builder
- (IBAction)incButtonPressed:(id)sender;
- (IBAction)decButtonPressed:(id)sender;
#end
Implementation (MyViewController.m):
#import "MyViewController.h"
#implementation MyViewController
-(void)viewDidLoad {
// If animation duration has not yet been set (it will be zero) then set it to the default.
if (animationDuration < kAnimationDurationMin) animationDuration = kAnimationDurationDefault;
// Start the animation
[self doAnimation];
}
// This method does the animation
-(void)doAnimation {
[UIView beginAnimations:#"ball" context:NULL];
[UIView setAnimationDuration: animationDuration];
[UIView setAnimationCurve: UIViewAnimationCurveEaseInOut];
// Do not repeat
[UIView setAnimationRepeatCount: 0];
// Not autoreversing allows us to trigger the animationDuration change twice as frequently.
[UIView setAnimationRepeatAutoreverses:NO];
// When the animation is complete, start it again by calling [self doAnimation];
[UIView setAnimationDelegate:self];
[UIView setAnimationDidStopSelector:#selector(doAnimation)];
if (reverse) {
// Reset to default
ball.transform = CGAffineTransformMakeScale(1,1);
} else {
// Target of forward animation
ball.transform = CGAffineTransformMakeScale(2,2);
}
// Toggle reverse
reverse = !reverse;
[UIView commitAnimations];
}
- (IBAction)incButtonPressed:(id)sender {
animationDuration += kAnimationDurationStep;
if (animationDuration > kAnimationDurationMax) animationDuration = kAnimationDurationMax;
}
- (IBAction)decButtonPressed:(id)sender {
animationDuration -= kAnimationDurationStep;
if (animationDuration < kAnimationDurationMin) animationDuration = kAnimationDurationMin;
}
#end
You want setAnimationDuration of the UIView class:
[UIView beginAnimations:nil context:nil];
[UIView setAnimationDuration:0.4];
view.transform = CGAffineTransformMakeScale(0.5, 0.5);
[UIView commitAnimations];
Here's the relevant documentation:
https://developer.apple.com/library/ios/#documentation/uikit/reference/UIView_Class/UIView/UIView.html#//apple_ref/doc/uid/TP40006816-CH3-SW62