Replacing a NSSplitView subview with Core Animation - objective-c

I'm trying to figure out how to animate switching (replacing) subviews in a vertically-configured, 2-view NSSplitView. I've got it semi-working using the following methods in my NSSplitView subclass:
To set up the animation:
- (void)awakeFromNib {
// set delegate
[self setWantsLayer:YES];
CATransition *transition = [CATransition animation];
[transition setType:kCATransitionPush];
[transition setSubtype:kCATransitionFromBottom];
[transition setDuration:1.0];
[self setAnimations:[NSDictionary dictionaryWithObject:transition
forKey:#"subviews"]];
}
And to perform it:
- (void)replaceRightView:(NSView *)newView animated:(BOOL)animate {
NSRect currentSize = [[[self subviews] objectAtIndex:1] frame];
[newView setFrame:currentSize];
if (animate) {
[[self animator] replaceSubview:[[self subviews] objectAtIndex:1]
with:newView];
} else {
[self replaceSubview:[[self subviews] objectAtIndex:1]
with:newView];
}
}
However, this code has the effect of pushing the entire NSSplitView off, rather than just the subview on the right side of the split.
Is there a way to animate just the subview transition? Perhaps I'm using the wrong animation key ("subviews")? Other animation methods would be fine too.
Thanks!

It's probably not the cleanest way, but I ended up using an NSView 'container' subclass with custom addSubview: and replaceSubview:withView: methods that modify the new subview's frame to match the container view's frame, which is then structured into the NSSplitView subview I wanted to animate. I then setup the CATransition on the container view, and everything worked as I wanted.

Related

Rotation Gesture on UIScrollView

I am working on Scroll View with gestures. I added a UIView in Scroll View whose size is equal to the ScrollView content size. I want to apply the pinch gesture and rotate gesture on the View which is subview of ScrollView. I have done the work of the pinch gesture by using zoom property and delegate of the ScrollView which give me same effect which I want. But Rotation Gesture is creating Problem. When I add rotation gesture on the view then zooming of the scroll view also get disturb.
So how can i apply the pinch gesture and Rotate gesture on the Scroll View's subview whose size must be equal to the content size of the ScrollView initially.
Can anybody give me the way to do this!
This is the code of .m file, when we rotate the view it become invisible
#import "ViewController.h"
#interface ViewController ()
{
UIView *backgroundView;
UIScrollView *scrollView;
CGFloat lastRotation;
}
#end
#implementation ViewController
-(void)loadView
{
[super loadView];
//Scroll View
scrollView = [[UIScrollView alloc] initWithFrame:self.view.frame];
scrollView.contentSize = self.view.frame.size;
scrollView.delegate = self;
scrollView.backgroundColor = [UIColor grayColor];
//Zooming factors of the Scroll View
scrollView.minimumZoomScale = 1.0;
scrollView.maximumZoomScale = 5.0f;
scrollView.zoomScale = 1.0;
[self.view addSubview:scrollView];
//Scroll View's subview
backgroundView = [[UIView alloc] initWithFrame:scrollView.frame];
[backgroundView setBackgroundColor:[UIColor orangeColor]];
[scrollView addSubview:backgroundView];
UIRotationGestureRecognizer *bgRotationGstr = [[UIRotationGestureRecognizer alloc] initWithTarget:self action:#selector(rotateBackgroundView:)];
bgRotationGstr.delegate = self;
bgRotationGstr.cancelsTouchesInView = NO;
[backgroundView addGestureRecognizer:bgRotationGstr];
//Child of background view
UIView *childView = [[UIView alloc] initWithFrame:CGRectMake(20, 50, 100, 100)];
childView.backgroundColor = [UIColor grayColor];
[backgroundView addSubview:childView];
}
//Rotation of the background view
-(void)rotateBackgroundView:(UIRotationGestureRecognizer*)gesture
{
CGFloat rotation = 0.0 - (lastRotation - [(UIRotationGestureRecognizer*)gesture rotation]);
CGAffineTransform currentTransform = backgroundView.transform;
CGAffineTransform newTransform = CGAffineTransformRotate(currentTransform,rotation);
[backgroundView setTransform:newTransform];
lastRotation = [(UIRotationGestureRecognizer*)gesture rotation];
if (gesture.state == UIGestureRecognizerStateBegan || gesture.state == UIGestureRecognizerStateChanged)
{
scrollView.scrollEnabled = NO;
}
else if (gesture.state == UIGestureRecognizerStateEnded)
{
lastRotation = 0.0;
scrollView.scrollEnabled = YES;
return;
}
}
#pragma mark<UIScrollViewDelegate>
- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView
{
return backgroundView;
}
#pragma mark<UIGetsureRecognizer>
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
return YES;
}
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
{
return YES;
}
#end
After many days of struggling I found a definitive solution in order to use UIScrollView integrated zoom behavior together with UIRotationGestureRecognizer working like a charm. You have to add a container dummy view as subview of the scroll view, and put the UIImageView as subview of the container view. Afterthat, return the container view in the viewForZoomingInScrollView method and add the UIRotationGestureRecognizer to the scrollview, applying CGAffineTransformRotate to the UIImageView. Finally, return true in the shouldRecognizeSimultaneouslyWithGestureRecognizer method. In this way the scrollView will capture both the two fingers rotation gesture and the pinch to zoom gesture: the zoom will be applied to the dummy view and rotation to the uiimageview, without conflicts between transformations.
In code: let's think to have a UIViewController presenting a UIScrollView. We want to use scrollview's zoom behaviour out of the box together with UIImageView rotation.
1) The controller (or any other object) containing UIScrollView must conforms to UIGestureRecognizerDelegate protocol.
In myViewController.h
#interface myViewController : UIViewController < UIGestureRecognizerDelegate> {
}
2) Create a UIScrollView, add a dummy view as subview and finally add a UIImageView as subview of the dummy view.
In myViewController.m
//Scrollview
myScrollView=[[UIScrollView alloc] initWithFrame:CGRectMake(0,0, view.frame.size.width, view.frame.size.height)];
myScrollView.delegate=self;
[view addSubview:myScrollView];
//Dummy View
UIView *dummyView=[[UIView alloc] initWithFrame:CGRectMake(0, 0, myScrollView.frame.size.width, myScrollView.frame.size.height)];
[self addSubview:dummyView];
//ImageView
imageView=[[UIImageView alloc] initWithFrame:CGRectMake(0, 0, dummyView.frame.size.width, dummyView.frame.size.height)];
imageView.contentMode=UIViewContentModeScaleAspectFit;
[dummyView addSubview:imageView];
//Add rotation gesture to the scrollView
rotationGestureRecognizer = [[UIRotationGestureRecognizer alloc] initWithTarget:self action:#selector(handleRotate:)];
[myScrollView addGestureRecognizer:_rotationGestureRecognizer];
//Set the controller as delegate of the recognizer
rotationGestureRecognizer.delegate=self;
[...]
#pragma UIScrollViewDelegate
- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView
{
//Set the dummy view (imageview's superview) as view for zooming
return imageView.superview;
}
[...]
#pragma Mark - UIGestureRecognizerDelegate
- (BOOL) gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
//Make it possibile to recognize simultaneously pinch and rotation gestures
return TRUE;
}
[...]
- (IBAction) handleRotate:(UIRotationGestureRecognizer*)recognizer {
//Apply the rotation to imageView
imageView.transform = CGAffineTransformRotate(imageView.transform, recognizer.rotation);
recognizer.rotation = 0;
}
For simplyfing purposes, I wrote everything in the same controller. You are free to subclass the UIScrollView. Remember that the tricks are:
1) returning the container dummy view as viewForZoomingInScrollView so zoom will affect the container view and rotation will affect the uiimageview.
2) set the viewcontroller containing the scrollview as delegate of the rotation gesture recognizer and return TRUE for shouldRecognizeSimultaneouslyWithGestureRecognizer.
Add this to your .m
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer{
return YES;
}
Also make sure you dont have exclusive touch enabled on any gesture recognizers.
Try to put you scalable/rotatable content in a subview of your content view. ( Maybe you must set the "clip content" property of your content view to true - not sure )
This way the scrollview is not concerned anymore by transformations, since its content view stays still.
If you have to display the clipped content ( if you rotate a square, for example, the corners go out the initial area), recompute your content view and update the scrollview.
Since you gave the code, I suggest to try:
//Scroll View's subview
backgroundViewHolder = [[UIView alloc] initWithFrame:scrollView.frame];
[backgroundViewHolder setBackgroundColor:[UIColor orangeColor]];
[scrollView backgroundViewHolder];
//Holder View's subview
backgroundView = [[UIView alloc] initWithFrame:backgroundViewHolder.bounds];
[backgroundViewHolder addSubview:backgroundView];
Everything else should remain the same. This is just an idea... Not sure it is the right answer.
Add this to your implement file, make it a UIGestureRecognizerDelegate.
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer{
return YES;
}
rotationGeustureRecognozier.delegate = self;// (the implement file)

