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;
Related
I set off to transform an NSImageView. My initially attempt was
self.imageView.wantsLayer = YES;
self.imageView.layer.transform = CATransform3DMakeRotation(1,1,1,1);
Unfortunately I noticed that the transform only happens sometimes (maybe once every 5 runs). Adding an NSLog between confirmed that on some runs self.imageView.layer is null. State of the whole project is shown on the image below.
It's an incredibly simple 200x200 NSImageView with an outlet to a generated NSViewController. Some experimentation showed settings wantsDisplay doesn't fix the problem, but putting the transform on an NSTimer makes it work every-time. I'd love an explanation why this happens (I presume it's due to some race condition).
I'm using Xcode 8 on the macOS 10.12 but I doubt this is the cause of the problem.
Update
Removing wantsLayer and madly enabling Core Animation Layers in Interface Builder did not fix the problem.
Neither did attempts to animate it (I wasn't sure what I was hoping for)
// Sometimes works.. doesn't animate
[NSAnimationContext runAnimationGroup:^(NSAnimationContext *context) {
context.duration = 1;
self.imageView.animator.layer.transform = CATransform3DMakeRotation(1,1,1,1);
} completionHandler:^{
NSLog(#"Done");
}];
or
// Animates but only sometimes
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"transform"];
animation.toValue = [NSValue valueWithCATransform3D:CATransform3DMakeRotation(1,1,1,1)];
animation.duration = 1;
[self.imageView.layer addAnimation:animation forKey:nil];
After experimenting with allowsImplicitAnimation I realised I might be trying to animate too early.
Moving the transform code into viewDidAppear made it work every time.
- (void)viewDidAppear {
[super viewDidAppear];
self.imageView.animator.layer.transform = CATransform3DMakeRotation(1,1,1,1);
}
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 am implementing a CCLayer subclass that houses 2 UIImageViews. The views and layer are all the same size: I initialized the UIImageViews with the same frame, and set the contentSize of the layer to be the frame as well. Everything is working fine, but it seems as though something is going haywire with the first point when drawing. When the image is just tapped there is no line jump, but when I attempt to draw a stroke, as soon as I move my finger, the start of the line jumps down randomly(so in this screenshot I am drawing from left to right except for bottom-left line). I am not sure where I am going wrong in my code:
//_drawImageView is the UIImageView I am drawing in.
#pragma mark - Touch Methods
- (BOOL)ccTouchBegan:(UITouch *)touch withEvent:(UIEvent *)event
{
penStroked = NO;
_prevPoint = [touch locationInView:[[CCDirector sharedDirector]view]];
CGRect rect = self.boundingBox;
if (CGRectContainsPoint(rect, _prevPoint)) {
return YES;
}
return NO;
}
- (void)ccTouchMoved:(UITouch *)touch withEvent:(UIEvent *)event
{
penStroked = YES;
_currPoint = [touch locationInView:_drawImageView];
UIGraphicsBeginImageContext(self.contentSize);
[_drawImageView.image drawInRect:CGRectMake(0, 0, _drawImageView.frame.size.width, _drawImageView.frame.size.height)];
CGContextMoveToPoint(UIGraphicsGetCurrentContext(), _prevPoint.x, _prevPoint.y);
CGContextAddLineToPoint(UIGraphicsGetCurrentContext(), _currPoint.x, _currPoint.y);
CGContextSetLineCap(UIGraphicsGetCurrentContext(), kCGLineCapRound);
CGContextSetLineWidth(UIGraphicsGetCurrentContext(), penSize);
CGContextSetRGBStrokeColor(UIGraphicsGetCurrentContext(), penColor.r, penColor.g, penColor.b, 1.0);
CGContextSetBlendMode(UIGraphicsGetCurrentContext(), kCGBlendModeNormal);
CGContextStrokePath(UIGraphicsGetCurrentContext());
_drawImageView.image = UIGraphicsGetImageFromCurrentImageContext();
[_drawImageView setAlpha: penOpacity];
UIGraphicsEndImageContext();
_prevPoint = _currPoint;
}
I was having a really hard time initially aligning the point at which the line would be draw n and the actual position of my finger, so that is why in the ccTouchBegan method the touch is taken from the CCDirector and in ccTouchMoved it is taken from the _drawImageView. This is the only way it seems to draw perfectly besides the initial wonky behavior.
The problem was definitely caused by the differing references for the touches. As I had said before though, it was the only way to have the line match the position my finger was in. I figured that the line jump was due to the line of code:
CGContextMoveToPoint(UIGraphicsGetCurrentContext(), _prevPoint.x, _prevPoint.y);
The problem was that on the first go around, or the first time that the pen/finger is stroked, the previous point was referenced from the CCDirectors view while the current point was from the imageView. I created an int variable, that was incremented every time the pen was stroked and when penStrokedInt == 1 (first stroke), I corrected the _prevPoint to the WorldSpace using the current point coordinates:
_prevPoint = [self convertToWorldSpace:_currPoint];
which basically got rid of the very first point in the line. It's not the best solution, but now it works! Make sure to reset the penStrokedInt back to 0 when the ccTouchEnded.
I have a NSView, subclassed, with custom background drawing, filling it with a gradient.
In IB, I've put a checkbox on it, somewhere in the middle.
This is the drawRect method.
-(void) drawRect:(NSRect)dirtyRect {
CGFloat sc = 0.9f;
CGFloat ec = 0.6f;
NSColor* startingColor = [NSColor colorWithDeviceRed:sc green:sc blue:sc alpha:1];
NSColor* endingColor = [NSColor colorWithDeviceRed:ec green:ec blue:ec alpha:1];
NSGradient *grad = [[NSGradient alloc] initWithStartingColor:startingColor endingColor:endingColor];
[grad drawInRect:dirtyRect angle:270];
}
What happens is, this same method gets called to draw the whole view area first and then for the part, where NSButton (checkbox) lies on top of it. OF course the checkbox background is drawn with a complete gradient and it is not right, since the portion is much smaller. The same happens with other controls I put on the said NSView.
What is the suggested approach on such thing?
One option is to make controls height the same as the views' but this will result in problems in the future.
The answer is, always draw the WHOLE area of the view, not just the dirtyRect
[grad drawInRect:[self bounds] angle:270];
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];