Is a UIView animation dependant on order of properties being set? - cocoa-touch

I have encountered something strange with UIView animations. The animation scales a sub view
from a rect to fill its parent view:
//update views
CGRect startRect = ...; //A rect in parentView co-ordinates space that childView appears from
UIView *parentView = ...;
UIView *childView = ...;
[parentView addSubview:childView];
//animation start state
childView.alpha = 0;
childView.center = (CGPointMake( CGRectGetMidX(startRect), CGRectGetMidY(startRect)));
//TODO: set childViews transform and so that it is completely contained with in startRect
childView.transform = CGAffineTransformMakeScale(.25, .25);
[UIView animateWithDuration:.25 animations:^{
childView.transform = CGAffineTransformIdentity;
childView.alpha = 1;
childView.frame = parentView.bounds;
}];
The above code works as expected. However, if the animation block is reordered to the following then the animation goes haywire (scales massively and center point is off screen):
[UIView animateWithDuration:.25 animations:^{
childView.frame = parentView.bounds; //This line was after .alpha
childView.transform = CGAffineTransformIdentity;
childView.alpha = 1;
}];
What's going on here? Why is the order that the properties are set significant to the animation?

The order of the properties is probably relevant because the frame is undefined when the transform is not the identity transform.
Warning: If the transform property is not the identity transform, the value of this property is undefined and therefore should be ignored.
From the documentation for frame (on UIView).
This is true for getting the frame value but I believe that it also is true for setting the frame.
I'm almost certain that the changes in your animation block happens on the model of the views layer before they are animated on the presentation of the views layer.
Since you are coming form a state where the transform is a scale, setting the frame is undefined.
In your top example the transform is set to identity before setting the frame, thus it works as expected, but in the second example you set the frame before restoring the transform.

Related

How to get object position and size in UIView objective c

