I'm trying to create a custom transition to a new view controller using a UIStoryboardSegue subclass, but I'm having some issues. I have a back button that's setup to with AutoLayout to be 5 pixels from the top layout guide (essentially 25 pixels from the top of the view). However, when I'm transiting my view doesn't take into account that the status bar exists and makes the button 5 pixels away from the top of the view instead of 25 pixels away from the top of view (status-bar.height == 20px). So when the transition finishes theres a sudden bounce when I call [[self sourceViewController] presentModalViewController:[self destinationViewController] animated:NO]; because then the button will actually be layer out correctly. Does anyone have any ideas how I can tell my view that it should layout assuming that the top layout guide starts at 20px instead of 0?
Edit: The issues seems to be directly related to the topLayoutGuide. When checking the value of topLayoutGuide it was 0 when I added the view in the transition, but when I checked it in the destination view controller's viewDidAppear it was 20 like it was supposed to be.
- (void)perform {
UIWindow *window = [[[UIApplication sharedApplication] windows] objectAtIndex:0];
UIViewController *sourceViewController = (UIViewController*)self.sourceViewController;
UIViewController *destinationViewController = (UIViewController*)self.destinationViewController;
[destinationViewController.view setCenter:CGPointMake(window.frame.size.width * 1.5, sourceViewController.view.center.y)];
[[sourceViewController.view superview] addSubview:destinationViewController.view];
[destinationViewController.view setBackgroundColor:[UIColor redColor]];
POPSpringAnimation *positionAnimation = [POPSpringAnimation animationWithPropertyNamed:kPOPLayerPositionX];
positionAnimation.toValue = #(window.center.x);
positionAnimation.springBounciness = 10;
POPSpringAnimation *fromPositionAnimation = [POPSpringAnimation animationWithPropertyNamed:kPOPLayerPositionX];
double offScreenX = -sourceViewController.view.frame.size.width;
fromPositionAnimation.toValue = #(offScreenX);
fromPositionAnimation.springBounciness = 10;
[fromPositionAnimation setCompletionBlock:^(POPAnimation *anim, BOOL finished) {
}];
POPSpringAnimation *scaleAnimation = [POPSpringAnimation animationWithPropertyNamed:kPOPLayerScaleXY];
scaleAnimation.springBounciness = 20;
scaleAnimation.fromValue = [NSValue valueWithCGPoint:CGPointMake(1.0, 1.1)];
[scaleAnimation setCompletionBlock:^(POPAnimation *anim, BOOL finished) {
[[self sourceViewController] presentModalViewController:[self destinationViewController] animated:NO];
}];
[sourceViewController.view.layer pop_addAnimation:fromPositionAnimation forKey:#"positionAnimation"];
[destinationViewController.view.layer pop_addAnimation:positionAnimation forKey:#"positionAnimation"];
[destinationViewController.view.layer pop_addAnimation:scaleAnimation forKey:#"scaleAnimation"];
}
I suggest call setNeedLayout on viewWillAppear. It's should adjust the layout of a view’s subviews.
Related
I've got a custom UIStoryboardSegue subclass which just replaces the root view controller with the destination VC. Works exactly as I want it to... however, I'd like to be able to add a transition animation, and I can't find any good examples of how to do that in the context of replacing the root VC.
The -perform selector of my class is this:
-(void)perform {
UIViewController *source = (UIViewController *)self.sourceViewController;
source.view.window.rootViewController = self.destinationViewController;
}
...How do I add a nice animated transition?
There are numerous ways you could add a "nice animation". Here is an example of a sort of card shuffle animation where one view moves up and left, while the other moves down and right, then reverses after changing the z-order of the two views. This implementation inserts the destination controller's view into the window's view hierarchy under the source controller's view.
-(void)perform {
CGFloat dur = 1.0;
UIView *destView = [self.destinationViewController view];
UIView *srcView = [self.sourceViewController view];
CGFloat viewWidth = srcView.frame.size.width;
CGPoint center = srcView.center;
AppDelegate *appDel = [[UIApplication sharedApplication] delegate];
destView.frame = srcView.bounds;
[appDel.window insertSubview:destView belowSubview:srcView];
[UIView animateWithDuration:dur animations:^{
srcView.frame = CGRectOffset(srcView.frame, -viewWidth/1.9, -20);
destView.frame = CGRectOffset(destView.frame, viewWidth/1.9, 20);
} completion:^(BOOL finished) {
[appDel.window bringSubviewToFront:destView];
[UIView animateWithDuration:dur animations:^{
destView.center = center;
srcView.center = center;
} completion:^(BOOL finished) {
[srcView removeFromSuperview];
appDel.window.rootViewController = self.destinationViewController;
}];
}];
}
I have one main viewController A with an UITabBar. My issue are when I scroll to the last cell and after I go to the viewController B with a cell click in UITableView and when I come back to my viewController A, my last cell is cut off at the bottom and I can scroll for appear all content of this cell. But by default at the bottom the UITableView doesn't keep the same place that last time.
When I launch viewController B I hide my UITabBar with this code in VCB "viewWillAppear" :
- (void)hideTabBar {
UITabBar *tabBar = self.tabBarController.tabBar;
UIView *parent = tabBar.superview;
UIView *content = [parent.subviews objectAtIndex:0];
UIView *window = parent.superview;
[UIView animateWithDuration:0.3
animations:^{
CGRect tabFrame = tabBar.frame;
tabFrame.origin.y = CGRectGetMaxY(window.bounds);
tabBar.frame = tabFrame;
content.frame = window.bounds;
}];
}
And when I come back to my viewController A I show my UITabBar with this code in VCB "viewWillDisappear":
- (void)showTabBar {
UITabBar *tabBar = self._tab;
UIView *parent = tabBar.superview;
UIView *content = [parent.subviews objectAtIndex:0];
UIView *window = parent.superview;
[UIView animateWithDuration:0.3
animations:^{
CGRect tabFrame = tabBar.frame;
tabFrame.origin.y = CGRectGetMaxY(window.bounds) - CGRectGetHeight(tabBar.frame);
tabBar.frame = tabFrame;
CGRect contentFrame = content.frame;
contentFrame.size.height -= tabFrame.size.height;
}];
}
I have the same problem in iOS 6 but the scroll doesn't allow to go at the bottom and the last cell are cut off always.
In my viewController A in "viewWillAppear" I do:
if ([[UIDevice currentDevice].systemVersion hasPrefix:#"7"]) {
[self._tabBarControllerArticle.tabBar setTranslucent:NO];
}
Before when I clicked (image 1)
When I come back (image 2)
Thanks for advance for all the answer!!!
Another way of doing it is to set translucent property of the tabBar to NO like this
// In init or viewDidLoad of tab bar controller
self.tabBar.translucent = NO;
Assuming you're using iOS7 this should adjust your UITableView just above self.tabBar
Click your tableViewController where the problem is happening, go to the Attributes Inspector, scroll down and uncheck the "Under Bottom Bars" box.
The selected answer to this question helped me.
You missed to assign frame back to your view. See the updated code.
- (void)showTabBar {
UITabBar *tabBar = self._tab;
UIView *parent = tabBar.superview;
UIView *content = [parent.subviews objectAtIndex:0];
UIView *window = parent.superview;
[UIView animateWithDuration:0.3
animations:^{
CGRect tabFrame = tabBar.frame;
tabFrame.origin.y = CGRectGetMaxY(window.bounds) - CGRectGetHeight(tabBar.frame);
tabBar.frame = tabFrame;
CGRect contentFrame = content.frame;
contentFrame.size.height -= tabFrame.size.height;
//YOU MISSED TO ADD BELOW LINE....
content.frame = contentFrame;
}];
}
I have a controller embedded in navigation controller. Let's say that i have a button that repositions self.navigationController.navigationBar a bit. Then i do presentViewControllerAnimated with any controller (doesn't matter if it's nav or not) and after dismissing it navigation bar returns to it's original position (actually it is at its original position at dismiss animation start). In iOS 6 and earlier the bar would not be repositioned automatically. Any idea how can i prevent this repositioning in iOS 7?
OK, so I finally got it right.
First of all - Apple does not want us to change position of UINavigationBar. Therefore you should avoid it at all cost. In my case i got an app to fix which moved UINavigationBar to show slide-out menu. The proper solution to slide-out menu problem is to put UINavigationController inside - then you can slide whole UINavigationController with its content (whatever it is) and everything works fine. For some reason UINavigationController was outside in this app. So, i had to resort to a hack. Do not use this method if you have ANY option not to use it. It's a hack, it might break in further iOS versions and Apple would certainly not appreciate it.
First, explore new transitioning system in iOS7: http://www.doubleencore.com/2013/09/ios-7-custom-transitions/
Then, replace:
[self presentViewController:navigationController animated:YES completion:nil];
with
if([UIApplication iOS7]) /* or any other custom iOS7 detection method you implement */
{ /* we simulate old transition with nav bar slided out */
navigationController.transitioningDelegate = [OMModalTransitionDelegate new];
}
[self presentViewController:navigationController animated:YES completion:nil];
So, we need a transition delegate to simulate standard behaviour and do the trick as well.
#import "OMModalTransitionDelegate.h"
#import "OMAnimatedTransitioning.h"
#implementation OMModalTransitionDelegate
- (id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source
{
OMAnimatedTransitioning *transitioning = [OMAnimatedTransitioning new];
return transitioning;
}
- (id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed
{
OMAnimatedTransitioning *transitioning = [OMAnimatedTransitioning new];
transitioning.reverse = YES;
return transitioning;
}
#end
And now the actual animation manager (you have to implement sharedBar in a category on UINavigationBar yourself):
static NSTimeInterval const DEAnimatedTransitionDuration = 0.4f;
static NSTimeInterval const DEAnimatedTransitionMarcoDuration = 0.15f;
#implementation OMAnimatedTransitioning
- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
UIViewController *fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
UIView *container = [transitionContext containerView];
UIView *superView = [UINavigationBar sharedBar].superview;
CGRect barFrame = [UINavigationBar sharedBar].frame;
if(self.reverse)
{ /* Trick - first, remove the bar from it's superview before animation starts */
[[UINavigationBar sharedBar] removeFromSuperview];
}
CGRect oldFrame = container.bounds;
if (self.reverse)
{
[container insertSubview:toViewController.view belowSubview:fromViewController.view];
}
else
{
toViewController.view.frame = oldFrame;
toViewController.view.transform = CGAffineTransformMakeTranslation(0, CGRectGetHeight(oldFrame));
[container addSubview:toViewController.view];
}
[UIView animateKeyframesWithDuration:DEAnimatedTransitionDuration delay:0 options:0 animations:^
{
if (self.reverse)
{
fromViewController.view.transform = CGAffineTransformMakeTranslation(0, CGRectGetHeight(oldFrame));
double delayInSeconds = 0.01; /* Trick - after an imperceivable delay - add it back - now it is immune to whatever Apple put there to move it */
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^(void)
{
[UINavigationBar sharedBar].frame = barFrame;
[superView addSubview:[UINavigationBar sharedBar]];
});
}
else
{
toViewController.view.transform = CGAffineTransformIdentity;
}
} completion:^(BOOL finished) {
[transitionContext completeTransition:finished];
}];
}
- (NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext
{
return DEAnimatedTransitionDuration;
}
#end
In your custom navigation controller, add
- (void)viewWillLayoutSubviews {
//do your navigation bar layout
}
hope this can help you. Remind, above method only be supported ios >= 5.0.
I'm using ECSliding and I have this problem!
In my project there are this files:
InitViewController (ECSlidingController)
FirstViewController (UIViewController)
SecondViewController (UIViewController)
LeftMenuViewController (UIViewController)
I use InitView to instatiate my FirstView as the topview.
In my FirstView there is a button, when pressed sets SecondView as topview.
Is it possible to animate the changing topview like I'm opening a new view?
or how can I open a new view that uses ECSliding like the first one?
I'm using this code to change topview:
self.slidingViewController.topViewController = second;
[self.slidingViewController resetTopView];
The animation that I want could just be the default one, like:
[self presentViewController:(*UIViewController) animated:YES completion:nil];
Subclass ECSlidingViewController. I call my subclass DeckController:
In DeckController.h:
- (void)setTopViewController:(UIViewController *)topViewController withTransition:(CATransition*)transition;
In DeckController.m:
- (void)setTopViewController:(UIViewController *)topViewController withTransition:(CATransition*)transition {
self.topViewController = topViewController;
[UIView animateWithDuration:0.25f animations:^() {
[[self.view layer] addAnimation:transition forKey:#"topViewTransition"];
}];
}
Then in the view controller that wants to switch to the new top view, do something like this:
CATransition *animation = [CATransition animation];
[animation setType:kCATransitionPush];
[animation setSubtype:kCATransitionFromRight];
[animation setTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]];
[animation setFillMode:kCAFillModeBoth];
[animation setDuration:.25];
DeckController *deck = (DeckController*)self.slidingViewController;
[deck setTopViewController:newTopViewController withTransition:animation];
Probably your best option is to use the animation methods of the sliding view controller. Animate the top view off screen, in the completion block change the top view controller (to second) then start a new animation to animate the top view back onto screen.
[self.slidingViewController anchorTopViewOffScreenTo:...
animations:...
onComplete:^{
self.slidingViewController.topViewController = second;
[self.slidingViewController resetTopView];
}];
I just use same operation as when Im clicking on a menuitem in my left menu.
[self.anchorTopViewOffScreenTo:ECRight animations:nil onComplete:^{
CGRect frame = self.topViewController.view.frame;
self.topViewController = [self.storyboard instantiateViewControllerWithIdentifier:#"secondVC"];
self.topViewController.view.frame = frame;
[self resetTopView];
}];
Just generalize this, and you can call it everywhere. It does the animation by itself.
You can do as #Alex, but if you don't have to do some custom animation, this is all you need.
NB. in my example Im using this in the initViewController (for which is the ECSlidingViewController, therefore i'm sending to self), for forcing user to a specific viewController. If you need to change view in any subViewControllers you just sending to self.slidingViewController like the example below:
[self.slidingViewController anchorTopViewOffScreenTo:ECRight animations:nil onComplete:^{
CGRect frame = self.slidingViewController.topViewController.view.frame;
self.slidingViewController.topViewController = [self.storyboard instantiateViewControllerWithIdentifier:#"secondVC"];
self.slidingViewController.topViewController.view.frame = frame;
[self.slidingViewController resetTopView];
}];
You can manually create the animation then set the topViewController at the end so that the sliding view controller is in the state that you expect.
SecondTopViewController *secondTopViewController = [self.storyboard instantiateViewControllerWithIdentifier:#"SecondTop"];
secondTopViewController.view.frame = self.slidingViewController.view.bounds;
secondTopViewController.view.alpha = 0;
// temporarily add the second top view so that we can animate it in.
[self.slidingViewController.view addSubview:secondTopViewController.view];
[UIView animateWithDuration:0.5 animations:^{
secondTopViewController.view.alpha = 1;
} completion:^(BOOL finished) {
// remove the second top view.
[secondTopViewController.view removeFromSuperview];
// let sliding view controlller take over from here
self.slidingViewController.topViewController = secondTopViewController;
}];
See my working example here: https://github.com/enriquez/ECSlidingViewController/tree/so-16658539
#Michael-Enriquez's answer converted to swift.
let viewController = (self.storyboard?.instantiateViewControllerWithIdentifier("ViewController"))! as? ViewController
let navController = UINavigationController(rootViewController: viewController!)
navController.view.frame = self.slidingViewController().view.bounds
navController.view.alpha = 0
self.slidingViewController().view.addSubview(navController.view)
UIView.animateWithDuration(0.25, animations: {
navController.view.alpha = 1
}, completion: {(finished:Bool) -> Void in
navController.view.removeFromSuperview()
self.slidingViewController().topViewController = navController
})
In order to use a UISplitViewController, I'm replacing my window root controller when navigating from one view controller to the other.
In order to have some nice transition while doing so, I'm using a zooming effect like this:
MyOtherViewController *controller = [[MyOtherViewController alloc] initWithNibName:#"MyOtherView" bundle:nil];
UIWindow *window = ((MyAppDelegate *)[[UIApplication sharedApplication] delegate]).window;
controller.view.frame = [window frame];
controller.view.transform = CGAffineTransformMakeScale(0.01,0.01);
controller.view.alpha = 0;
[window addSubview:controller.view];
[UIView animateWithDuration:0.2 animations:^{
controller.view.transform = CGAffineTransformMakeScale(1,1);
controller.view.alpha = 1.0;
} completion:^(BOOL finished) {
if (finished) {
[self.view removeFromSuperview];
window.rootViewController = controller;
}
}];
and this works pretty well, except that while doing the animation, the new view is always oriented as if in portrait mode, regardless of the current device orientation. When the animation is finished, the view orients itself correctly.
What am I missing?
Things I've tried:
putting my new controller view as the sole subview of the UIWindow
making my new controller the root view controller before the animation begins
A curious thing is that, if I do a recursiveDescription on the window at the beginning of my method, the window frame is defined as having a size of 768x1024 (i.e., portrait), and the view inside it of 748x1024 but with a transform of [0, -1, 1, 0, 0, 0] (does this mean a rotation or what? Shouldn't it be the identity transform?)
UIWindow doesn't rotate. It has a rotated view inside of it (as you've seen). In this case, though, I think the problem is likely that your view has a transform on it already at this point, and you need to concatenate with it rather than replace it as you're doing in your setTransform: calls.
You shouldn't be asking the app delegate for the window, you should be getting the window from the view (self.view.window).
If at any point you're attaching your view to the window itself, rather than putting it inside the rotation view, then you'll need to know the effective transform of the view you want to match by walking the hierarchy:
- (CGAffineTransform)effectiveTransform {
CGAffineTransform transform = [self transform];
UIView *view = [self superview];
while (view) {
transform = CGAffineTransformConcat(transform, [view transform]);
view = [view superview];
}
return transform;
}
I finally figured out what was wrong. Since the frame is not a real property but a sort of calculated one, based on the values of the view bounds and the view transform, I needed to set the frame after setting the same transform as the current view, and before setting the transform again to set up the initial state of the animation. Also, the frame I need to set is the same one as the current view is currently using, as it is taking into account the window orientation (or lack thereof, as Rob Napier pointed)
So, without further ado, here's the working code:
MyOtherViewController *controller = [[MyOtherViewController alloc] initWithNibName:#"MyOtherView" bundle:nil];
UIWindow *window = [[UIApplication sharedApplication] keyWindow];
CGAffineTransform t = self.view.transform;
controller.view.transform = t;
controller.view.frame = self.view.frame;
controller.view.transform = CGAffineTransformScale(t,.01,.01);;
[window addSubview:controller.view];
controller.view.alpha = 0;
[UIView animateWithDuration:0.2 animations:^{
controller.view.transform = t;
controller.view.alpha = 1.0;
} completion:^(BOOL finished) {
if (finished) {
[self.view removeFromSuperview];
window.rootViewController = controller;
[controller release];
}
}];