NSWindow animate size change new subview with auto layout

I have a NSWindowController containing several NSViewController. The windowController displays the view of the first viewController as a subView of the main window.
On a button click the windowController adds the next viewControllers view as subView, adds some layout constraints and animates them so that the first view moves out and the next view moves in. After the animation the first view is removed from its superView.
[nextController setLeftLayoutConstraint:nextLeft];
// ^^^^^^^^^^^^^^^^^^^^^^^ custom category'd property
[containerView addSubview:nextView];
[containerView addConstraints:#[nextWidth, nextHeight, nextTop, nextLeft]];
[NSAnimationContext runAnimationGroup:^(NSAnimationContext *context) {
[context setTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut]];
[context setDuration:0.5];
[[nextLeft animator] setConstant:[self leftOffset]];
[[[[self currentViewController] view] animator] setAlphaValue:-0.25]; // negative alpha to shift the timing
[[[[self currentViewController] leftLayoutConstraint] animator] setConstant:-NSWidth(containerView.frame)];
// ^^^^^^^^^^^^^^^^^^^^ custom category'd property
} completionHandler:^{
[[[self currentViewController] view] setAlphaValue:1.0];
[[[self currentViewController] view] removeFromSuperview];
[[self currentViewController] removeFromParentViewController];
_currentViewController = nextController;
}];
Now the second view is much taller than the first so it changes the windows hight as well.
Unfortunately this window frame change is not animated and the window pops ugly in the right size.
I tried getting the next views hight first to then animate all constraints or something like this. Unfortunately the views are not in the correct size before the animation is done.
Is there any way to animate the window change as well?

