I am trying to use UIViewController's transitionFromViewController:toViewController:duration method but with a custom animation.
I have the following two view controllers added as children to a custom container UIViewController:
firstController - This is an instance of UITabBarController
secondController - This is a subclass of UIViewController
The following code works as expected:
[self transitionFromViewController:firstController
toViewController:secondController
duration:2
options:UIViewAnimationOptionTransitionFlipFromLeft
animations:^(void){}
completion:^(BOOL finished){}];
However I would like to create a custom animation where the where firstController slides to the left and is replaced by secondController sliding in from the right similar to how UINavigationControllers push and pop methods work. After changing the options to UIViewAnimationOptionTransitionNone I have tried to implement custom animations in the animations block but have had absolutely no success. firstController is immediately swapped for secondController without and animations.
I would really appreciate any help.
Thank you
This is actually really easy. For some reason I assumed that the secondController's view would be be under/ behind that of firstController's. I had only tried to animate the firstController's view. This of course is wrong. As soon as transitionFromViewController:toViewController:duration is called secondController's view is placed over firstController's view. The following code works:
CGFloat width = self.view.frame.size.width;
CGFloat height = self.view.frame.size.height;
secondController.view.frame = CGRectMake(width, 0, width, height);
[self transitionFromViewController:firstController
toViewController:secondController
duration:0.4
options:UIViewAnimationOptionTransitionNone
animations:^(void) {
firstController.view.frame = CGRectMake(0 - width, 0, width, height);
secondController.view.frame = CGRectMake(0, 0, width, height);
}
completion:^(BOOL finished){
[secondController didMoveToParentViewController:self];
}
];
Related
I've created a RootViewController / RootView that:
Handles the content layout for the app
Exposes and interface for performing application level behaviors, like presenting the "hamburger" menu or overlay views with CAKeyframe animations.
This is in accordance with good practice.
The Problem:
When the main content view presents a form, there's a utility to animate the frame of that view, when a field is selected that would otherwise be obscured by the keyboard. This has been working fine all the way up until iOS 8.0.2
On iOS 8.0.2 the frame for the form will no longer animate if you set a negative value for origin.y. Instead of going from the current origin.y to the required origin.y it jerks down by the amount it was supposed to move, then animates back to 0.
If I present the form outside of the RootVC it works correctly.
What I've tried:
Checked that RootView is not doing anything in layout subviews to prevent the animation. (In iOS8.0 it was. I removed this and problem was solved. Only to return in iOS8.0.2)
Checked the BeginFromCurrentState flags.
Instead of animating the form view, animate [UIScreen mainScreen].keyWindow. Works but causes some other side effects that I don't want.
Question:
What has changed with animation of UIViewControllers that are contained in another view in iOS8.0.2. It seems to be something very fundamental.
The code that animates frame to move input fields out the keyboard's way:
Looks something like this:
- (void)scrollToAccommodateField:(UIView *)view
{
UIView *rootView = [UIApplication sharedApplication].keyWindow.rootViewController.view;
CGPoint position = [view convertPoint:view.bounds.origin toView:rootView];
CGFloat y = position.y;
CGFloat scrollAmount = 0;
CGFloat margin = 25;
CGSize screenSize = [self screenSizeWithOrientation:[UIApplication sharedApplication].statusBarOrientation];
CGSize accessorySize = CGSizeMake(_view.width, 44);
CGFloat maxVisibleY = screenSize.height - [self keyboardSize].height - accessorySize.height - margin;
if (y > maxVisibleY)
{
scrollAmount = maxVisibleY - y;
CGFloat scrollDelta = scrollAmount - _currentScrollAmount;
_currentScrollAmount = scrollAmount;
[self scrollByAmount:scrollDelta];
}
else
{
if (_currentScrollAmount != 0)
{
_currentScrollAmount = 0;
[UIView transitionWithView:_view duration:0.30
options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionCurveEaseOut animations:^
{
_view.frame = [_view bounds];
} completion:nil];
}
}
}
Update:
I've since installed TPKeyboardAvoiding pod and its working very well. . leaving this open, in case its of interest to others.
I have an iPad app that is displaying a "note" (subclassed UILabel) with text on it. In order to navigate to the next note, I'd like it to slide it off the screen to the left while having the next one slide in from the right. I can get either animation to work, but not both at the same time.
Here's the code in my controller:
- (void)slideOutLeft {
// create the new note
flSlidingNote *newNote = [[flSlidingNote alloc] init];
newNote.text = #"blah blah";
CGRect newFrame = CGRectMake(1000, 70, 637, 297); // off the right
newNote.frame = newFrame;
[self.view addSubview:newNote];
// slide off the current one
CGRect currentFrameEnd = noteLabel.frame; // noteLabel is the existing note
currentFrameEnd.origin.x = 0 - noteLabel.frame.size.width; // off the left
[UIView animateWithDuration:1.0 animations:^{
noteLabel.frame = currentFrameEnd;
} completion:nil];
}
noteLabel does not animate at all. If I comment out the addSubview:newNote part it does. I'm still relatively new at this, so it's probably just something simple.
The problem happens whether newNote is animated or not (not animated in the code snippet).
You want to put the animation for both your views and the addSubview call for your new view in one animation block, like so:
// layout the new label like the old, but 300px offscreen right
UILabel * newNote = [[UILabel alloc] initWithFrame:CGRectOffset(self.noteLabelView.frame, 300, 0)];
newNote.text = #"NEW NOTE";
[UIView animateWithDuration:2.0f
delay:0
options:UIViewAnimationCurveEaseInOut
animations:^{
// animate the old label left 300px to offscreen left
self.noteLabelView.center = CGPointMake(self.noteLabelView.center.x-300, self.noteLabelView.center.y);
// add the new label to the view hierarchy
[self.view addSubview:newNote];
// animate the new label left 300px into the old one's spot
newNote.center = CGPointMake(newNote.center.x-300, newNote.center.y);
}
completion:^(BOOL finished) {
// remove the old label from the view hierarchy
[self.noteLabelView removeFromSuperview];
// set the property to point to the new label
self.noteLabelView = newNote;
}];
In this snippet above, I'm assuming the old label is addressable via the property self.noteLabelView.
(Also have a look at https://github.com/algal/SlidingNotes , though I can't promise that will stay there long so maybe SO isn't the right format for such a link? )
Adding to what Gabriele Petronella commented, here is a good resource on KeyFrames and AnimationGroup, with a sample GitHub project linked:
http://blog.corywiles.com/using-caanimationgroup-for-view-animations
This really helped me understand animations better.
not sure but i've seen other code that loops through subviews and performs animations, you may want to try the following:
for(UIView *view in self.subviews)
{
// perform animations
}
I have a custom segue where I am trying to do the reverse of the standard "Cover Vertical" segue transition. I think this should work:
UIView *srcView = ((UIViewController *)self.sourceViewController).view;
UIView *dstView = ((UIViewController *)self.destinationViewController).view;
[dstView setFrame:CGRectMake(0, 0, srcView.window.bounds.size.width, srcView.window.bounds.size.height)];
[srcView.window insertSubview:dstView belowSubview:srcView];
[UIView animateWithDuration:0.3
animations:^{
srcView.center = CGPointMake(srcView.center.x - srcView.frame.size.width, srcView.center.y);
}
completion:^(BOOL finished){
[srcView removeFromSuperview];
}
];
The problem is the destination view shows up in portrait orientation even though every other view in the app is landscape orientation. Also, the x and y coordinates are reversed. Why is this happening?
I have set up shouldAutorotateToInterfaceOrientation in every view controller, but that didn't help. I could rotate the view with CGAffineTransformMakeRotation, but that doesn't seem like the right thing to do; coordinates would still be reversed, for one thing.
Update:
I tried using CGAffineTransformRotate to rotate the view , but it looks ridiculous. Most of the background shows up black. I don't think this is the way it's suppose to work.
Since I was not able the get the correct orientation and framing of the destination view before the controllers exchange, I did reverse the process:
save the source view as an image (see Remove custom UIStoryboardSegue with custom animation ) ;
switch the current controller [presentViewController: ...];
then, do the animation with the (now correct) destination view embedding the saved source image.
Here is the complete code :
#include <QuartzCore/QuartzCore.h>
#implementation HorizontalSwipingSegue
- (void) perform {
UIViewController *source = self.sourceViewController;
UIViewController *destination = self.destinationViewController;
UIView *sourceView = source.view;
UIView *destinationView = destination.view;
// Create a UIImageView with the contents of the source
UIGraphicsBeginImageContext(sourceView.bounds.size);
[sourceView.layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage *sourceImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
UIImageView *sourceImageView = [[UIImageView alloc] initWithImage:sourceImage];
[[self sourceViewController] presentViewController:[self destinationViewController] animated:NO completion:NULL];
CGPoint destinationViewFinalCenter = CGPointMake(destinationView.center.x, destinationView.center.y);
CGFloat deltaX = destinationView.frame.size.width;
CGFloat deltaY = destinationView.frame.size.height;
switch (((UIViewController *)self.sourceViewController).interfaceOrientation) {
case UIDeviceOrientationPortrait:
destinationView.center = CGPointMake(destinationView.center.x - deltaX, destinationView.center.y);
sourceImageView.center = CGPointMake(sourceImageView.center.x + deltaX, sourceImageView.center.y);
break;
case UIDeviceOrientationPortraitUpsideDown:
destinationView.center = CGPointMake(destinationView.center.x + deltaX, destinationView.center.y);
sourceImageView.center = CGPointMake(sourceImageView.center.x + deltaX, sourceImageView.center.y);
break;
case UIDeviceOrientationLandscapeLeft:
destinationView.center = CGPointMake(destinationView.center.x, destinationView.center.y - deltaY);
sourceImageView.center = CGPointMake(sourceImageView.center.x + deltaY, sourceImageView.center.y);
break;
case UIDeviceOrientationLandscapeRight:
destinationView.center = CGPointMake(destinationView.center.x, destinationView.center.y + deltaY);
sourceImageView.center = CGPointMake(sourceImageView.center.x + deltaY, sourceImageView.center.y);
break;
}
[destinationView addSubview:sourceImageView];
[UIView animateWithDuration:0.6f
animations:^{
destinationView.center = destinationViewFinalCenter;
}
completion:^(BOOL finished){
[sourceImageView removeFromSuperview];
}];
}
#end
UPDATED ANSWER:
Make sure you are setting your dstView's frame correctly. I think you'll have better luck using:
dstView.frame = srcView.frame;
instead of:
[dstView setFrame:CGRectMake(0, 0, srcView.window.bounds.size.width, srcView.window.bounds.size.height)];
the bounds property does not react to changes in orientation the same way frame does.
Apparently, it was much easier than I thought to do the segue I wanted. Here's is what worked:
- (void)perform {
UIViewController *src = (UIViewController *)self.sourceViewController;
[src dismissViewControllerAnimated:YES completion:^(void){ [src.view removeFromSuperview]; }];
}
Hopefully this helps someone else.
Replace
[srcView insertSubview:dstView belowSubview:srcView];
with
[srcView insertSubview:dstView atIndex:0];
If you are just adding another view controller's view to your view hierarchy, that view controller is not going to get any information about the orientation of the device, so it will present the view in its default configuration. You need to add the view controller into the view controller hierarchy as well, using the containment methods described here. You also need to remove it from the hierarchy when done.
There are also several good WWDC talks on the subject, one from 2011 called "Implementing view controller containment" and one from this year, the evolution of view controllers.
I have a main view that is supposed to display several subviews. Those subviews are directly above and below one another (in the z axis) and will drop down (in the y axis) and move up using this code:
if (!CGRectIsNull(rectIntersection)) {
CGRect newFrame = CGRectOffset (rectIntersection, 0, -2);
[backgroundView setFrame:newFrame];
} else{
[viewsUpdater invalidate];
viewsUpdater = nil;
}
rectIntersection is used to tell when the view has entirely moved down and is no longer behind the front one (when they no longer overlap rectIntersection is null), it moves down by 2 pixels at a time because this is all inside a repeating timer. I want my main view, the one that contains these two other views, to resize downward so that it expands just as the view in the background is being lowered. This is the code I'm trying for that:
CGRect mainViewFrame = [mainView frame];
if (!CGRectContainsRect(mainViewFrame, backgroundFrame)) {
CGRect newMainViewFrame = CGRectMake(0,
0,
mainViewFrame.size.width,
(mainViewFrame.size.height + 2));
[mainView setFrame:newMainViewFrame];
}
The idea is to check if the mainView contains this background view. When the backgroundView is lowered, the main view no longer contains it, and it (should) expand downward by 2 pixels. This would happen until the background view stopped moving and the mainView finally contains backgroundView.
The problem is that the mainView is not resizing at all. the background view is being lowered, and I can see it until it disappears off the bottom of the mainView. mainView should have resized but it does not change in any direction. I tried using setFrame and setBounds (with and without setNeedsDisplay) but nothing worked.
I'm really just looking for a way to programmatically change the size of the main view.
I think I understood, what the problem is. I read carefuly the code.
if (!CGRectIsNull(rectIntersection)) {
// here you set the wrong frame
//CGRect newFrame = CGRectOffset (rectIntersection, 0, -2);
CGRect newFrame = CGRectOffset (backgroundView.frame, 0, -2);
[backgroundView setFrame:newFrame];
} else{
[viewsUpdater invalidate];
viewsUpdater = nil;
}
rectIntersection is actually the intersection of the two views, that overlap, and as the backgroundView is moved downward, that rect's height decreases.
That way the mainView gets resized only one time.
To add on this, here is a simple solution using block syntax, to animate your views, this code would typically take place in your custom view controller.
// eventually a control action method, pass nil for direct call
-(void)performBackgroundViewAnimation:(id)sender {
// first, double the mainView's frame height
CGFrame newFrame = CGRectMake(mainView.frame.origin.x,
mainView.frame.origin.y,
mainView.frame.size.width,
mainView.frame.size.height*2);
// then get the backgroundView's destination rect
CGFrame newBVFrame = CGRectOffset(backgroundView.frame,
0,
-(backgroundView.frame.size.height));
// run the animation
[UIView animateWithDuration:1.0
animations:^{
mainView.frame = newFrame;
backgroundView.frame = newBVFrame;
}
];
}
In this example.
I'm animating the PhotoViewerViewController's frame when I animate out the tabBarController (for fullscreen effect). The PhotoViewer uses a uiscrollview to generate the same sort of effect Apple's photos app does. For whatever reason sometimes it animates along with my PhotoViewer frame, and sometimes it doesn't.
You can see in the first example it jumps when increasing the frame size, but animates nicely when decreasing the frame size (and restoring the tab bar).
However in this example when the photo is vertical it jumps in both directions.
In both cases if I zoom in on the photo at all with the scrollview, it animates correctly in both directions.
I know the evidence is there, but I can't put my finger on what's happening here.
Here's my animation block:
void (^updateProperties) (void) = ^ {
self.navigationController.navigationBar.alpha = hidden ? 0.0f : 1.0f;
self.navigationController.toolbar.alpha = hidden ? 0.0f : 1.0f;
if (self.tabBarController) {
int height = self.tabBarController.tabBar.bounds.size.height;
for(UIView *view in self.tabBarController.view.subviews)
{
int newHeight;
UIInterfaceOrientation orientation = [[UIApplication sharedApplication] statusBarOrientation];
if (UIInterfaceOrientationIsPortrait(orientation)) {
newHeight = hidden ? [[UIScreen mainScreen] bounds].size.height : [[UIScreen mainScreen] bounds].size.height - height;
} else {
newHeight = hidden ? [[UIScreen mainScreen] bounds].size.width : [[UIScreen mainScreen] bounds].size.width - height;
}
if([view isKindOfClass:[UITabBar class]])
{
[view setFrame:CGRectMake(view.frame.origin.x, newHeight, view.frame.size.width, view.frame.size.height)];
}
else
{
CGRect newFrame = CGRectMake(view.frame.origin.x, view.frame.origin.y, view.frame.size.width, newHeight);
[view setFrame:newFrame];
// update our VC frame with animation
[self.view setFrame:newFrame];
}
}
}
};
Code adapted from a post on SO: UITabBar wont hide
Full source on github.
In general, if animations sometimes work and sometimes don't, it's because in the latter case a delayed perform is causing them to be effectively cancelled by setting the property concerned to its final value.
In the particular case of UIScrollView it often happens that -layoutSubviews is called without animation one run loop after your animation blocks have closed. For this reason I usually try to avoid doing any layout in that method where I have a UIScrollView in the view hierarchy (because particularly prior to iOS 5, UIScrollView is really trigger-happy on setting itself to need layout).
I would recommend removing your call to -[EGOPhotoImageView layoutScrollViewAnimated:] from -layoutSubviews and add it to an overridden -setFrame: instead.
If you can target iOS4 and above, take a look at UIViewAnimationOptionLayoutSubviews too. Using a block-based animation with this as an option might just work without changing anything else.
I tried your github code on iPad 4.2 and 4.3 simulators and it works fine... no image resizing whatsoever! There might be some version issues.
Also, I tried changing your animations block and the following serves the same purpose:
void (^updateProperties) (void) = ^ {
self.navigationController.navigationBar.alpha = hidden ? 0.0f : 1.0f;
self.navigationController.toolbar.alpha = hidden ? 0.0f : 1.0f; }
Let me know if I'm missing something :)