I put UIImageView in my Scene from Object library, and give it an image and defined OUTLET in .h file. Now I want to check its coordinates, or center point, or frame X,Y,Width,Height.
I am using
This
CGRect newFrameSize = CGRectMake(recycleBin.frame.origin.x, recycleBin.frame.origin.y,
recycleBin.frame.size.width, recycleBin.frame.size.height);
or
CGRect newFrameSize = recycleBin.frame;
by using this
NSLog(#"%#", NSStringFromCGRect(newFrameSize));
gives same result that is
2013-01-16 21:42:25.101 xyzapp[6474:c07] {{0, 0}, {0, 0}}
I want its actual position and size when viewcontroller loaded, so when user click on image view it will fadeout by zoom-in towards users and will disappear, and when user tap on reset button, it fadein and zoom-in back to original form (reverse to the previous animation).
Also give me hint, how to perform this animation on UIImageView or any button or label. Thx
Unfortunately, you can't check an item's actual frame as set in IB in -viewDidLoad. The earliest you can check it (that I've found) is by overriding -viewDidAppear:. But, since -viewDidAppear: could be called multiple times throughout the life of the view, you need to make sure you're not saving the frame it's in the modified state.
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
if(savedFrame == CGRectZero) {
savedFrame = self.recycleBin.frame;
NSLog(#"Frame: %#", NSStringFromCGRect(savedFrame));
}
}
Where savedFrame is a member variable (or you could make it a property).
From the description of the animation you're wanting, it sounds like adjusting the frame isn't the way to go about it. It sounds like you're wanting to get the effect of the view stretching and fading out (and the reverse when being reset)? If so, some code like this might be more so what you're looking for...
Fade out:
float animationDuration = 2.0f; // Duration of animation in seconds
float zoomScale = 3.0f; // How much to zoom in duration the animation
[UIView animateWithDuration:animationDuration animations:^{
CGAffineTransform transform = CGAffineTransformMakeScale(zoomScale, zoomScale);
self.recycleBin.transform = transform;
self.recycleBin.alpha = 0; // Make fully transparent
}];
And then, to reset the view:
float animationDuration = 2.0f; // Duration of animation in seconds
[UIView animateWithDuration:animationDuration animations:^{
CGAffineTransform transform = CGAffineTransformMakeScale(1.0f, 1.0f);
self.recycleBin.transform = transform;
self.recycleBin.alpha = 1.0; // Make fully opaque
}];
You can play around with the numbers to see if you get the effects you desire. Most animations in iOS are actually extremely simple to do. This code would work for any UIView subclass.
It sounds as if your IBOutlet is not attached to your class.
Open up your view controller header file (if that is where you property declaration is) and look beside the declaration:
Notice how on the first IBOutlet, the circle (to the left of the line number) is filled in. This means that it is connected to your scene. However, the second one is not (the circle is not filled in).

GMGridView appears offscreen, but overlaps with onscreen view

I am using a custom controller for transitions (could not use navigation controller due to inherent cycles in project, which would allow the navigation controllers stack to grow unbounded [which I assume would cause memory issues]). I am emulating a navigation controllers sliding animation (when transitioning to new screens) using UIView animateWithDuration:animations:completion:
When a button triggering the transition is pressed, the frame of the new view I want to transition to is set to an offscreen position. In the animation for the transition (UIView animateWithDuration:animations:completion:), the view currently on screen has its frame set to an offscreen position, and the new view is set to an onscreen position.
This is inside my custom controller for transitions:
CGFloat windowWidth = self.mainView.frame.size.width;
CGFloat windowHeight = self.mainView.frame.size.height;
CGRect offScreenLeft = CGRectMake(-1*windowWidth, 0.0, windowWidth, windowHeight);
CGRect onScreen = self.mainView.frame;
CGRect offScreenRight = CGRectMake(windowWidth, 0.0, windowWidth, windowHeight);
if (direction == TransitionDirectionForward)
{
if (dragBackgroundOnscreen){
[self.mainView addSubview:self.backgroundView];
[self.mainView sendSubviewToBack:self.backgroundView];
self.backgroundView.frame = offScreenRight;
}
self.currentViewController.view.frame = offScreenRight;
[UIView animateWithDuration:0.65
animations:^{
oldViewController.view.frame = offScreenLeft;
if (dragBackgroundOffscreen)
self.backgroundView.frame = offScreenLeft;
else if (dragBackgroundOnscreen)
self.backgroundView.frame = onScreen;
self.currentViewController.view.frame = onScreen;
}
completion:^(BOOL finished){
[oldViewController.view removeFromSuperview];
if (dragBackgroundOffscreen)
[self.backgroundView removeFromSuperview];
[oldViewController willMoveToParentViewController:nil];
[oldViewController removeFromParentViewController];
[self.currentViewController didMoveToParentViewController:self];
}];
}
else if (direction == TransitionDirectionBackward)
{
if (dragBackgroundOnscreen){
[self.mainView addSubview:self.backgroundView];
[self.mainView sendSubviewToBack:self.backgroundView];
self.backgroundView.frame = offScreenLeft;
}
self.currentViewController.view.frame = offScreenLeft;
[UIView animateWithDuration:0.65
animations:^{
oldViewController.view.frame = offScreenRight;
if (dragBackgroundOffscreen)
self.backgroundView.frame = offScreenRight;
else if (dragBackgroundOnscreen)
self.backgroundView.frame = onScreen;
self.currentViewController.view.frame = onScreen;
}
completion:^(BOOL finished){
[oldViewController.view removeFromSuperview];
if (dragBackgroundOffscreen)
[self.backgroundView removeFromSuperview];
[oldViewController willMoveToParentViewController:nil];
[oldViewController removeFromParentViewController];
[self.currentViewController didMoveToParentViewController:self];
}];
}
I also want the background (self.backgroundView) to remain static unless moving to a screen that has its own background (i.e. I dont want the background to slide if the new views background is the same background).
I am using TransitionDirectionBackward and TransitionDirectionForward just to differentiate between sliding left and sliding right.
Everything works great, except when transitioning involves a GMGridView. the problem when the Gridviews frame is set to an offscreen frame its really setting its currently selected page's frame to that offscreen frame. Other pages of the gridview are not bounded by this frame, so they can appear on screen even before the transition. I tried setting the frame and bounds property on the GridView's viewcontroller's view, but I can still get a page of the gridview appearing onscreen before the transition animation.
Anyone see a solution to this? I was trying to find a way to clip the view of the GridView during the transition so pages dont appear except for the currently selected page, but havent found anything useful.
UPDATE: I found a possible fix by setting alpha = 0.0 for cells that are visible but shouldnt be (later setting alpha = 1.0 when the transition animation is complete). However, I need to know which cells to do this for. I need a way to access the page that the GMGridView is currently on so I can set the adjacent page's cells to have an alpha of 0.0.
UPDATE: Found a way to get it to work by using myGridView convertPoint:(A cgpoint i found by trial and error to be on the first cell of a page.) fromView:myGridView.window. NOTE: I needed an if/else if to check if i was in lanscape left or landscape right since the window coordinates do not rotate when the device is rotated. with this i was able to get the index of the cell at the point on the screen i had specified and then set the previous page to be transparent until after the transition animation.
I would still like to know if there is a way of "clipping" the gridview so that if the cells could be opaque, but just never displayed....?
I think I over complicated the problem. I was looking for the clipsToBounds method of a UIView (although I could have sworn I tried that before). In any case, its working now!

How do I transfer the frame *and* transform from one UIView to another without distortion?

I have a UIView that may have scale and/or rotation transforms applied to it. My controller creates a new controller and passes the view to it. The new controller creates a new view and tries to place it in the same location and rotation as the passed view. It sets the location and size by converting the original view's frame:
CGRect frame = [self.view convertRect:fromView.frame fromView:fromView.superview];
ImageScrollView *isv = [[ImageScrollView alloc]initWithFrame:frame image:image];
This works great, with the scaled size and location copied perfectly. However, if there is a rotation transform applied to fromView, it does not transfer.
So I added this line:
isv.transform = fromView.transform;
That nicely handles transfers the rotation, but also the scale transform. The result is that the scale transform is effectively applied twice, so the resulting view is much too large.
So how do I go about transferring the location (origin), scale, and rotation from one view to another, without doubling the scale?
Edit
Here is a more complete code example, where the original UIImageView (fromView) is being used to size and position a UIScrollView (the ImageScrollView subclass):
CGRect frame = [self.view convertRect:fromView.frame fromView:fromView.superview];
frame.origin.y += pagingScrollView.frame.origin.y;
ImageScrollView *isv = [[ImageScrollView alloc]initWithFrame:frame image:image];
isv.layer.anchorPoint = fromView.layer.anchorPoint;
isv.transform = fromView.transform;
isv.bounds = fromView.bounds;
isv.center = [self.view convertPoint:fromView.center fromView:fromView.superview];
[self.view insertSubview:isv belowSubview:captionView];
And here is the entirety of the configuration in ImageScrollView:
- (id)initWithFrame:(CGRect)frame image:(UIImage *)image {
if (self = [self initWithFrame:frame]) {
CGRect rect = CGRectMake(0, 0, frame.size.width, frame.size.height);
imageLoaded = YES;
imageView = [[UIImageView alloc] initWithImage:image];
imageView.frame = rect;
imageView.contentMode = UIViewContentModeScaleAspectFill;
imageView.clipsToBounds = YES;
[self addSubview:imageView];
}
return self;
}
It looks as though the transform causes the imageView to scale up too large, as you can see in this ugly video.
Copy the first view's bounds, center, and transform to the second view.
Your code doesn't work because frame is a value that is derived from the bounds, center, and transform. The setter for frame tries to do the right thing by reversing the process, but it can't always work correctly when a non-identity transform is set.
The documentation is pretty clear on this point:
If the transform property is not the identity transform, the value of this property is undefined and therefore should be ignored.
...
If the transform property contains a non-identity transform, the value of the frame property is undefined and should not be modified. In that case, you can reposition the view using the center property and adjust the size using the bounds property instead.
Let say viewA is first view that contains frame & transform, and you want to pass those values to viewB.
So that, you need to get the original frame of viewA and pass it to viewB before pass the transform. Otherwise, viewB's frame will be changed 1 more time on when you add transform.
To get the original frame, just make viewA.transform to CGAffineTransformIdentity
Here is code
CGAffineTransform originalTransform = viewA.transform; // Remember old transform
viewA.transform = CGAffineTransformIdentity; // Remove transform so that you can get original frame
viewB.frame = viewA.frame; // Pass originalFrame into viewB
viewA.transform = originalTransform; // Restore transform into viewA
viewB.transform = originalTransform; // At this step, transform will change frame and make it the same with viewA
After that, viewA & viewB will have the same UI on superView.

CALayer - animating layer size blocks animation from running

I have a method in which a bunch of layers are positioned, and a single "activated" layer (basically a layer that the user has clicked on) is both positioned and resized at the same time. All the layers, including the activated layer are sublayers of a larger layer. Here is my method:
[CATransaction begin];
[CATransaction setValue:[NSNumber numberWithFloat:0.7]
forKey:kCATransactionAnimationDuration];
}];
for (CALayer *layer in self.inactiveLayers) {
... do some positioning ...
}
CGRect newFrame = activeLayer.frame;
newFrame.origin.x = 50.0;
newFrame.origin.y = 50.0;
newFrame.size.width = 100.0;
newFrame.size.height = 100.0;
activeLayer.frame = newFrame;
[CATransaction commit];
The problem I'm experiencing is really weird. Using the code above, none of the animations run (not even the animations for the inactive layers). But as soon as I comment out the lines that set the size and width of the new frame, the animations magically start working again.
Is there any reason this should be happening?
Figured out the problem, I had a layout manager set on the superlayer of the layers I was trying to animate, and the layout code (which reset the sizes of the layers) was being called when the layer bounds changed. Disabling the layout manager for the duration of the animation solved the issue.
It is because the frame property of a CAlayer is not animatable. Use the bounds property instead and the position animation will run without a hitch.

