How do I make an expand/contract transition between views on iOS? - ios

I'm trying to make a transition animation in iOS where a view or view controller appears to expand to fill the whole screen, then contract back to its former position when done. I'm not sure what this type of transition is officially called, but you can see an example in the YouTube app for iPad. When you tap one of the search result thumbnails on the grid, it expands from the thumbnail, then contracts back into the thumbnail when you return to the search.
I'm interested in two aspects of this:
How would you make this effect when transitioning between one view and another? In other words, if view A takes up some area of the screen, how would you transition it to view B which takes up the whole screen, and vice versa?
How would you transition to a modal view this way? In other words, if UIViewController C is currently showing and contains view D which takes up part of the screen, how do you make it look like view D is turning into UIViewController E which is presented modally on top of C?
Edit: I'm adding a bounty to see if that gets this question more love.
Edit: I've got some source code that does this, and Anomie's idea works like a charm, with a few refinements. I had first tried animating the modal controller's view (E), but it didn't produce the effect of feeling like you're zooming into the screen, because it wasn't expanding all the stuff around the thumbnail view in (C). So then I tried animating the original controller's view (C), but the redrawing of it made for a jerky animation, and things like background textures did not zoom properly. So what I wound up doing is taking an image of the the original view controller (C) and zooming that inside the modal view (E). This method is substantially more complex than my original one, but it does look nice! I think it's how iOS must do its internal transitions as well. Anyway, here's the code, which I've written as a category on UIViewController.
UIViewController+Transitions.h:
#import <Foundation/Foundation.h>
#interface UIViewController (Transitions)
// make a transition that looks like a modal view
// is expanding from a subview
- (void)expandView:(UIView *)sourceView
toModalViewController:(UIViewController *)modalViewController;
// make a transition that looks like the current modal view
// is shrinking into a subview
- (void)dismissModalViewControllerToView:(UIView *)view;
#end
UIViewController+Transitions.m:
#import "UIViewController+Transitions.h"
#implementation UIViewController (Transitions)
// capture a screen-sized image of the receiver
- (UIImageView *)imageViewFromScreen {
// make a bitmap copy of the screen
UIGraphicsBeginImageContextWithOptions(
[UIScreen mainScreen].bounds.size, YES,
[UIScreen mainScreen].scale);
// get the root layer
CALayer *layer = self.view.layer;
while(layer.superlayer) {
layer = layer.superlayer;
}
// render it into the bitmap
[layer renderInContext:UIGraphicsGetCurrentContext()];
// get the image
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
// close the context
UIGraphicsEndImageContext();
// make a view for the image
UIImageView *imageView =
[[[UIImageView alloc] initWithImage:image]
autorelease];
return(imageView);
}
// make a transform that causes the given subview to fill the screen
// (when applied to an image of the screen)
- (CATransform3D)transformToFillScreenWithSubview:(UIView *)sourceView {
// get the root view
UIView *rootView = sourceView;
while (rootView.superview) rootView = rootView.superview;
// convert the source view's center and size into the coordinate
// system of the root view
CGRect sourceRect = [sourceView convertRect:sourceView.bounds toView:rootView];
CGPoint sourceCenter = CGPointMake(
CGRectGetMidX(sourceRect), CGRectGetMidY(sourceRect));
CGSize sourceSize = sourceRect.size;
// get the size and position we're expanding it to
CGRect screenBounds = [UIScreen mainScreen].bounds;
CGPoint targetCenter = CGPointMake(
CGRectGetMidX(screenBounds),
CGRectGetMidY(screenBounds));
CGSize targetSize = screenBounds.size;
// scale so that the view fills the screen
CATransform3D t = CATransform3DIdentity;
CGFloat sourceAspect = sourceSize.width / sourceSize.height;
CGFloat targetAspect = targetSize.width / targetSize.height;
CGFloat scale = 1.0;
if (sourceAspect > targetAspect)
scale = targetSize.width / sourceSize.width;
else
scale = targetSize.height / sourceSize.height;
t = CATransform3DScale(t, scale, scale, 1.0);
// compensate for the status bar in the screen image
CGFloat statusBarAdjustment =
(([UIApplication sharedApplication].statusBarFrame.size.height / 2.0)
/ scale);
// transform to center the view
t = CATransform3DTranslate(t,
(targetCenter.x - sourceCenter.x),
(targetCenter.y - sourceCenter.y) + statusBarAdjustment,
0.0);
return(t);
}
- (void)expandView:(UIView *)sourceView
toModalViewController:(UIViewController *)modalViewController {
// get an image of the screen
UIImageView *imageView = [self imageViewFromScreen];
// insert it into the modal view's hierarchy
[self presentModalViewController:modalViewController animated:NO];
UIView *rootView = modalViewController.view;
while (rootView.superview) rootView = rootView.superview;
[rootView addSubview:imageView];
// make a transform that makes the source view fill the screen
CATransform3D t = [self transformToFillScreenWithSubview:sourceView];
// animate the transform
[UIView animateWithDuration:0.4
animations:^(void) {
imageView.layer.transform = t;
} completion:^(BOOL finished) {
[imageView removeFromSuperview];
}];
}
- (void)dismissModalViewControllerToView:(UIView *)view {
// take a snapshot of the current screen
UIImageView *imageView = [self imageViewFromScreen];
// insert it into the root view
UIView *rootView = self.view;
while (rootView.superview) rootView = rootView.superview;
[rootView addSubview:imageView];
// make the subview initially fill the screen
imageView.layer.transform = [self transformToFillScreenWithSubview:view];
// remove the modal view
[self dismissModalViewControllerAnimated:NO];
// animate the screen shrinking back to normal
[UIView animateWithDuration:0.4
animations:^(void) {
imageView.layer.transform = CATransform3DIdentity;
}
completion:^(BOOL finished) {
[imageView removeFromSuperview];
}];
}
#end
You might use it something like this in a UIViewController subclass:
#import "UIViewController+Transitions.h"
...
- (void)userDidTapThumbnail {
DetailViewController *detail =
[[DetailViewController alloc]
initWithNibName:nil bundle:nil];
[self expandView:thumbnailView toModalViewController:detail];
[detail release];
}
- (void)dismissModalViewControllerAnimated:(BOOL)animated {
if (([self.modalViewController isKindOfClass:[DetailViewController class]]) &&
(animated)) {
[self dismissModalViewControllerToView:thumbnailView];
}
else {
[super dismissModalViewControllerAnimated:animated];
}
}
Edit: Well, it turns out that doesn't really handle interface orientations other than portrait. So I had to switch to animating the transition in a UIWindow using a view controller to pass along the rotation. See the much more complicated version below:
UIViewController+Transitions.m:
#interface ContainerViewController : UIViewController { }
#end
#implementation ContainerViewController
- (BOOL)shouldAutorotateToInterfaceOrientation:
(UIInterfaceOrientation)toInterfaceOrientation {
return(YES);
}
#end
...
// get the screen size, compensating for orientation
- (CGSize)screenSize {
// get the size of the screen (swapping dimensions for other orientations)
CGSize size = [UIScreen mainScreen].bounds.size;
if (UIInterfaceOrientationIsLandscape(self.interfaceOrientation)) {
CGFloat width = size.width;
size.width = size.height;
size.height = width;
}
return(size);
}
// capture a screen-sized image of the receiver
- (UIImageView *)imageViewFromScreen {
// get the root layer
CALayer *layer = self.view.layer;
while(layer.superlayer) {
layer = layer.superlayer;
}
// get the size of the bitmap
CGSize size = [self screenSize];
// make a bitmap to copy the screen into
UIGraphicsBeginImageContextWithOptions(
size, YES,
[UIScreen mainScreen].scale);
CGContextRef context = UIGraphicsGetCurrentContext();
// compensate for orientation
if (self.interfaceOrientation == UIInterfaceOrientationLandscapeLeft) {
CGContextTranslateCTM(context, size.width, 0);
CGContextRotateCTM(context, M_PI_2);
}
else if (self.interfaceOrientation == UIInterfaceOrientationLandscapeRight) {
CGContextTranslateCTM(context, 0, size.height);
CGContextRotateCTM(context, - M_PI_2);
}
else if (self.interfaceOrientation == UIInterfaceOrientationPortraitUpsideDown) {
CGContextTranslateCTM(context, size.width, size.height);
CGContextRotateCTM(context, M_PI);
}
// render the layer into the bitmap
[layer renderInContext:context];
// get the image
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
// close the context
UIGraphicsEndImageContext();
// make a view for the image
UIImageView *imageView =
[[[UIImageView alloc] initWithImage:image]
autorelease];
// done
return(imageView);
}
// make a transform that causes the given subview to fill the screen
// (when applied to an image of the screen)
- (CATransform3D)transformToFillScreenWithSubview:(UIView *)sourceView
includeStatusBar:(BOOL)includeStatusBar {
// get the root view
UIView *rootView = sourceView;
while (rootView.superview) rootView = rootView.superview;
// by default, zoom from the view's bounds
CGRect sourceRect = sourceView.bounds;
// convert the source view's center and size into the coordinate
// system of the root view
sourceRect = [sourceView convertRect:sourceRect toView:rootView];
CGPoint sourceCenter = CGPointMake(
CGRectGetMidX(sourceRect), CGRectGetMidY(sourceRect));
CGSize sourceSize = sourceRect.size;
// get the size and position we're expanding it to
CGSize targetSize = [self screenSize];
CGPoint targetCenter = CGPointMake(
targetSize.width / 2.0,
targetSize.height / 2.0);
// scale so that the view fills the screen
CATransform3D t = CATransform3DIdentity;
CGFloat sourceAspect = sourceSize.width / sourceSize.height;
CGFloat targetAspect = targetSize.width / targetSize.height;
CGFloat scale = 1.0;
if (sourceAspect > targetAspect)
scale = targetSize.width / sourceSize.width;
else
scale = targetSize.height / sourceSize.height;
t = CATransform3DScale(t, scale, scale, 1.0);
// compensate for the status bar in the screen image
CGFloat statusBarAdjustment = includeStatusBar ?
(([UIApplication sharedApplication].statusBarFrame.size.height / 2.0)
/ scale) : 0.0;
// transform to center the view
t = CATransform3DTranslate(t,
(targetCenter.x - sourceCenter.x),
(targetCenter.y - sourceCenter.y) + statusBarAdjustment,
0.0);
return(t);
}
- (void)expandView:(UIView *)sourceView
toModalViewController:(UIViewController *)modalViewController {
// get an image of the screen
UIImageView *imageView = [self imageViewFromScreen];
// show the modal view
[self presentModalViewController:modalViewController animated:NO];
// make a window to display the transition on top of everything else
UIWindow *window =
[[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
window.hidden = NO;
window.backgroundColor = [UIColor blackColor];
// make a view controller to display the image in
ContainerViewController *vc = [[ContainerViewController alloc] init];
vc.wantsFullScreenLayout = YES;
// show the window
[window setRootViewController:vc];
[window makeKeyAndVisible];
// add the image to the window
[vc.view addSubview:imageView];
// make a transform that makes the source view fill the screen
CATransform3D t = [self
transformToFillScreenWithSubview:sourceView
includeStatusBar:(! modalViewController.wantsFullScreenLayout)];
// animate the transform
[UIView animateWithDuration:0.4
animations:^(void) {
imageView.layer.transform = t;
} completion:^(BOOL finished) {
// we're going to crossfade, so change the background to clear
window.backgroundColor = [UIColor clearColor];
// do a little crossfade
[UIView animateWithDuration:0.25
animations:^(void) {
imageView.alpha = 0.0;
}
completion:^(BOOL finished) {
window.hidden = YES;
[window release];
[vc release];
}];
}];
}
- (void)dismissModalViewControllerToView:(UIView *)view {
// temporarily remove the modal dialog so we can get an accurate screenshot
// with orientation applied
UIViewController *modalViewController = [self.modalViewController retain];
[self dismissModalViewControllerAnimated:NO];
// capture the screen
UIImageView *imageView = [self imageViewFromScreen];
// put the modal view controller back
[self presentModalViewController:modalViewController animated:NO];
[modalViewController release];
// make a window to display the transition on top of everything else
UIWindow *window =
[[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
window.hidden = NO;
window.backgroundColor = [UIColor clearColor];
// make a view controller to display the image in
ContainerViewController *vc = [[ContainerViewController alloc] init];
vc.wantsFullScreenLayout = YES;
// show the window
[window setRootViewController:vc];
[window makeKeyAndVisible];
// add the image to the window
[vc.view addSubview:imageView];
// make the subview initially fill the screen
imageView.layer.transform = [self
transformToFillScreenWithSubview:view
includeStatusBar:(! self.modalViewController.wantsFullScreenLayout)];
// animate a little crossfade
imageView.alpha = 0.0;
[UIView animateWithDuration:0.15
animations:^(void) {
imageView.alpha = 1.0;
}
completion:^(BOOL finished) {
// remove the modal view
[self dismissModalViewControllerAnimated:NO];
// set the background so the real screen won't show through
window.backgroundColor = [UIColor blackColor];
// animate the screen shrinking back to normal
[UIView animateWithDuration:0.4
animations:^(void) {
imageView.layer.transform = CATransform3DIdentity;
}
completion:^(BOOL finished) {
// hide the transition stuff
window.hidden = YES;
[window release];
[vc release];
}];
}];
}
Whew! But now it looks just about like Apple's version without using any restricted APIs. Also, it works even if the orientation changes while the modal view is in front.

Making the effect is simple. You take the full-sized view, initialize its transform and center to position it on top of the thumbnail, add it to the appropriate superview, and then in an animation block reset the transform and center to position it in the final position. To dismiss the view, just do the opposite: in an animation block set transform and center to position it on top of the thumbnail, and then remove it completely in the completion block.
Note that trying to zoom from a point (i.e. a rectangle with 0 width and 0 height) will screw things up. If you're wanting to do that, zoom from a rectangle with width/height something like 0.00001 instead.
One way would be to do the same as in #1, and then call presentModalViewController:animated: with animated NO to present the actual view controller when the animation is complete (which, if done right, would result in no visible difference due to the presentModalViewController:animated: call). And dismissModalViewControllerAnimated: with NO followed by the same as in #1 to dismiss.
Or you could manipulate the modal view controller's view directly as in #1, and accept that parentViewController, interfaceOrientation, and some other stuff just won't work right in the modal view controller since Apple doesn't support us creating our own container view controllers.

After watching the Youtube iPad animation, I figured out that it's just an illusion. Let's say that there's a SearchViewController for the search results, and a DetailViewController for the video itself, and the additional info of the video.
DetailViewController has a method like - (id)initWithFullscreen which starts the view controller using the full screen space with the video.
So the sequence goes like this:
SearchViewController presents its results.
User clicks on a video.
DetailViewController is created with initWithFullscreen, but not presented
The "Zoom in" animation begins. (Notice that we are still on the SearchViewController, and this animation is just a simple View animation)
The "Zoom in" animation ends, presents the DetailViewController with animated:NO (as Anomie mentioned).
The DetailViewController is now presented, and using full space.
It doesn't seem that the youtube app is doing anything fancier, the give-away was that the "Zoom in" animation zooms to a black square, before presenting the full video.

Related

How do I calculate the correct CGRect origin on a scaled UIView subview?

I need to calculate the visible CGRect of a UIView subview, in the coordinates of the original view. I've got it working if the scale is 1, but if one of the superviews or the view itself is scaled (pinch), the visible CGRect origin is offset slightly.
This works when the scale of the views is 1 or the view is a subview of the root view:
// return the part of the passed view that is visible
// TODO: figure out why result origin is wrong for scaled subviews
//
- (CGRect)getVisibleRect:(UIView *)view {
// get the root view controller (and it's view is vc.view)
UIViewController *vc = UIApplication.sharedApplication.keyWindow.rootViewController;
// get the view's frame in the root view's coordinate system
CGRect frame = [vc.view convertRect:view.frame fromView:view.superview];
// get the intersection of the root view bounds and the passed view frame
CGRect intersection = CGRectIntersection(vc.view.bounds, frame);
// adjust the intersection coordinates thru any nested views
UIView *loopView = view;
do {
intersection = [loopView convertRect:intersection fromView:loopView.superview];
loopView = loopView.superview;
} while (loopView != vc.view);
return intersection; // may be same as the original view frame
}
When a subview is scaled, the size of the resultant view is correct, but the origin is offset by a small amount. It appears that the convertRect does not calculate the origin properly for scaled subviews.
I tried adjusting the origin relative to the X/Y transform scale but I could not get the calculation correct. Perhaps someone can help?
To save time, here is a complete test ViewController.m, where a box with an X is drawn on the visible part of the views - just create a reset button in the Main.storyboard and connect it to the reset method:
//
// ViewController.m
// VisibleViewDemo
//
// Copyright © 2018 ByteSlinger. All rights reserved.
//
#import "ViewController.h"
CG_INLINE void drawLine(UIView *view,CGPoint point1,CGPoint point2, UIColor *color, NSString *layerName) {
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:point1];
[path addLineToPoint:point2];
CAShapeLayer *shapeLayer = [CAShapeLayer layer];
shapeLayer.path = [path CGPath];
shapeLayer.strokeColor = color.CGColor;
shapeLayer.lineWidth = 2.0;
shapeLayer.fillColor = [UIColor clearColor].CGColor;
shapeLayer.name = layerName;
[view.layer addSublayer:shapeLayer];
}
CG_INLINE void removeShapeLayers(UIView *view,NSString *layerName) {
if (view.layer.sublayers.count > 0) {
for (CALayer *layer in [view.layer.sublayers copy]) {
if ([layer.name isEqualToString:layerName]) {
[layer removeFromSuperlayer];
}
}
}
}
CG_INLINE void drawXBox(UIView *view, CGRect rect,UIColor *color) {
NSString *layerName = #"xbox";
removeShapeLayers(view, layerName);
CGPoint topLeft = CGPointMake(rect.origin.x,rect.origin.y);
CGPoint topRight = CGPointMake(rect.origin.x + rect.size.width,rect.origin.y);
CGPoint bottomLeft = CGPointMake(rect.origin.x, rect.origin.y + rect.size.height);
CGPoint bottomRight = CGPointMake(rect.origin.x + rect.size.width, rect.origin.y + rect.size.height);
drawLine(view,topLeft,topRight,color,layerName);
drawLine(view,topRight,bottomRight,color,layerName);
drawLine(view,topLeft,bottomLeft,color,layerName);
drawLine(view,bottomLeft,bottomRight,color,layerName);
drawLine(view,topLeft,bottomRight,color,layerName);
drawLine(view,topRight,bottomLeft,color,layerName);
}
#interface ViewController ()
#end
#implementation ViewController
UIView *view1;
UIView *view2;
UIView *view3;
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
CGFloat width = [UIScreen mainScreen].bounds.size.width / 2;
CGFloat height = [UIScreen mainScreen].bounds.size.height / 4;
view1 = [[UIView alloc] initWithFrame:CGRectMake(width / 2, height / 2, width, height)];
view1.backgroundColor = UIColor.yellowColor;
[self.view addSubview:view1];
[self addGestures:view1];
view2 = [[UIView alloc] initWithFrame:CGRectMake(width / 2, height / 2 + height + 16, width, height)];
view2.backgroundColor = UIColor.greenColor;
[self.view addSubview:view2];
[self addGestures:view2];
view3 = [[UIView alloc] initWithFrame:CGRectMake(10, 10, width / 2, height / 2)];
view3.backgroundColor = [UIColor.blueColor colorWithAlphaComponent:0.5];
[view1 addSubview:view3]; // this one will behave differently
[self addGestures:view3];
}
- (void)viewWillLayoutSubviews {
[super viewWillLayoutSubviews];
[self checkOnScreen:view1];
[self checkOnScreen:view2];
[self checkOnScreen:view3];
}
- (IBAction)reset:(id)sender {
view1.transform = CGAffineTransformIdentity;
view2.transform = CGAffineTransformIdentity;
view3.transform = CGAffineTransformIdentity;
[self.view setNeedsLayout];
}
- (void)addGestures:(UIView *)view {
UIPanGestureRecognizer *panGestureRecognizer = [[UIPanGestureRecognizer alloc]
initWithTarget:self action:#selector(handlePan:)];
[view addGestureRecognizer:panGestureRecognizer];
UIPinchGestureRecognizer *pinchGestureRecognizer = [[UIPinchGestureRecognizer alloc]
initWithTarget:self action:#selector(handlePinch:)];
[view addGestureRecognizer:pinchGestureRecognizer];
}
// return the part of the passed view that is visible
- (CGRect)getVisibleRect:(UIView *)view {
// get the root view controller (and it's view is vc.view)
UIViewController *vc = UIApplication.sharedApplication.keyWindow.rootViewController;
// get the view's frame in the root view's coordinate system
CGRect frame = [vc.view convertRect:view.frame fromView:view.superview];
// get the intersection of the root view bounds and the passed view frame
CGRect intersection = CGRectIntersection(vc.view.bounds, frame);
// adjust the intersection coordinates thru any nested views
UIView *loopView = view;
do {
intersection = [loopView convertRect:intersection fromView:loopView.superview];
loopView = loopView.superview;
} while (loopView != vc.view);
return intersection; // may be same as the original view
}
- (void)checkOnScreen:(UIView *)view {
CGRect visibleRect = [self getVisibleRect:view];
if (CGRectEqualToRect(visibleRect, CGRectNull)) {
visibleRect = CGRectZero;
}
drawXBox(view,visibleRect,UIColor.blackColor);
}
//
// Pinch (resize) an image on the ViewController View
//
- (IBAction)handlePinch:(UIPinchGestureRecognizer *)recognizer {
static CGAffineTransform initialTransform;
if (recognizer.state == UIGestureRecognizerStateBegan) {
[self.view bringSubviewToFront:recognizer.view];
initialTransform = recognizer.view.transform;
} else if (recognizer.state == UIGestureRecognizerStateEnded) {
} else {
recognizer.view.transform = CGAffineTransformScale(initialTransform,recognizer.scale,recognizer.scale);
[self checkOnScreen:recognizer.view];
[self.view setNeedsLayout]; // update subviews
}
}
- (IBAction)handlePan:(UIPanGestureRecognizer *)recognizer {
static CGAffineTransform initialTransform;
if (recognizer.state == UIGestureRecognizerStateBegan) {
[self.view bringSubviewToFront:recognizer.view];
initialTransform = recognizer.view.transform;
} else if (recognizer.state == UIGestureRecognizerStateEnded) {
} else {
//get the translation amount in x,y
CGPoint translation = [recognizer translationInView:recognizer.view];
recognizer.view.transform = CGAffineTransformTranslate(initialTransform,translation.x,translation.y);
[self checkOnScreen:recognizer.view];
[self.view setNeedsLayout]; // update subviews
}
}
#end
So you need to know the real visible frame of a view that is somehow derived from bounds+center+transform and calculate everything else from that, instead of the ordinary frame value. This means you'll also have to recreate convertRect:fromView: to be based on that. I always sidestepped the problem by using transform only for short animations where such calculations are not necessary. Thinking about coding such a -getVisibleRect: method makes me want to run away screaming ;)
What is a frame?
The frame property is derived from center and bounds.
Example:
center is (60,50)
bounds is (0,0,100,100)
=> frame is (10,0,100,100)
Now you change the frame to (10,20,100,100). Because the size of the view did not change, this results only in a change to the center. The new center is now (60,70).
How about transform?
Say you now transform the view, by scaling it to 50%.
=> the view has now half the size than before, while still keeping the same center. It looks like the new frame is (35,45,50,50). However the real result is:
center is still (60,50): this is expected
bounds is still (0,0,100,100): this should be expected too
frame is still (10,20,100,100): this is somewhat counterintuitive
frame is a calculated property, and it doesn't care at all about the current transform. This means that the value of the frame is meaningless whenever transform is not the identity transform. This is even documented behaviour. Apple calls the value of frame to be "undefined" in this case.
Consequences
This has the additional consequences that methods such as convertRect:fromView: do not work properly when there are non-standard transforms involved. This is because all these methods rely on either frame or bounds of views, and they break as soon as there are transforms involved.
What can be done?
Say you have three views:
view1 (no transform)
view2 (scale transform 50%)
view3 (no transform)
and you want to know the coordinates of view3 from the point of view of view1.
From the point of view of view2, view3 has frame view3.frame. Easy.
From the point of view of view1, view2 has not frame view2.frame, but the visible frame is a rectangle with size view2.bounds/2 and center view2.center.
To get this right you need some basic linear algebra (with matrix multiplications). (And don't forget the anchorPoint..)
I hope it helps..
What can be done for real?
In your question you said that there is an offset. Maybe you can just calculate the error now? The error should be something like 0.5 * (1-scale) * (bounds.size) . If you can calculate the error, you can subtract it and call it a day :)
Thanks to #Michael for putting in so much effort in his answer. It didn't solve the problem but it made me think some more and try some other things.
And voila, I tried something that I'm certain I had done before, but this time I started with my latest code. It turns out a simple solution did the trick. The builtin UIView convertRect:fromView and convertRect:toView worked as expected when used together.
I apologize to anyone that has spent time on this. I'm humbled in my foolishness and how much time I have spent on this. I must have made a mistake somewhere when I tried this before because it didn't work. But this works very well now:
// return the part of the passed view that is visible
- (CGRect)getVisibleRect:(UIView *)view {
// get the root view controller (and it's view is vc.view)
UIViewController *vc = UIApplication.sharedApplication.keyWindow.rootViewController;
// get the view's frame in the root view's coordinate system
CGRect rootRect = [vc.view convertRect:view.frame fromView:view.superview];
// get the intersection of the root view bounds and the passed view frame
CGRect rootVisible = CGRectIntersection(vc.view.bounds, rootRect);
// convert the rect back to the initial view's coordinate system
CGRect visible = [view convertRect:rootVisible fromView:vc.view];
return visible; // may be same as the original view frame
}
If someone uses the Viewcontroller.m from my question, just replace the getVisibleRect method with this one and it will work very nicely.
NOTE: I tried rotating the view and the visible rect is rotated too because I displayed it on the view itself. I guess I could reverse whatever the view rotation is on the shape layers, but that's for another day!

View's position is changed while adding a child view?

I have two views side by side(left & right).At one time i am just showing one view other view not visible because i have moved that view out of scene towards the right of screen.I have set the x position in such a way so that it is not visible(view is always to the right of screen so not visible).So i press next button which is near to first view then my second view is visible & first view is hidden. I have used animation to slide thew view from left to right & right to left.
-(void)handleSingleTapGesture:(UITapGestureRecognizer *)tapGestureRecognizer
{
NSLog(#"first");
CGFloat screenWidth = 0.0;
if(IS_IPHONE_6)
{
screenWidth=self.view.frame.size.width-120.0;
}
if(IS_IPHONE_5)
{
screenWidth=self.view.frame.size.width-68.0;
}
NSLog(#"New screen width is %f",screenWidth);
[self.back_view setHidden:false];
[self.front_view setHidden:true];
[UIView animateWithDuration:0.5 animations:^{
self.front_view.frame = CGRectOffset(self.front_view.frame, -screenWidth, 0.0);
self.back_view.frame = CGRectOffset(self.back_view.frame, -screenWidth, 0.0);
}];
}
-(void)handleSingleTapGesture_one:(UITapGestureRecognizer *)tapGestureRecognizer
{
CGFloat screenWidth = 0.0;
if(IS_IPHONE_6)
{
screenWidth=self.view.frame.size.width-120.0;
}
if(IS_IPHONE_5)
{
screenWidth=self.view.frame.size.width-68.0;
}
NSLog(#"New screen width is %f",screenWidth);
[self.back_view setHidden:true];
[self.front_view setHidden:false];
[UIView animateWithDuration:0.5 animations:^{
self.front_view.frame = CGRectOffset(self.front_view.frame, screenWidth, 0.0);
self.back_view.frame = CGRectOffset(self.back_view.frame, screenWidth, 0.0);
}];
}
On the backView i have added a subview on bottom-right so when ever i try to add the view inside that view then then main view is shifted to the right i don't know why.Please tell me how can i get the solution?

Fake push and pop animation when replace UIView

For some reasons, I have to use addSubview and addChildViewController to replace view instead of push/pop view controller. The problem is that I want to fake exactly the animation when changing UIViewController (push/pop).
Here is my try:
In RootViewController.m
// switch controller view
currentController = nextViewController;
[self addChildViewController:currentController];
[self.view addSubview:currentController.view];
// pop - move down
[currentController.view setFrame:CGRectMake(self.view.frame.size.width, 0, currentController.view.frame.size.width, currentController.view.frame.size.height)];
[UIView animateWithDuration:0.5
animations:^{
[currentController.view setFrame:CGRectMake(self.view.frame.size.width, self.view.frame.size.height, currentController.view.frame.size.width, currentController.view.frame.size.height)];
[self addConstrainForView];
}];
//Push - move up
[currentController.view setFrame:CGRectMake(0, self.view.frame.size.height, currentController.view.frame.size.width, currentController.view.frame.size.height)];
[UIView animateWithDuration:0.5
animations:^{
[currentController.view setFrame:CGRectMake(0, 0, currentController.view.frame.size.width, currentController.view.frame.size.height)];
[self addConstrainForView];
}];
This code doesn't work as aspect because the next controller move from bottom to top (push), and when it is moving, there is a blank background behind it (root controller 's background).
So I need the correct way to fake push and pop animation (pull up & pull down). Any help would be appreciated.
I use this effect in an app. I wanted (and have) the look of an iPhone series of pick screens embedded into the edge of an iPad app. So the user presses a button from a list then the screen transitions, that looks like a UIViewController advancing, to more options, though it's all in a window on the edge (and the relevant content pops up on the right). Here's the code I use:
- (void)advanceLevel
{
UIImageView *screenShot = [self getTableScreenShot];
[self.tableView.superview addSubview:screenShot];
// Put screen shot over the whole thing with mask for iPhone style animation
TableViewMask *mask =
[[TableViewMask alloc] initWithFrame:CGRectMake(0, 0, 1024, 768) withImage:[Util TakeScreenshot]];
[self.tableView.superview addSubview:mask];
tableLevel++;
[self updateData];
[UIView animateWithDuration:.6 animations:^
{
screenShot.frame =
CGRectMake(self.tableView.frame.origin.x-self.tableView.frame.size.width, self.tableView.frame.origin.y,
self.tableView.frame.size.width, self.tableView.frame.size.height);
controller.toolbar.backButton.alpha = (tableLevel>1?1:0);
}
completion:^(BOOL finished)
{
[screenShot removeFromSuperview];
[mask removeFromSuperview];
[self updateUI];
}];
}
- (void)goBackLevel
{
if(tableLevel==1){ return; }
if(tableLevel==3 && isEditing==YES)
{
// Skip this screen if coming from higher screens
tableLevel--;
}
UIImageView *screenShot = [self getTableScreenShot];
[self.tableView.superview addSubview:screenShot];
// Put screen shot over the whole thing with mask for iPhone style animation
TableViewMask *mask = [[TableViewMask alloc] initWithFrame:CGRectMake(0, 0, 1024, 768) withImage:[Util TakeScreenshot]];
[self.tableView.superview addSubview:mask];
tableLevel--;
[self updateData];
[UIView animateWithDuration:.6 animations:^
{
screenShot.frame =
CGRectMake(self.tableView.frame.origin.x+self.tableView.frame.size.width, self.tableView.frame.origin.y,
self.tableView.frame.size.width, self.tableView.frame.size.height);
controller.toolbar.backButton.alpha = (tableLevel>1?1:0);
}
completion:^(BOOL finished)
{
[screenShot removeFromSuperview];
[mask removeFromSuperview];
[self updateUI];
}];
}
... here's the screen-shot taker:
+ (UIImage *)TakeScreenshot
{
CGSize imageSize = [[UIScreen mainScreen] bounds].size;
UIGraphicsBeginImageContextWithOptions(imageSize, NO, 0);
CGContextRef context = UIGraphicsGetCurrentContext();
// Iterate over every window from back to front
for (UIWindow *window in [[UIApplication sharedApplication] windows])
{
if (![window respondsToSelector:#selector(screen)] || [window screen] == [UIScreen mainScreen])
{
// -renderInContext: renders in the coordinate space of the layer,
// so we must first apply the layer's geometry to the graphics context
CGContextSaveGState(context);
// Center the context around the window's anchor point
CGContextTranslateCTM(context, [window center].x, [window center].y);
// Apply the window's transform about the anchor point
CGContextConcatCTM(context, [window transform]);
// Offset by the portion of the bounds left of and above the anchor point
CGContextTranslateCTM(context,
-[window bounds].size.width * [[window layer] anchorPoint].x,
-[window bounds].size.height * [[window layer] anchorPoint].y);
// Render the layer hierarchy to the current context
[[window layer] renderInContext:context];
// Restore the context
CGContextRestoreGState(context);
}
}
// Retrieve the screenshot image
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
}
.. and finally TableViewMask:
#implementation TableViewMask
UIImage *screenShotImage;
- (instancetype)initWithFrame:(CGRect)frame withImage:(UIImage *)_screenShotImage
{
self = [super initWithFrame:frame];
if(self)
{
self.backgroundColor = [UIColor clearColor];
screenShotImage = _screenShotImage;
[self setNeedsDisplay];
}
return self;
}
- (void)drawRect:(CGRect)rect
{
CGContextRef context = UIGraphicsGetCurrentContext();
[screenShotImage drawInRect:rect];
CGRect intersection = CGRectIntersection( CGRectMake(10.2, 130, 299.6, 600), rect );
if( CGRectIntersectsRect( intersection, rect ) )
{
CGContextFillRect(context, CGRectMake(10, 130, 300, 600));
CGContextClearRect(context, intersection);
CGContextSetFillColorWithColor( context, [UIColor clearColor].CGColor );
CGContextFillRect( context, intersection);
}
}
#end
I left in some things that are unique to mine because you'll likely need similar calls. For example [self updateData] takes the state of the system into account to update the model and set any other state-based calls. There's also an exception when a screen should be skipped I've left in. You can safely remove those, though you'll probably need similar variants (since you're not really changing UIViewController's).
The only caveat I'd have with this is that unless you have a corner case like mine -- imitating an iPhone in a popped out view on an iPad or something similar -- it's probably better just to use real UIVewController's.
This article would seem relevant to what you are trying to do?
When to use addChildViewController vs pushViewController
It seems to suggest you can use transitionFromViewController:toViewController:duration:options:animations:completion: to animate transition between controllers you are managing yourself.

Custom transition results in black screen or unresponsive screen

Ive created a custom UIViewControllerAnimatedTransition (the code is show below). The transition will focus only on the animation when dismissing the view. When I dismiss the view I get a black screen, the master is briefly shown before it disappears. Please see the code bel
#import "GCSplitDismissTransition.h"
#implementation GCSplitDismissTransition
#pragma mark - UIViewControllerAnimatedTransitioning protocol
-(void) animateTransition: (id < UIViewControllerContextTransitioning > ) transitionContext {
UIViewController * fromVC = [transitionContext viewControllerForKey: UITransitionContextFromViewControllerKey];
UIViewController * toVC = [transitionContext viewControllerForKey: UITransitionContextToViewControllerKey];
UIView * inView = [transitionContext containerView];
UIView * masterView = toVC.view;
UIView * detailView = fromVC.view;
masterView.frame = [transitionContext finalFrameForViewController: toVC];
// add the to VC's view to the intermediate view (where it has to be at the
// end of the transition anyway). We'll hide it during the transition with
// a blank view. This ensures that renderInContext of the 'To' view will
// always render correctly
[inView addSubview: toVC.view];
// if the detail view is a UIScrollView (eg a UITableView) then
// get its content offset so we get the snapshot correctly
CGPoint detailContentOffset = CGPointMake(.0, .0);
if ([detailView isKindOfClass: [UIScrollView class]]) {
detailContentOffset = ((UIScrollView * ) detailView).contentOffset;
}
// if the master view is a UIScrollView (eg a UITableView) then
// get its content offset so we get the snapshot correctly and
// so we can correctly calculate the split point for the zoom effect
CGPoint masterContentOffset = CGPointMake(.0, .0);
if ([masterView isKindOfClass: [UIScrollView class]]) {
masterContentOffset = ((UIScrollView * ) masterView).contentOffset;
}
// Take a snapshot of the detail view
// use renderInContext: instead of the new iOS7 snapshot API as that
// only works for views that are currently visible in the view hierarchy
UIGraphicsBeginImageContextWithOptions(detailView.bounds.size, detailView.opaque, 0);
CGContextRef ctx = UIGraphicsGetCurrentContext();
CGContextTranslateCTM(ctx, 0, -detailContentOffset.y);
[detailView.layer renderInContext: ctx];
UIImage * detailSnapshot = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
// take a snapshot of the master view
// use renderInContext: instead of the new iOS7 snapshot API as that
// only works for views that are currently visible in the view hierarchy
UIGraphicsBeginImageContextWithOptions(masterView.bounds.size, masterView.opaque, 0);
ctx = UIGraphicsGetCurrentContext();
CGContextTranslateCTM(ctx, 0, -masterContentOffset.y);
[masterView.layer renderInContext: ctx];
UIImage * masterSnapshot = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
// get the rect of the source cell in the coords of the from view
CGRect sourceRect = [masterView convertRect: self.sourceView.bounds fromView: self.sourceView];
CGFloat splitPoint = sourceRect.origin.y + sourceRect.size.height - masterContentOffset.y;
CGFloat scale = [UIScreen mainScreen].scale;
// split the master view snapshot into two parts, splitting
// below the master view (usually a UITableViewCell) that originated the transition
CGImageRef masterImgRef = masterSnapshot.CGImage;
CGImageRef topImgRef = CGImageCreateWithImageInRect(masterImgRef, CGRectMake(0, 0, masterSnapshot.size.width * scale, splitPoint * scale));
UIImage * topImage = [UIImage imageWithCGImage: topImgRef scale: scale orientation: UIImageOrientationUp];
CGImageRelease(topImgRef);
CGImageRef bottomImgRef = CGImageCreateWithImageInRect(masterImgRef, CGRectMake(0, splitPoint * scale, masterSnapshot.size.width * scale, (masterSnapshot.size.height - splitPoint) * scale));
UIImage * bottomImage = [UIImage imageWithCGImage: bottomImgRef scale: scale orientation: UIImageOrientationUp];
CGImageRelease(bottomImgRef);
// create views for the top and bottom parts of the master view
UIImageView * masterTopView = [
[UIImageView alloc] initWithImage: topImage
];
UIImageView * masterBottomView = [
[UIImageView alloc] initWithImage: bottomImage
];
CGRect bottomFrame = masterBottomView.frame;
bottomFrame.origin.y = splitPoint;
masterBottomView.frame = bottomFrame;
// setup the inital and final frames for the master view top and bottom
// views depending on whether we're doing a push or a pop transition
CGRect masterTopEndFrame = masterTopView.frame;
CGRect masterBottomEndFrame = masterBottomView.frame;
CGRect masterTopStartFrame = masterTopView.frame;
masterTopStartFrame.origin.y = -(masterTopStartFrame.size.height - sourceRect.size.height);
masterTopView.frame = masterTopStartFrame;
CGRect masterBottomStartFrame = masterBottomView.frame;
masterBottomStartFrame.origin.y += masterBottomStartFrame.size.height;
masterBottomView.frame = masterBottomStartFrame;
CGFloat initialAlpha = 1.0;
CGFloat finalAlpha = .0;
// create views to cover the master top and bottom views so that
// we can fade them in / out
UIView * masterTopFadeView = [
[UIView alloc] initWithFrame: masterTopView.frame
];
masterTopFadeView.backgroundColor = masterView.backgroundColor;
masterTopFadeView.alpha = initialAlpha;
UIView * masterBottomFadeView = [
[UIView alloc] initWithFrame: masterBottomView.frame
];
masterBottomFadeView.backgroundColor = masterView.backgroundColor;
masterBottomFadeView.alpha = initialAlpha;
// create snapshot view of the to view
UIImageView * detailSmokeScreenView = [
[UIImageView alloc] initWithImage: detailSnapshot
];
// create a background view so that we don't see the actual VC
// views anywhere - start with a blank canvas.
UIView * backgroundView = [
[UIView alloc] initWithFrame: inView.frame
];
backgroundView.backgroundColor = [UIColor lightGrayColor];
// add all the views to the transition view
[inView addSubview: backgroundView];
[inView addSubview: detailSmokeScreenView];
[inView addSubview: masterTopView];
[inView addSubview: masterTopFadeView];
[inView addSubview: masterBottomView];
[inView addSubview: masterBottomFadeView];
NSTimeInterval totalDuration = [self transitionDuration: transitionContext];
[UIView animateKeyframesWithDuration: totalDuration
delay: 0
options: UIViewKeyframeAnimationOptionCalculationModeLinear
animations: ^ {
// move the master view top and bottom views (and their
// respective fade views) to where we wna them to end up
masterTopView.frame = masterTopEndFrame;
masterTopFadeView.frame = masterTopEndFrame;
masterBottomView.frame = masterBottomEndFrame;
masterBottomFadeView.frame = masterBottomEndFrame;
detailSmokeScreenView.layer.transform = CATransform3DMakeAffineTransform(CGAffineTransformMakeScale(.1, .1));
// fade out (or in) the master view top and bottom views
// want the fade out animation to happen near the end of the transition
// and the fade in animation to happen at the start of the transition
CGFloat fadeStartTime = .0;
[UIView addKeyframeWithRelativeStartTime: fadeStartTime relativeDuration: .5 animations: ^ {
masterTopFadeView.alpha = finalAlpha;
masterBottomFadeView.alpha = finalAlpha;
}];
}
completion: ^ (BOOL finished) {
// remove all the intermediate views from the heirarchy
[backgroundView removeFromSuperview];
[detailSmokeScreenView removeFromSuperview];
[masterTopView removeFromSuperview];
[masterTopFadeView removeFromSuperview];
[masterBottomView removeFromSuperview];
[masterBottomFadeView removeFromSuperview];
[transitionContext completeTransition: YES];
}
];
}
If I comment out this line : [transitionContext completeTransition: YES]; then the black view does not appear but the master is unresponsive. The idea was adapted from https://github.com/mluisbrown/LCZoomTransition
If there are any problems, please let me know, I hope I provided enough.
Update
import "GCSplitPresentTransition.h"
#implementation GCSplitPresentTransition
-(void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext{
UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
UIView *inView = [transitionContext containerView];
UIView *masterView = fromVC.view;
UIView *detailView = toVC.view;
detailView.frame = [transitionContext finalFrameForViewController:toVC];
// add the to VC's view to the intermediate view (where it has to be at the
// end of the transition anyway). We'll hide it during the transition with
// a blank view. This ensures that renderInContext of the 'To' view will
// always render correctly
[inView addSubview:toVC.view];
// if the detail view is a UIScrollView (eg a UITableView) then
// get its content offset so we get the snapshot correctly
CGPoint detailContentOffset = CGPointMake(.0, .0);
if ([detailView isKindOfClass:[UIScrollView class]]) {
detailContentOffset = ((UIScrollView *)detailView).contentOffset;
}
// if the master view is a UIScrollView (eg a UITableView) then
// get its content offset so we get the snapshot correctly and
// so we can correctly calculate the split point for the zoom effect
CGPoint masterContentOffset = CGPointMake(.0, .0);
if ([masterView isKindOfClass:[UIScrollView class]]) {
masterContentOffset = ((UIScrollView *) masterView).contentOffset;
}
// Take a snapshot of the detail view
// use renderInContext: instead of the new iOS7 snapshot API as that
// only works for views that are currently visible in the view hierarchy
UIGraphicsBeginImageContextWithOptions(detailView.bounds.size, detailView.opaque, 0);
CGContextRef ctx = UIGraphicsGetCurrentContext();
CGContextTranslateCTM(ctx, 0, -detailContentOffset.y);
[detailView.layer renderInContext:ctx];
UIImage *detailSnapshot = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
// take a snapshot of the master view
// use renderInContext: instead of the new iOS7 snapshot API as that
// only works for views that are currently visible in the view hierarchy
UIGraphicsBeginImageContextWithOptions(masterView.bounds.size, masterView.opaque, 0);
ctx = UIGraphicsGetCurrentContext();
CGContextTranslateCTM(ctx, 0, -masterContentOffset.y);
[masterView.layer renderInContext:ctx];
UIImage *masterSnapshot = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
// get the rect of the source cell in the coords of the from view
CGRect sourceRect = [masterView convertRect:self.sourceView.bounds fromView:self.sourceView];
CGFloat splitPoint = sourceRect.origin.y + sourceRect.size.height - masterContentOffset.y;
CGFloat scale = [UIScreen mainScreen].scale;
// split the master view snapshot into two parts, splitting
// below the master view (usually a UITableViewCell) that originated the transition
CGImageRef masterImgRef = masterSnapshot.CGImage;
CGImageRef topImgRef = CGImageCreateWithImageInRect(masterImgRef, CGRectMake(0, 0, masterSnapshot.size.width * scale, splitPoint * scale));
UIImage *topImage = [UIImage imageWithCGImage:topImgRef scale:scale orientation:UIImageOrientationUp];
CGImageRelease(topImgRef);
CGImageRef bottomImgRef = CGImageCreateWithImageInRect(masterImgRef, CGRectMake(0, splitPoint * scale, masterSnapshot.size.width * scale, (masterSnapshot.size.height - splitPoint) * scale));
UIImage *bottomImage = [UIImage imageWithCGImage:bottomImgRef scale:scale orientation:UIImageOrientationUp];
CGImageRelease(bottomImgRef);
// create views for the top and bottom parts of the master view
UIImageView *masterTopView = [[UIImageView alloc] initWithImage:topImage];
UIImageView *masterBottomView = [[UIImageView alloc] initWithImage:bottomImage];
CGRect bottomFrame = masterBottomView.frame;
bottomFrame.origin.y = splitPoint;
masterBottomView.frame = bottomFrame;
// setup the inital and final frames for the master view top and bottom
// views depending on whether we're doing a push or a pop transition
CGRect masterTopEndFrame = masterTopView.frame;
CGRect masterBottomEndFrame = masterBottomView.frame;
masterTopEndFrame.origin.y = -(masterTopEndFrame.size.height - sourceRect.size.height);
masterBottomEndFrame.origin.y += masterBottomEndFrame.size.height;
CGFloat initialAlpha = 1.0;
CGFloat finalAlpha = 1.0;
// create views to cover the master top and bottom views so that
// we can fade them in / out
UIView *masterTopFadeView = [[UIView alloc] initWithFrame:masterTopView.frame];
masterTopFadeView.backgroundColor = masterView.backgroundColor;
masterTopFadeView.alpha = initialAlpha;
UIView *masterBottomFadeView = [[UIView alloc] initWithFrame:masterBottomView.frame];
masterBottomFadeView.backgroundColor = masterView.backgroundColor;
masterBottomFadeView.alpha = initialAlpha;
// create snapshot view of the to view
UIImageView *detailSmokeScreenView = [[UIImageView alloc] initWithImage:detailSnapshot];
// for a push transition, make the detail view small, to be zoomed in
// for a pop transition, the detail view will be zoomed out, so it starts without
// a transform
// create a background view so that we don't see the actual VC
// views anywhere - start with a blank canvas.
UIView *backgroundView = [[UIView alloc] initWithFrame:inView.frame];
//backgroundView.backgroundColor = self.transitionBackgroundColor;
// add all the views to the transition view
[inView addSubview:backgroundView];
[inView addSubview:detailSmokeScreenView];
[inView addSubview:masterTopView];
[inView addSubview:masterTopFadeView];
[inView addSubview:masterBottomView];
[inView addSubview:masterBottomFadeView];
NSTimeInterval totalDuration = [self transitionDuration:transitionContext];
[UIView animateKeyframesWithDuration:totalDuration
delay:0
options:UIViewKeyframeAnimationOptionCalculationModeLinear
animations:^{
// move the master view top and bottom views (and their
// respective fade views) to where we wna them to end up
masterTopView.frame = masterTopEndFrame;
masterTopFadeView.frame = masterTopEndFrame;
masterBottomView.frame = masterBottomEndFrame;
masterBottomFadeView.frame = masterBottomEndFrame;
// zoom the detail view in or out, depending on whether we're doing a push
// or pop transition
detailSmokeScreenView.layer.transform = CATransform3DMakeAffineTransform(CGAffineTransformIdentity);
// fade out (or in) the master view top and bottom views
// want the fade out animation to happen near the end of the transition
// and the fade in animation to happen at the start of the transition
CGFloat fadeStartTime = .5 ;
[UIView addKeyframeWithRelativeStartTime:fadeStartTime relativeDuration:.5 animations:^{
masterTopFadeView.alpha = finalAlpha;
masterBottomFadeView.alpha = finalAlpha;
}];
}
completion:^(BOOL finished) {
// remove all the intermediate views from the heirarchy
[backgroundView removeFromSuperview];
[detailSmokeScreenView removeFromSuperview];
[masterTopView removeFromSuperview];
[masterTopFadeView removeFromSuperview];
[masterBottomView removeFromSuperview];
[masterBottomFadeView removeFromSuperview];
if ([transitionContext transitionWasCancelled]) {
// we added this at the start, so we have to remove it
// if the transition is canccelled
[toVC.view removeFromSuperview];
[transitionContext completeTransition:NO];
} else {
[fromVC.view removeFromSuperview];
[transitionContext completeTransition:YES];
}
}];
}
-(NSTimeInterval) transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext{
return 0.3;
}
#end
Update 2
This is how I present the transition:
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{
NSIndexPath *selectedIndexPath = [tableView indexPathForSelectedRow];
UITableViewCell * cell = [self.tableView cellForRowAtIndexPath:selectedIndexPath];
self.transition.sourceView=cell;
self.dismissTransition.sourceView=cell;
DetailViewController * detailViewController = [self.storyboard instantiateViewControllerWithIdentifier:#"detail"];
detailViewController.modalPresentationStyle = UIModalPresentationCustom;
detailViewController.transitioningDelegate = self;
[self presentViewController:detailViewController animated:YES completion:NULL];
}
-(id<UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source{
//return new instance of custom transition
return self.transition;
}
-(id<UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed{
return self.dismissTransition;
}
The issue has arisen as you've used the LCZoomTransition code, which was written for a navigation push-pop transition. You require code for a custom modal transition animation. The two are different, have a read of these too articles for a flavour of things.
http://www.objc.io/issue-5/view-controller-transitions.html
and
http://www.objc.io/issue-12/custom-container-view-controller-transitions.html
To fix the issue you are having, you just need to remove the code that adds and removes the master view controllers view. As it is a presenting view controller, it's view should stay in the hierarchy.
In the file GCSplitPresentTransition, remove the line
[fromVC.view removeFromSuperview];
in the animation completion handler.
And in the file GCSplitDismissTransition, remove the line
[inView addSubview: toVC.view];
at the start of the -animateTransition method.
I was seeing the symptoms, but different issue:
in [transitionContext viewControllerForKey:]
I was passing it UITransitionContextToViewKey instead of UITransitionContextToViewControllerKey.
No errors reported at compile or runtime, but resulted in a black screen

Creating a larger flip-side view for 'tile' in storyboard

I currently have a Container View Controller which is home to a button. When this is pressed, I want the view to flip and grow, similar to iTunes on the iPad.
Main View Controller Layout:
Flipping the Container:
When the flip button is pressed, I intend to use this code (Inside the container view's View Controller) to flip the container:
FrontVC *cb = [[FrontVC alloc]initWithFrame:CGRectMake(0, 0, 92, 65)];
FlipVC *cf = [[FlipVC alloc]initWithFrame:CGRectMake(0, 0, 92, 65)];
[UIView transitionWithView:self.view
duration:1
options:UIViewAnimationOptionTransitionFlipFromLeft
animations:^{
// Remove last view
for (UIView *subview in self.view.subviews) {
[subview removeFromSuperview];
}
[self.view addSubview:(displayingPrimary ? cb: cf)];
}
completion:^(BOOL finished) {
if (finished) {
displayingPrimary = !displayingPrimary;
}
}];
This works well if the two views are the same size, and I can lay out the views using code. FrontVC and FlipVC are sub-classes of UIView.
I was thinking about doing something like this to move the view to the centre of the screen, but I'm stuck from there. Hey, I don't even know if this would work! Is there a better way to do this?
if (!displayingPrimary) {
self.view.center = CGPointMake(320.0f, 480.0f);
cb.center = CGPointMake(320.0f, 480.0f);
}
else
{
self.view.center = CGPointMake(59, 324);
}
What I Am Aiming For:
Ideally, I would like to be able to design the flip view in storyboard, and have the 'tile' grow, similar to iTunes:
Whereas in my example I would like it to look like this:
How would I be able to design this in storyboard? If I try to make a view controller, I am unable to resize it to the size that I would like the flip size view controller to be.
The FlipView project is quite polished and looks exactly as the iTunes flip does. I used two imageViews for the demo project. The actual flip code is as follows:
- (IBAction)flip
{
UIView *fromView, *toView;
CGPoint pt;
CGSize size;
CGFloat sense;
if(showingView == a) {
fromView = a;
toView = b;
pt = BORIGIN;
size = BRECT.size;
sense = -1;
} else {
fromView = b;
toView = a;
pt = AORIGIN;
size = ARECT.size;
sense = 1;
}
[UIView animateWithDuration:TIME/2 animations:^
{
CATransform3D t = CATransform3DIdentity;
t.m34 = M34;
t = CATransform3DRotate(t, M_PI_2*sense, 0, 1, 0);
containerView.layer.transform = t;
containerView.frame = CRECT; // midpoint
NSLog(#"Container MID Frame %#", NSStringFromCGRect(containerView.frame));
}
completion:^(BOOL isFinished)
{
toView.layer.transform = CATransform3DMakeRotation(M_PI*sense, 0, 1, 0);
[UIView animateWithDuration:TIME/2 animations:^
{
[fromView removeFromSuperview];
[containerView addSubview:toView];
CATransform3D t = CATransform3DIdentity;
t.m34 = M34;
t = CATransform3DRotate(t, M_PI*sense, 0, 1, 0);
containerView.layer.transform = t; //finish the flip
containerView.frame = (CGRect){ pt, size};
} completion:^(BOOL isFinished)
{
NSLog(#"Container Frame %#", NSStringFromCGRect(containerView.frame));
NSLog(#"A Frame %#", NSStringFromCGRect(b.frame));
NSLog(#"B Frame %#", NSStringFromCGRect(b.frame));
toView.layer.transform = CATransform3DIdentity;
containerView.layer.transform = CATransform3DIdentity;
showingView = showingView == a ? b : a;
}];
}];
}
http://dl.dropbox.com/u/60414145/FlipView.zip
EDIT: One way to work around the resizing a real view with real controls in it might have is to take a snapshot of that view, use the snapshot while zooming/flipping, and in the final completion block switch out the image for the real view. That is:
create a UIImage of your current small view when the user taps the image
create a UIImage of the final view, rendered into a context as its natural size
use the two images in the container view, for the animation
in the final completion block, remove your imageView from view and put the second view now in self.view subviews
I did a rough code (not at all perfect) on this (hope it is what you are looking for). Have a look into it. I created a baseView and two flip views, after the first flip, I increased the frame size of baseView and added the second flip view on it.I am not an expert of Story Board, so I havent used it here.
FlipTest
-anoop

Resources