I want to achieve smooth animation between views with a different UINavigationBar
background colors. Embedded views have the same background color as UINavigationBar
and I want to mimic push/pop transition animation like:
I've prepared custom transition:
class CustomTransition: NSObject, UIViewControllerAnimatedTransitioning {
private let duration: TimeInterval
private let isPresenting: Bool
init(duration: TimeInterval = 1.0, isPresenting: Bool) {
self.duration = duration
self.isPresenting = isPresenting
}
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return duration
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let container = transitionContext.containerView
guard
let toVC = transitionContext.viewController(forKey: .to),
let fromVC = transitionContext.viewController(forKey: .from),
let toView = transitionContext.view(forKey: .to),
let fromView = transitionContext.view(forKey: .from)
else {
return
}
let rightTranslation = CGAffineTransform(translationX: container.frame.width, y: 0)
let leftTranslation = CGAffineTransform(translationX: -container.frame.width, y: 0)
toView.transform = isPresenting ? rightTranslation : leftTranslation
container.addSubview(toView)
container.addSubview(fromView)
fromVC.navigationController?.navigationBar.backgroundColor = .clear
fromVC.navigationController?.navigationBar.setBackgroundImage(UIImage.fromColor(color: .clear), for: .default)
UIView.animate(
withDuration: self.duration,
animations: {
fromVC.view.transform = self.isPresenting ? leftTranslation :rightTranslation
toVC.view.transform = .identity
},
completion: { _ in
fromView.transform = .identity
toVC.navigationController?.navigationBar.setBackgroundImage(
UIImage.fromColor(color: self.isPresenting ? .yellow : .lightGray),
for: .default
)
transitionContext.completeTransition(true)
}
)
}
}
And returned it in the UINavigationControllerDelegate
method implementation:
func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationControllerOperation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return CustomTransition(isPresenting: operation == .push)
}
While push animation works pretty well pop doesn't.
Questions:
Here is the link to my test project on GitHub.
EDIT
Here is the gif presenting the full picture of discussed issue and the desired effect:
These components are always very difficult to customize. I think, Apple wants system components to look and behave equally in every app, because it allows to keep shared user experience around whole iOS environment.
Sometimes, it easier to implement your own components from scratch instead of trying to customize system ones. Customization often could be tricky because you do not know for sure how components are designed inside. As a result, you have to handle lots of edge cases and deal with unnecessary side effects.
Nevertheless, I believe I have a solution for your situation. I have forked your project and implemented behavior you had described.
You can find my implementation on GitHub. See animation-implementation
branch.
The root cause of pop animation does not work properly, is that UINavigationBar
has it's own internal animation logic. When UINavigationController's
stack changes, UINavigationController
tells UINavigationBar
to change UINavigationItems
. So, at first, we need to disable system animation for UINavigationItems
. It could be done by subclassing UINavigationBar
:
class CustomNavigationBar: UINavigationBar {
override func pushItem(_ item: UINavigationItem, animated: Bool) {
return super.pushItem(item, animated: false)
}
override func popItem(animated: Bool) -> UINavigationItem? {
return super.popItem(animated: false)
}
}
Then UINavigationController
should be initialized with CustomNavigationBar
:
let nc = UINavigationController(navigationBarClass: CustomNavigationBar.self, toolbarClass: nil)
Since there is requirement to keep animation smooth and synchronized between UINavigationBar
and presented UIViewController
, we need to create custom transition animation object for UINavigationController
and use CoreAnimation
with CATransaction
.
Your implementation of transition animator almost perfect, but from my point of view few details were missed. In the article Customizing the Transition Animations you can find more info. Also, please pay attention to methods comments in UIViewControllerContextTransitioning
protocol.
So, my version of push animation looks as follows:
func animatePush(_ transitionContext: UIViewControllerContextTransitioning) {
let container = transitionContext.containerView
guard let toVC = transitionContext.viewController(forKey: .to),
let toView = transitionContext.view(forKey: .to) else {
return
}
let toViewFinalFrame = transitionContext.finalFrame(for: toVC)
toView.frame = toViewFinalFrame
container.addSubview(toView)
let viewTransition = CABasicAnimation(keyPath: "transform")
viewTransition.duration = CFTimeInterval(self.duration)
viewTransition.fromValue = CATransform3DTranslate(toView.layer.transform, container.layer.bounds.width, 0, 0)
viewTransition.toValue = CATransform3DIdentity
CATransaction.begin()
CATransaction.setAnimationDuration(CFTimeInterval(self.duration))
CATransaction.setCompletionBlock = {
let cancelled = transitionContext.transitionWasCancelled
if cancelled {
toView.removeFromSuperview()
}
transitionContext.completeTransition(cancelled == false)
}
toView.layer.add(viewTransition, forKey: nil)
CATransaction.commit()
}
Pop animation implementation is almost the same. The only difference in CABasicAnimation
values of fromValue
and toValue
properties.
In order to animate UINavigationBar
we have to add CATransition
animation on UINavigationBar
layer:
let transition = CATransition()
transition.duration = CFTimeInterval(self.duration)
transition.type = kCATransitionPush
transition.subtype = self.isPresenting ? kCATransitionFromRight : kCATransitionFromLeft
toVC.navigationController?.navigationBar.layer.add(transition, forKey: nil)
The code above will animate whole UINavigationBar
. In order to animate only background of UINavigationBar
we need to retrieve background view from UINavigationBar
. And here is the trick: first subview of UINavigationBar
is _UIBarBackground
view (it could be explored using Xcode Debug View Hierarchy). Exact class is not important in our case, it is enough that it is successor of UIView
.
Finally we could add our animation transition on _UIBarBackground
's view layer direcly:
let backgroundView = toVC.navigationController?.navigationBar.subviews[0]
backgroundView?.layer.add(transition, forKey: nil)
I would like to note, that we are making prediction that first subview is a background view. View hierarchy could be changed in future, just keep this in mind.
It is important to add both animations in one CATransaction
, because in this case these animations will run simultaneously.
You could setup UINavigationBar
background color in viewWillAppear
method of every view controller.
Here is how final animation looks like:
I hope this helps.