I'm making a custom Transition Manager. It conforms to the protocols UIViewControllerAnimatedTransitioning & UIViewControllerTransitioningDelegate.
When presenting from RocketsVC to RocketDetailsVC, func is called animationController(for Presented presented: UIViewController, presenting: UIViewController, source: UIViewController), the following types are passed there:
presented: RocketDetailsViewController
presenting: MainTabBarController (Error is here)
source: RocketsViewController
This functions are declared in TransitionManager.swift:
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
type = .presentation
return self // TransitionManager
}
And then animateTransition method is called...
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let fromViewController = transitionContext.viewController(forKey: .from) as? TransitionManagerProtocol,
let toViewController = transitionContext.viewController(forKey: .to) as? TransitionManagerProtocol
...with zero effect, because MainTabBarController does not conform to TransitionManagerProtocol.
If I build a project without MainTabBarController (rootVC is RocketsVC), then everything works as it should.
What should I do to make the transition work? I'm sinning on MainTabBarController, but maybe there is a way to somehow pass to animationController method source instead of presenting?
Full code is in my GitHub
TransitionManager.swift
RocketsViewController.swift
MainTabBarController.swift
The solution I've found after long hours:
private var fromVC: UIViewController?
private var toVC: UIViewController?
...
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
type = .presentation
fromVC = source
toVC = presented
return self
}
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
type = .dismissal
toVC = fromVC // last `fromVC` is now `toVC`
fromVC = dismissed // and now we set this to fromVC
return self
}
And:
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let fromViewController = fromVC as? TransitionManagerProtocol,
let toViewController = toVC as? TransitionManagerProtocol
else {
transitionContext.completeTransition(false)
return
}
...
Related
I tried making transition animation for changing between two viewController. Is there any way to make that without using storyboard?
This code for calling new viewController (which I didn't use storyboard):
#objc func handleAccount() {
let navController = UINavigationController(rootViewController: userButtonLauncher())
navController.transitioningDelegate = self
navController.modalPresentationStyle = .custom
self.present(navController, animated: true, completion: nil)
}
And this codes for transition:
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
transition.transitionMode = .present
transition.startingPoint = CGPoint(x: 0, y: 0)
return transition
}
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
transition.transitionMode = .pop
transition.startingPoint = CGPoint(x: 0, y: 0)
return transition
}
Of course you can! It depends on what you would like to do, so if you need to use the defaults animation on code without using storyboard you can just:
instantiate your ViewController, set the animation you want and open the ViewController
let vc = self.storyboard?.instantiateViewController(withIdentifier: "vc_id")
vc.modalTransitionStyle = .flipHorizontal
self.present(vc, animated: true, completion: nil)
Otherwise you can create your custom transition, see this easy guide:
https://www.raywenderlich.com/322-custom-uiviewcontroller-transitions-getting-started
I solve my problem. I just add that code for calling animation transition in my homecontroller:
extension HomeController: UIViewControllerTransitioningDelegate {
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return faceanim()
}
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return faceanim()
}
}
And to button action is:
#objc func handleAccount() {
let userConVC = UINavigationController(rootViewController: userButtonLauncher())
userConVC.transitioningDelegate = self
navigationController?.isNavigationBarHidden = false
self.present(userConVC, animated: true, completion: nil)
}
I have a UITabBarController which has 9 view controllers and I am custom handling the navigation with a forward and back button. I have created a UIViewControllerAnimatedTransitioning object to handle the animation (simple left to right) and it gets returned in the delegate method:
tabBarController(_ tabBarController: UITabBarController, animationControllerForTransitionFrom fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning?
However, on and after selecting the fifth index, this function is no longer called (as it is now handled by the UIMoreNavigationController. Is there an animation delegate or some way I should be handling it for the UIMoreNavigationController instance?
You would need to specify the delegate of your moreNavigationController. So in your UITabBarController class you would need to:
self.moreNavigationController.delegate = strongDelegate // where strongDelegate is a local instance of MyControllerDelegate as defined below
The delegate should then implement the navigationController function which will call the applicable UIViewControllerAnimatedTransitioning instance:
class MyControllerDelegate: NSObject, UINavigationControllerDelegate {
func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationController.Operation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
switch operation {
case .push:
return MyPresentationAnimationController(isPresenting: true)
case .pop:
return MyPresentationAnimationController(isPresenting: false)
default:
return nil
}
}
}
Your animateTransition function contains the logic where the magic happens:
class MyPresentationAnimationController : NSObject, UIViewControllerAnimatedTransitioning {
private let isPresenting: Bool
init(isPresenting: Bool) {
self.isPresenting = isPresenting
}
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return TimeInterval(UINavigationController.hideShowBarDuration)
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard
let toView = transitionContext.view(forKey: .to),
let fromView = transitionContext.view(forKey: .from)
else {
return
}
let containerView = transitionContext.containerView
// TODO: Implement the logic with UIView.animateKeyframes here ...
}
}
Can somebody explain why my fromViewController is nil? I am trying to understand custom transition animations in iOS going back and forth between two different view controllers.
Removing the guard part of my code and modifying it a little bit, my present animation works, but I cannot figure out how to implement the dismiss animation. Any suggestions?
func animateTransition(using transitionContext: UIViewControllerContextTransitioning)
{
let containerView = transitionContext.containerView
guard
let fromViewController = transitionContext.view(forKey: .from),
let toViewController = transitionContext.view(forKey: .to)
else
{
print("Nope")
return
}
containerView.addSubview(toViewController)
if (presentationMode == .Present)
{
toViewController.alpha = 0.0
UIView.animate(withDuration: 1.0,
animations: {
toViewController.alpha = 1.0
},
completion: {
_ in transitionContext.completeTransition(true)
})
}
Edit: This is where I use the animation
override func prepare(for segue: UIStoryboardSegue, sender: Any?)
{
let secondVC = segue.destination as! SignUpViewController
secondVC.transitioningDelegate = self
}
extension LoginViewController: UIViewControllerTransitioningDelegate
{
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning?
{
print("Presenting")
presentationTransition.presentationMode = .Present
return presentationTransition
}
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning?
{
print("Dismissing")
presentationTransition.presentationMode = .Dismiss
return presentationTransition
}
}`
I have followed this tutorial and watched the WWDC video on this subject but I couldn't find my answer.
I have almost the same transition in my code. It is working pretty well when I am doing it as a presented view, but not as a pushed view.
It is supposed to animate a snapshot of the pushed view from a CGRect to the full screen and vice versa when popped.
Here is the code of my UIViewControllerAnimatedTransitioning class:
class ZoomingTransitionController: NSObject, UIViewControllerAnimatedTransitioning {
let originFrame: CGRect
let isDismissing: Bool
init(originFrame: CGRect, isDismissing: Bool) {
self.originFrame = originFrame
self.isDismissing = isDismissing
}
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return Constant.Animation.VeryShort
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let containerView = transitionContext.containerView
guard let fromVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from),
let toVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to) else {
return
}
let finalFrame = transitionContext.finalFrame(for: toVC)
toVC.view.frame = finalFrame
let snapshot = self.isDismissing ? fromVC.view.snapshotView(afterScreenUpdates: true) : toVC.view.snapshotView(afterScreenUpdates: true)
snapshot?.frame = self.isDismissing ? finalFrame : self.originFrame
snapshot?.layer.cornerRadius = Constant.FakeButton.CornerRadius
snapshot?.layer.masksToBounds = true
containerView.addSubview(toVC.view)
containerView.addSubview(snapshot!)
if self.isDismissing {
fromVC.view.isHidden = true
} else {
toVC.view.isHidden = true
}
let duration = transitionDuration(using: transitionContext)
UIView.animate(withDuration: duration,
animations: {
snapshot?.frame = self.isDismissing ? self.originFrame : finalFrame
},
completion: { _ in
if self.isDismissing {
fromVC.view.isHidden = false
} else {
toVC.view.isHidden = false
}
snapshot?.removeFromSuperview()
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
})
}
}
Then, I tried to show a new View Controller using 2 ways: by presenting it and by pushing it.
My FromViewController is subclassing both UINavigationControllerDelegate and UIViewControllerTransitioningDelegate.
FromViewController class presenting the ToViewController (which works fine):
func buttonAction(_ sender: AnyObject) {
self.tappedButtonFrame = sender.frame
let toVC = self.storyboard!.instantiateViewController(withIdentifier: "ToViewController")
toVC.transitioningDelegate = self
self.present(toVC, animated: true, completion: nil)
}
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
let transitionController = ZoomingTransitionController(originFrame: self.tappedButtonFrame, isDismissing: false)
return transitionController
}
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
let transitionController = ZoomingTransitionController(originFrame: self.tappedButtonFrame, isDismissing: true)
return transitionController
}
FromViewController class pushing the ToViewController (which doesn't work):
func buttonAction(_ sender: AnyObject) {
self.tappedButtonFrame = sender.frame
let toVC = self.storyboard!.instantiateViewController(withIdentifier: "ToViewController")
self.navigationController?.pushViewController(toVC, animated: true)
}
func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationControllerOperation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
switch operation {
case .push:
return ZoomingTransitionController(originFrame: self.tappedButtonFrame, isDismissing: false)
case .pop:
return ZoomingTransitionController(originFrame: self.tappedButtonFrame, isDismissing: true)
default:
return nil
}
}
When pushing, the delegate method is called and the ZoomingTransitionController performs its code fine (going in animateTransition until the end without notable issue). But on the screen, the snapshot view isn't display at any moment. The ToVC appears after the transition duration, but without anything else meanwhile.
I am running out of idea on how to debug this... Do you have any idea?
Thanks!!
I found my answer by replacing the snapshot element (which was causing the problem) by a CGAffineTransform of the toVC.
Code is almost the same than here.
I have a UINavigationController setup and am expecting to use a custom animation for pop / push of views with it. I have used custom transitions before without issue, but in this case I am actually finding nil values in my 'from' and 'to' UIViewControllers.
My setup is very similar to this SO Post
Custom DataEntryViewController
class DataEntryViewController : UIViewController, DataEntryViewDelegate, UINavigationControllerDelegate {
func navigationController(navigationController: UINavigationController, animationControllerForOperation operation: UINavigationControllerOperation, fromViewController fromVC: UIViewController, toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
let animator = DataEntryTransitionAnimator()
animator.duration = 2
return animator
}
}
Custom BaseTransitionAnimator
class BaseTransitionAnimator : NSObject, UIViewControllerAnimatedTransitioning {
var duration : NSTimeInterval = 0.5 // default transition time of 1/2 second
var appearing : Bool = true // is the animation appearing (or disappearing)
func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {
return duration
}
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
assert(true, "animateTransition MUST be implemented by child class")
}
}
Subclassed TransitionAnimator
class DataEntryTransitionAnimator : BaseTransitionAnimator {
override func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
let containerView = transitionContext.containerView()
let fromVC = transitionContext.viewControllerForKey(UITransitionContextFromViewKey) as! DataEntryViewController
let toVC = transitionContext.viewControllerForKey(UITransitionContextToViewKey) as! DataEntryViewController
let duration = transitionDuration(transitionContext)
// do fancy animations
}
}
Using the above, both fromVC and toVC are nil
How is it possible that the transitionContext doesn't have valid pointers to the 'to' and 'from' UIViewControllers?
you are using UITransitionContextFromViewKey but you need to use UITransitionContextFromViewControllerKey
same for "to"