I have two view controllers where I use a regular Show Segue (Right to Left) to go in one direction and a custom segue (Left to Right) to go in the other direction. I don't think I should do an unwind because neither VC is subordinate to other and using these segues means one navigation controller manages things.
In the upper left hand corner of both VCs I have a common BarButton containing a profile photo.
When using the regular Right to Left Segue, the profile photo on the bar button remains unchanged. This looks great as the rest of the screen moves in but the element common to both, the profile photo stays in place.
In the other direction (Left to Right), however, using the custom segue, the entire VC screen including the navigation bar swoops in and you basically see the profile photo come in from the left edge before resting in the normal left bar button position where the same photo was a moment earlier. This looks bad.
Is there any way to force a common element in the navigation bar to stay in place during a custom segue to better mimic the behavior of the system show segue?
Thanks in advance for any suggestions.
Here is my code for the custom segue:
#import "customSegue.h"
#import "QuartzCore/QuartzCore.h"
@implementation customSegue
-(void)perform {
UIViewController *destinationController = (UIViewController*)[self destinationViewController];
UIViewController *sourceViewController = (UIViewController*)[self sourceViewController];
CGFloat animationDuration = .40;
transition.duration = animationDuration;
transition.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionDefault];
transition.type = kCATransitionMoveIn;
transition.subtype = kCATransitionFromLeft;
[sourceViewController.navigationController.view.layer addAnimation:transition
forKey:kCATransition];
UIColor *previousWindowBackgroundColor = sourceViewController.view.window.backgroundColor;
sourceViewController.view.window.backgroundColor = destinationController.view.backgroundColor;
[sourceViewController.navigationController pushViewController:destinationController animated:NO];
// switch the window color back after the transition duration from above
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(animationDuration * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
// make sure we still have a handle on the destination controller
if (destinationController) {
destinationController.view.window.backgroundColor = previousWindowBackgroundColor;
}
});
}
@end
Hello again @user6631314
I don't think you're going to get what you want by applying using a CATransition and attaching it to the navigationController view's layer.
Instead I would recommend making the presentingViewController a delegate for UINavigationController and rolling your own logic for the LTR push (it will be a lot smoother with the navigation bar and you should no longer have to worry about that black bar during the transition that my previous response helped you resolve/workaround).
So to do this you'll need to set the presenting view controller (or some coordinator view controller) to be the UINavigationControllerDelegate and implement this method:
- (id<UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController
animationControllerForOperation:(UINavigationControllerOperation)operation
fromViewController:(UIViewController*)fromVC
toViewController:(UIViewController*)toVC
Within that method you'll want to check if the operation is a UINavigationControllerOperationPush
and if so return a subclass of NSObject that conforms to the UIViewControllerAnimatedTransitioning
protocol (otherwise return nil to allow all other navigation operations to be standard). Within that class will be where you handle the custom navigation controller push animation override.
The basic logic for the LTR push animation is that you want to start the toView off the screen to the left, then animate it in so that it's completely onscreen after your animation duration (0.4 from your code) -- therefore start the x position offset to the negative value of the view's width (so it's completely offscreen) then during the animation set the x position to 0 (or you could just += the view's width).
Here's the example of what the current custom segue implementation looks like (notice the navigation bar slides over as well, which is the issue you're posting about here):
And the transition by using the custom animation controller:
Here's the complete code:
#import "ViewController.h"
#import "LTRPushAnimator.h"
@interface ViewController () < UINavigationControllerDelegate>
@property (strong, nullable) UIView *profileView;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.navigationController.delegate = self;
self.navigationItem.hidesBackButton = YES;
self.profileView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 40, 40)];
UIImageView *profileImageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"Homer.jpg"]];
[profileImageView setFrame:CGRectMake(0, 0, 40, 40)];
profileImageView.layer.cornerRadius = 20.0f;
profileImageView.layer.masksToBounds = YES;
profileImageView.clipsToBounds = YES;
profileImageView.layer.borderColor = [UIColor blackColor].CGColor;
profileImageView.layer.borderWidth = 1.0f;
[self.profileView addSubview:profileImageView];
UIBarButtonItem *lbi = [[UIBarButtonItem alloc] initWithCustomView:self.profileView];
self.navigationItem.leftBarButtonItem = lbi;
// Do any additional setup after loading the view, typically from a nib.
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
}
- (IBAction)pushFakeViewController:(id)sender {
UIViewController *fakeViewController = [[UIViewController alloc] init];
fakeViewController.view.backgroundColor = [UIColor redColor];
UIBarButtonItem *lbi = [[UIBarButtonItem alloc] initWithCustomView:self.profileView];
fakeViewController.navigationItem.leftBarButtonItem = lbi;
fakeViewController.navigationItem.hidesBackButton = YES;
[self.navigationController pushViewController:fakeViewController animated:YES];
// this is just in here to pop back to the root view controller since we removed the back button, it can be removed obviously
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0f * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self.navigationController popViewControllerAnimated:YES];
});
}
- (id<UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController
animationControllerForOperation:(UINavigationControllerOperation)operation
fromViewController:(UIViewController*)fromVC
toViewController:(UIViewController*)toVC
{
if (operation == UINavigationControllerOperationPush) {
return [[LTRPushAnimator alloc] init];
}
// otherwise standard animation
return nil;
}
@end
LTRPushAnimator.h
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface LTRPushAnimator : NSObject <UIViewControllerAnimatedTransitioning>
@end
NS_ASSUME_NONNULL_END
LTRPushAnimator.m
#import "LTRPushAnimator.h"
@implementation LTRPushAnimator
- (NSTimeInterval)transitionDuration:(id <UIViewControllerContextTransitioning>)transitionContext
{
return 0.4;
}
- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
[self _animatePushByFrameWithContext:transitionContext];
}
- (void)_animatePushByFrameWithContext:(id<UIViewControllerContextTransitioning>)transitionContext {
UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
CGRect toVCFrame = toViewController.view.frame;
CGFloat viewWidth = toVCFrame.size.width;
toVCFrame.origin.x -= viewWidth;
[toViewController.view setFrame:toVCFrame];
[[transitionContext containerView] addSubview:toViewController.view];
[UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
CGRect finalVCFrame = toViewController.view.frame;
finalVCFrame.origin.x = 0;
[toViewController.view setFrame:finalVCFrame];
} completion:^(BOOL finished) {
[transitionContext completeTransition:![transitionContext transitionWasCancelled]];
}];
}
@end