I have an extension for performing transitions between view controllers. It will perform an animation with the views of each VC and then replace the current vc with the destination vc using this method:
private func replace(with: UIViewController, completion: (() -> Void)?) {
if let baseWindow = self.view.window, baseWindow.rootViewController == self {
baseWindow.rootViewController = with
}
}
Everything works fine except in the following scenario:
- VC1 calls transition.to(NC1, animation: .fade) // NC1 is a navigation controller with VC2 as its primary view controller
- VC2 calls self.navigationController?.pushViewController(VC3, animated: true)
- VC3 will ignore the safe area when presenting and it will be pushed without an animation. This is the issue that I am unable to solve.
Using animation: .none works, so it must be something to do with my transition code, which I will share below:
extension UIViewController {
enum HorizontalDirection { case left, right }
enum TransitionAnimation {
case slide(_ direction: HorizontalDirection)
case pageIn(_ direction: HorizontalDirection)
case pageOut(_ direction: HorizontalDirection)
case zoomOut
case zoomIn
case fade
case none
}
private func replace(with: UIViewController, completion: (() -> Void)?) {
if let baseWindow = self.view.window, baseWindow.rootViewController == self {
baseWindow.rootViewController = with
}
}
// Note that these transitions will not work inside a macOS modal
func transition(to: UIViewController, animation: TransitionAnimation, completion: (() -> Void)? = nil) {
// initialSpringVelocity (default 0) determines how quickly the view moves during the first part of the animation, before the spring starts to slow it down.
// A higher value for initialSpringVelocity means that the view will move more quickly at the beginning of the animation, while a lower value means that it will start more slowly. The velocity value is measured in points per second.
// The usingSpringWithDamping (default 0.5) parameter determines how quickly the view slows down during each oscillation. A smaller value for usingSpringWithDamping creates a bouncier effect with more oscillations, while a larger value creates a more damped effect with fewer oscillations.
switch animation {
case .none: replace(with: to, completion: completion)
case .fade:
to.view.alpha = 0
self.view.insertSubview(to.view, aboveSubview: self.view)
UIView.animate(withDuration: 0.3, delay: 0.0, options: [.curveEaseOut], animations: {
to.view.alpha = 1
}, completion: { [self] _ in replace(with: to, completion: completion) })
case .slide(.left):
let width = self.view.frame.size.width
self.view.superview?.insertSubview(to.view, aboveSubview: self.view)
to.view.transform = CGAffineTransform(translationX: width, y: 0)
UIView.animate(withDuration: 0.7, delay: 0.0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0.2, options: [.curveEaseInOut], animations: { [self] in
self.view.transform = CGAffineTransform(translationX: -width, y: 0)
self.view.subviews.forEach({ $0.alpha = 0 })
to.view.transform = .identity
}, completion: { [self] _ in replace(with: to, completion: completion) })
case .slide(.right):
let width = self.view.frame.size.width
self.view.superview?.insertSubview(to.view, aboveSubview: self.view)
to.view.transform = CGAffineTransform(translationX: -width, y: 0)
UIView.animate(withDuration: 0.7, delay: 0.0, usingSpringWithDamping: 0.8, initialSpringVelocity: 0.2, options: [.curveEaseInOut], animations: { [self] in
self.view.transform = CGAffineTransform(translationX: width, y: 0)
self.view.subviews.forEach({ $0.alpha = 0 })
to.view.transform = .identity
}, completion: { [self] _ in replace(with: to, completion: completion) })
case .pageIn(.left):
let width = self.view.frame.size.width
self.view.superview?.insertSubview(to.view, aboveSubview: self.view)
to.view.transform = CGAffineTransform(translationX: width, y: 0)
UIView.animate(withDuration: 0.3, delay: 0.0, options: [.curveEaseInOut], animations: { [self] in
self.view.transform = CGAffineTransform(scaleX: 0.8, y: 0.8)
self.view.alpha = 0
to.view.transform = .identity
}, completion: { [self] _ in replace(with: to, completion: completion) })
case .pageIn(.right):
let width = self.view.frame.size.width
self.view.superview?.insertSubview(to.view, aboveSubview: self.view)
to.view.transform = CGAffineTransform(translationX: -width, y: 0)
UIView.animate(withDuration: 0.3, delay: 0.0, options: [.curveEaseInOut], animations: { [self] in
self.view.transform = CGAffineTransform(scaleX: 0.8, y: 0.8)
self.view.alpha = 0
to.view.transform = .identity
self.view.layoutIfNeeded() // This is here to prevent the safe area from being impacted by the scaling animation
}, completion: { [self] _ in replace(with: to, completion: completion) })
case .pageOut(.left):
let width = self.view.frame.size.width
self.view.superview?.insertSubview(to.view, belowSubview: self.view)
to.view.transform = CGAffineTransform(scaleX: 0.8, y: 0.8)
to.view.alpha = 0
UIView.animate(withDuration: 0.3, delay: 0.0, options: [.curveEaseInOut], animations: { [self] in
self.view.transform = CGAffineTransform(translationX: width, y: 0)
to.view.transform = .identity
to.view.alpha = 1
self.view.layoutIfNeeded() // This is here to prevent the safe area from being impacted by the scaling animation
}, completion: { [self] _ in replace(with: to, completion: completion) })
case .pageOut(.right):
let width = self.view.frame.size.width
self.view.superview?.insertSubview(to.view, belowSubview: self.view)
to.view.transform = CGAffineTransform(scaleX: 0.8, y: 0.8)
to.view.alpha = 0
UIView.animate(withDuration: 0.3, delay: 0.0, options: [.curveEaseInOut], animations: { [self] in
self.view.transform = CGAffineTransform(translationX: -width, y: 0)
to.view.transform = .identity
to.view.alpha = 1
self.view.layoutIfNeeded() // This is here to prevent the safe area from being impacted by the scaling animation
}, completion: { [self] _ in replace(with: to, completion: completion) })
case .zoomOut:
self.view.superview?.insertSubview(to.view, aboveSubview: self.view)
to.view.transform = CGAffineTransform(scaleX: 1.3, y: 1.3)
to.view.alpha = 0
// This backgroundView is added behind the 2 views transitioning because in certain scenarios, there are pages still visible behind them and the zoom animation reveals them.
let backgroundView = UIView(frame: self.view.frame)
backgroundView.backgroundColor = self.view.backgroundColor
self.view.superview?.insertSubview(backgroundView, belowSubview: self.view)
UIView.animate(withDuration: 0.2, delay: 0.0, options: [.curveEaseInOut], animations: { to.view.alpha = 1 }, completion: nil)
UIView.animate(withDuration: 0.3, delay: 0.0, options: [.curveEaseInOut], animations: { [self] in
self.view.transform = CGAffineTransform(scaleX: 0.7, y: 0.7)
to.view.transform = .identity
self.view.layoutIfNeeded() // This is here to prevent the safe area from being impacted by the scaling animation
}, completion: { [self] _ in replace(with: to, completion: completion) })
case .zoomIn:
self.view.superview?.insertSubview(to.view, aboveSubview: self.view)
to.view.transform = CGAffineTransform(scaleX: 0.7, y: 0.7)
to.view.alpha = 0
// This backgroundView is added behind the 2 views transitioning because in certain scenarios, there are pages still visible behind them and the zoom animation reveals them.
let backgroundView = UIView(frame: self.view.frame)
backgroundView.backgroundColor = self.view.backgroundColor
self.view.superview?.insertSubview(backgroundView, belowSubview: self.view)
UIView.animate(withDuration: 0.15, delay: 0.0, options: [.curveEaseInOut], animations: { [self] in view.alpha = 0 }, completion: nil)
UIView.animate(withDuration: 0.3, delay: 0.0, options: [.curveEaseInOut], animations: { [self] in
view.transform = CGAffineTransform(scaleX: 1.3, y: 1.3)
to.view.transform = .identity
to.view.alpha = 1
view.layoutIfNeeded() // This is here to prevent the safe area from being impacted by the scaling animation
}, completion: { [self] _ in replace(with: to, completion: completion) })
}
}
}
I have also created a demo Xcode project demonstrating the issue if anyone is this helps: https://drive.google.com/file/d/1aX3sQCCcp56wqRS5kH0wG8VzPpE5XIny/view?usp=share_link
Any hints about what causes this issue and how to fix it will be greatly appreciated.
You're manipulating view hierarchy in ways that cause problems - as we see from the results.
Notice that running your test project as-is, we get this in the debug console:
What your code is doing is pulling the navigation controller's view out of the view hierarchy, then trying to add that controller.
You can try this...
IF the "to" controller is a
UINavigationController,.viewControllers.firstcontrollerafter adding and showing the "to" view,
UINavigationController.firstVCreplace()with the New nav controllerWith only quick testing, this appears to solve the issue:
Notes:
.fadecase...It may get you headed in the right direction.