UIVIew animation - Scaling + Translating

I have a view which I want to be scaled and translated to a new location by animating it. I tried to achieve it with the following code:
[UIView beginAnimations:nil context:nil];
[UIView setAnimationDuration:kDurationForFullScreenAnimation];
[[self animatingView] setFrame:finalRect];
[UIView commitAnimations];
The effect of this code is, the view first changes its content's size to the finalRect and then translates it to the new location. i.e. The scaling part is never animated. The view is just transformed to the new size and then translated.
This issue is already discussed in several other threads but none of them draw a conclusion. A solution does exist though, to use a timer and set the frame each time in the timer callback, but it has a performance drawback.
What is the most appropriate solution to this problem, also, why in first case this problem occur?
Thanks
Setting the frame does neither a scale nor a translation. You are either using the wrong terminology or you are using the wrong tool for the job. Scale and translate are both done using Core Graphics Affine transforms when you're looking to affect the UIView (as opposed to the layer which use Core Animation transforms).
To scale a view use
// 2x
[rotationView setTransform:CGAffineTransformMakeScale(2.0, 2.0)];
To translate, use
// Move origin by 100 on both axis
[rotationView setTransform:CGAffineTransformMakeTranslation(100.0, 100.0)];
To animate these, wrap them in an animation block. If you want to transform the view with both of these, then you need to concatenate them.
If you are not wanting scale and translation (transforms) at all, then what you mean is you want to change the view's bounds and position. These are changed with calls to
[view setBounds:newRect];
[view setCenter:newCenter];
Where newRect and newCenter are a CGRect and CGPoint respectively that represent the new position on the screen. Again, these need wrapped in an animation block.
Here is an example with block animation:
CGPoint newCenter = CGPointMake(100.0,100.0);
[UIView animateWithDuration: 1
delay: 0
options: (UIViewAnimationOptionCurveLinear | UIViewAnimationOptionAllowUserInteraction)
animations:^{object.center = newCenter ; object.transform = CGAffineTransformScale(CGAffineTransformIdentity, 2.0, 2.0);}
completion:^(BOOL finished) { }
];
Try this solution :
CGAffineTransform s = CGAffineTransformMakeScale(0.5f,0.5f);
CGAffineTransform t = CGAffineTransformMakeTranslation(100, 0);
v2.transform = CGAffineTransformConcat(t,s); // not s,t
this operation is not commutative. The order is the opposite of the order when using convenience functions for applying one transform to another.
And you can use this operation to : for example (remove the Scale) :
v2.transform =
CGAffineTransformConcat(CGAffineTransformInvert(s), v2.transform);