UIButton not receiving touch after superview rotate

I am working with a UICollectionView, and I have defined a custom UICollectionViewCell by subclassing that class. The initializer of the class defines two views, just like this:
[[NSBundle mainBundle] loadNibNamed:#"ProtelViewCellFlipped" owner:self options:nil];
CATransform3D transform = CATransform3DMakeRotation(M_PI, 0, 1, 0);
[flippedView.layer setTransform:transform];
[flippedView setHidden:YES];
[self addSubview:flippedView];
[[NSBundle mainBundle] loadNibNamed:#"ProtelViewCell" owner:self options:nil];
[self addSubview:selfView];
[selfView.layer setZPosition:1];
where flippedView is a view defined by the "ProtelViewCellFlipped" xib file, and selfView is the view defined in the "ProtelViewCell" xib file. The view hierarchy is like this: the view hierarchy http://massimomarra.altervista.org/gerarchia.png
The thing is that the flipped view is rotated (as if it was the other side of the "selfView" view) by a CATransform3D, and then it is hidden. And that's all right, it works.
The selfView view has a UIButton on it. In the bottom right corner of the view. If I press that button, this method gets triggered:
- (void)infoButtonPressed:(UIButton *)sender
{
NSString *animationKey = (selfView.layer.zPosition > flippedView.layer.zPosition)? #"forward" : #"backwards";
CGFloat animationDuration = 0.3;
if (selfView.layer.zPosition > flippedView.layer.zPosition)
[self.layer removeAllAnimations];
CABasicAnimation *rotation = [CABasicAnimation animationWithKeyPath:#"transform.rotation.y"];
[rotation setValue:animationKey forKey:#"id"];
rotation.toValue = [NSNumber numberWithFloat:M_PI];
rotation.duration = animationDuration;
rotation.removedOnCompletion = NO;
rotation.fillMode = kCAFillModeForwards;
[self.layer addAnimation:rotation forKey:animationKey];
[self performSelector:#selector(flipLayers) withObject:nil afterDelay:animationDuration/2];
}
So that the self view is rotated with an animation. Also, during the animation gets called this method:
- (void)flipLayers
{
[selfView setHidden:!selfView.hidden];
[flippedView setHidden:!selfView.hidden];
[selfView.layer setZPosition:flippedView.layer.zPosition];
[flippedView.layer setZPosition:(1-selfView.layer.zPosition)];
}
so that the flippedView becomes visible (and selfView hides), and the zPosition of his layer becomes higher than selfView.
So now I have the flippedView visible, and it is displayed correctly. It has also a button, but to trigger his action I have to tap in another position: flipped view scheme: i have to tap where the button "was" before the rotation http://massimomarra.altervista.org/s03KRJAUk7C_nP3yTXb7TBQ.png
It seems like the rotation just modifies the graphics of the view, but not the content.
Can you please help me?
Thanks to you all in advance!!
transform the button back to identity before change orientation
something like this:
- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
{
oneButton.transform=CGAffineTransformIdentity;
}
I don't know why but it seems working

CABasicAnimation move frame left by 300px

I need help with CABasicAnimation. I am trying to move a NSView left by 300 pixels. I found this SO thread: How to animate the frame of an layer with CABasicAnimation?
Turns out animating the frame is not possible and one of the answer points to a link to QA on Apple's website but it takes me a to a generic page:
http://developer.apple.com/library/mac/#qa/qa1620/_index.html
So, how can I do something as simple as translation of my NSView/CALyer?
Thanks!
NSView has a protocol called NSAnimatablePropertyContainer which allows you to create basic animations for views:
The NSAnimatablePropertyContainer protocol defines a way to add
animation to an existing class with a minimum of API impact ...
Sending of key-value-coding compliant "set" messages to the proxy will
trigger animation for automatically animated properties of its target
object.
The NSAnimatablePropertyContainer protocol can be found here
I recently used this technique to change the origin of a frame:
-(void)setOrigin:(NSPoint)aPoint {
[[self animator] setFrameOrigin:aPoint];
}
Instead of calling the [view setFrameOrigin:], I created another method called setOrigin: which then applies the setFrameOrigin: call to the view's animator.
If you need to change the duration of the animation, you can do so like this (similar to CATransactions):
-(void)setOrigin:(NSPoint)aPoint {
[NSAnimationContext beginGrouping];
[[NSAnimationContext currentContext] setCompletionHandler:^{
...Completion Callback Code goes here...
}];
[[NSAnimationContext currentContext] setDuration:1.0];
[[self animator] setFrameOrigin:aPoint];
[NSAnimationContext endGrouping];
}
The NSAnimationContext is described here
You can animate center property instead. Eg:
//assuming view is your NSView
CGPoint newCenter = CGPointMake(view.center.x - 300, view.center.y);
CABasicAnimation *animation = [CABasicAnimation animation];
//setup your animation eg. duration/other options
animation.fromValue = [NSValue valueWithCGPoint:v.center];
animation.toValue = [NSValue valueWithCGPoint:newCenter];
[view.layer addAnimation:animation forKey:#"key"];

Animating sibling NSViews

Background & Goal
I have a window and three sepearate views in a nib file in IB. The three views are used for the three 'parts' of the program: the first were the user drops a file, the second where it chooses some options and the third where it is presented with a progress bar as the program accomplishes a task.
I want to animate between those views as they change, and I'm doing a fade-in/out animation. The goal is to have the old view (the one that is going to disappear) fade out as the new view fades in. The views have different sizes, so I animate the window frame as well at the same time (those views fill up the window). All three views have Core Animation enabled in IB, but nothing else (the window's content view for example) has.
Code
At the start of the program, I register the views' sizes so I can use them later, add the views to the window and set the 2nd and 3rd view hidden.
- (void)awakeFromNib
{
// Register native views' size (when they get displayed again the window can be set to their bounds)
view1NativeSize = [firstView bounds].size;
view2NativeSize = [secondView bounds].size;
view3NativeSize = [thirdView bounds].size;
// Add views to the main window's content view
NSRect viewFrame = [[mainWindow contentView] bounds];
[firstView setFrame: viewFrame];
[secondView setFrame: viewFrame];
[thirdView setFrame: viewFrame];
[[mainWindow contentView] addSubview: firstView];
[[mainWindow contentView] addSubview: secondView];
[[mainWindow contentView] addSubview: thirdView];
// Set their attributes
[secondView setHidden: TRUE];
[thirdView setHidden: TRUE];
currView = 1;
}
Then I have three methods for swapping between views, where I calculate the new window frame and call a method for the animation.
- (IBAction)goToFirstView: (id)sender
{
NSRect newFrame = [mainWindow frame];
newFrame.size = view1NativeSize;
newFrame.size.height += [self titleBarHeight]; // Method that returns the title bar height
if (currView == 1) {
return;
}
else if (currView == 2) {
[self animateFromView: secondView toView: firstView andWindowFrame: newFrame];
currView--;
}
else if (currView == 3) {
[self animateFromView: thirdView toView: firstView andWindowFrame: newFrame];
currView -= 2;
}
}
- (IBAction)goToSecondView: (id)sender
{
NSRect newFrame = [mainWindow frame];
newFrame.size = view2NativeSize;
newFrame.size.height += [self titleBarHeight];
if (currView == 2) {
return;
}
else if (currView == 1) {
[self animateFromView: firstView toView: secondView andWindowFrame: newFrame];
currView++;
}
else if (currView == 3) {
[self animateFromView: thirdView toView: secondView andWindowFrame: newFrame];
currView--;
}
}
- (IBAction)goToThirdView: (id)sender
{
NSRect newFrame = [mainWindow frame];
newFrame.size = view3NativeSize;
newFrame.size.height += [self titleBarHeight];
if (currView == 3) {
return;
}
else if (currView == 1) {
[self animateFromView: firstView toView: thirdView andWindowFrame: newFrame];
currView += 2;
}
else if (currView == 2) {
[self animateFromView: secondView toView: thirdView andWindowFrame: newFrame];
currView++;
}
}
The method for the animation is pretty straightforward. I animate the views.
- (void)animateFromView: (NSView*)oldView
toView: (NSView*)newView
andWindowFrame: (NSRect)newWindowFrame
{
[NSAnimationContext beginGrouping];
[[NSAnimationContext currentContext] setDuration: 0.5];
[[newView animator] setHidden: FALSE];
[[oldView animator] setHidden: TRUE];
//[[mainWindow animator] setFrame: newWindowFrame]; --Doesn't work??
[NSAnimationContext endGrouping];
[mainWindow setFrame: newWindowFrame display: YES animate: YES];
}
Questions
I have two problems with this (most important first):
On the third view there is a progress indicator and it never displays correctly. It doesn't update! I call -startAnimation: but it doesn't move at all. If i turn off CA on that third layer it won't fade in/out correctly but the progress bar starts to work (animate). No clue why...
In my numerous experiments trying to solve the first problem I tried turning on CA for the window's content view and turning off CA for the three subviews. The first problem wasn't solved, but I noticed something: that way the old view was fading out AS the new view faded in, while before that change (subviews with CA) I could only notice the new view fading in. I recorded it and watched it in sloooowww-moootion :) and found I was right: the old view magically disappears and all I see is the new view fading in. It's funny I only found that after watching the animation I really wanted. Am I supposed to turn CA on only for the content view or for all three subviews separately? (or is this problem from something else?)
I tried many things like turning on CA for the progress bar. I found also many questions here that are close to this one but don't specifically address the progress bar problem.
Good luck finding a solution :)
To handle your crossfade you could just use a CATransition as explained in this answer but for a crossfade you wouldn't need to set the transition type as it is the default.
I'm afraid I don't have an answer for the progress bar problem though, it may be something to do with the unorthodox way that you're doing the crossfade.