I followed many tutorials about how to create custom transitions but most of them apply to the presentation and dismissing of the view controllers. I just want to apply custom transitions to the UINavigationController for the push/pop actions.
After reading apple documentation I created simple project to test if it works.
Here is all my code for custom Navigation Controller:
class CustomNC: UINavigationController {
override func viewDidLoad() {
super.viewDidLoad()
delegate = self
}
}
extension CustomNC: UINavigationControllerDelegate {
func navigationController(
navigationController: UINavigationController,
animationControllerForOperation operation: UINavigationControllerOperation,
fromViewController fromVC: UIViewController,
toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
print("Creating UIViewControllerAnimatedTransitioning for operation: \(operation.rawValue)")
return self
}
}
extension CustomNC: UIViewControllerAnimatedTransitioning {
func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {
print("Returning duration for transition")
return 1.0
}
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
print("Animating transition")
let container = transitionContext.containerView()!
let fromView = transitionContext.viewForKey(UITransitionContextFromViewKey)!
let toView = transitionContext.viewForKey(UITransitionContextToViewKey)!
// set up from 2D transforms that we'll use in the animation
let offScreenRight = CGAffineTransformMakeTranslation(container.frame.width, 0)
let offScreenLeft = CGAffineTransformMakeTranslation(-container.frame.width, 0)
// start the toView to the right of the screen
toView.transform = offScreenRight
// add the both views to our view controller
container.addSubview(toView)
container.addSubview(fromView)
// get the duration of the animation
// DON'T just type '0.5s' -- the reason why won't make sense until the next post
// but for now it's important to just follow this approach
let duration = self.transitionDuration(transitionContext)
// perform the animation!
// for this example, just slid both fromView and toView to the left at the same time
// meaning fromView is pushed off the screen and toView slides into view
// we also use the block animation usingSpringWithDamping for a little bounce
UIView.animateWithDuration(
duration,
delay: 0.0,
usingSpringWithDamping: 0.5,
initialSpringVelocity: 0.8,
options: [],
animations: {
fromView.transform = offScreenLeft
toView.transform = CGAffineTransformIdentity
}, completion: { finished in
// tell our transitionContext object that we've finished animating
transitionContext.completeTransition(true)
})
}
}
Then I simply create two default view controllers First and Second. First has UIButton with show action to the Second. First is embedded in NavigationController and this NavigationController has Custom Class set to CustomNC.
After running I see V1 controller but when I tap UIButton application crash without any error message. In Logs I see only those lines:
Creating UIViewControllerAnimatedTransitioning for operation: 1
Returning duration for transition
(lldb)
So as I understand the delegate is visible and navigation controller is using this delegate properly. When transition starts it gets the animation duration time interval but animateTransition method is never called because I don't see "Animating transition" message in log.
Can somebody point me where I have problem?
Related
So I have 2 view controllers, and I want to get from view controller 1 to view controller 2 with a custom animation. Here is the code for my custom animation:
let transition = CATransition()
transition.duration = 0.5
transition.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionDefault)
transition.type = kCATransitionPush
transition.subtype = kCATransitionFromRight
self.view.window?.layer.add(transition, forKey: nil)
I run this before I call performSegue(), and it works. But what I want to do is in the code for view controller 2, I want to run something after the segue animation finishes (so after the 0.5 seconds). My view controllers are not part of a navigation controller, so this post doesn't help. I also want my code to be in the target view controller, but this post has it in the source view controller, so that doesn't help either.
I've tried testing viewDidLoad() and viewDidAppear(), but they both run before the sliding animation is finished. Please help, thanks!
When you animate your transition correctly, viewDidAppear will be called when the animation is done. See Customizing Transition Animations in View Controller Programming Guide for iOS for instructions on the proper way to customize a transition between two view controllers.
As that guide says, when you want to customize a modal transition, you should specify a modalPresentationStyle of .custom and then specify a transitioningDelegate which will supply:
Presentation controller;
Animation controller for presenting a modal; and
Animation controller for dismissing a modal
For example, the destination view controller would specify that it will do a custom transition:
class ViewController: UIViewController {
// if storyboards, override `init(coder:)`; if NIBs or programmatically
// created view controllers, override the appropriate `init` method.
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
transitioningDelegate = self
modalPresentationStyle = .custom
}
...
}
And, in its UIViewControllerTransitioningDelegate, it vends that presentation controller and the animation controllers:
extension ViewController: UIViewControllerTransitioningDelegate {
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return TransitionAnimator(operation: .present)
}
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return TransitionAnimator(operation: .dismiss)
}
func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
return PresentationController(presentedViewController: presented, presenting: presenting)
}
}
All the presentation controller does is to specify that the presenter's view should be removed from the view hierarchy when the transition is done (which is the rule of thumb unless the presenting view is translucent or doesn't cover the whole screen):
class PresentationController: UIPresentationController {
override var shouldRemovePresentersView: Bool { return true }
}
And the animator specifies the duration and the particular details of the animation:
class TransitionAnimator: NSObject {
enum TransitionOperation {
case present
case dismiss
}
private let operation: TransitionOperation
init(operation: TransitionOperation) {
self.operation = operation
super.init()
}
}
extension TransitionAnimator: UIViewControllerAnimatedTransitioning {
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 2.0
}
func animateTransition(using context: UIViewControllerContextTransitioning) {
let toVC = context.viewController(forKey: .to)!
let fromVC = context.viewController(forKey: .from)!
let container = context.containerView
let frame = fromVC.view.frame
let rightFrame = CGRect(origin: CGPoint(x: frame.origin.x + frame.width, y: frame.origin.y), size: frame.size)
let leftFrame = CGRect(origin: CGPoint(x: frame.origin.x - frame.width, y: frame.origin.y), size: frame.size)
switch operation {
case .present:
toVC.view.frame = rightFrame
container.addSubview(toVC.view)
UIView.animate(withDuration: transitionDuration(using: context), animations: {
toVC.view.frame = frame
fromVC.view.frame = leftFrame
}, completion: { finished in
fromVC.view.frame = frame
context.completeTransition(!context.transitionWasCancelled)
})
case .dismiss:
toVC.view.frame = leftFrame
container.addSubview(toVC.view)
UIView.animate(withDuration: transitionDuration(using: context), animations: {
toVC.view.frame = frame
fromVC.view.frame = rightFrame
}, completion: { finished in
fromVC.view.frame = frame
context.completeTransition(!context.transitionWasCancelled)
})
}
}
}
Obviously, do whatever animation you want, but hopefully you get the basic idea. Bottom line, the destination view controller should specify its transitioningDelegate and then you can just do a standard modal presentation (either via present or show or just a segue), and your transition animation will be customized and your destination's viewDidAppear will be called when the animation is done.
I have a navigation controller, and inside of that navigation controller I have a home screen, from the home screen I click a button which goes to another screen.
But the standard show animation when using a navigation controller is that it slides from the side, but what I want to do is that the view controller slides up from bottom of the screen and creates a sort of bouncing animation when it reaches the top.
Anyone who wanted to use custom transition two things to remember UIViewControllerAnimatedTransitioning and UIViewControllerTransitioningDelegate protocols. Now conform UIViewControllerAnimatedTransitioning inside your customclass inheriting from NSObject
import UIKit
class CustomPushAnimation: NSObject, UIViewControllerAnimatedTransitioning {
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.2
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let containerVw = transitionContext.containerView
let fromViewController = transitionContext.viewController(forKey: .from)
let toViewController = transitionContext.viewController(forKey: .to)
guard let fromVc = fromViewController, let toVc = toViewController else { return }
let finalFrame = transitionContext.finalFrame(for: toVc)
//For different animation you can play around this line by changing frame
toVc.view.frame = finalFrame.offsetBy(dx: 0, dy: finalFrame.size.height/2)
containerVw.addSubview(toVc.view)
UIView.animate(withDuration: transitionDuration(using: transitionContext), animations: {
toVc.view.frame = finalFrame
fromVc.view.alpha = 0.5
}, completion: {(finished) in
transitionContext.completeTransition(finished)
fromVc.view.alpha = 1.0
})
}
}
The above method will take care of the animation. After that create
the above object and use inside yourViewController class
import UIKit
class YourViewController: UIViewController {
lazy var customPushAnimation: CustomPushAnimation = {
return CustomPushAnimation()
}()
func openViewControler() {
}let vc =//Assuming your view controller which you want to open
let navigationController = UINavigationController(rootViewController: vc)
//Set transitioningDelegate to invoke protocol method
navigationController.transitioningDelegate = self
present(navigationController, animated: true, completion: nil)
}
Note: In order to see the animation. Never set animation flag to false
while presenting the ViewController. Otherwise your animation will
never work.
Lastly implement the UIViewControllerTransitioningDelegate protocol method inside YourViewcontroller class
extension YourViewController: UIViewControllerTransitioningDelegate {
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return customPushAnimation
}
Whenever you present the viewcontroller above protocol method will
called and your animation magic will appear.
So I've been following a couple tutorials on how to perform a custom animation when presenting a view controller. I currently have the following,
A class called TransitionManager which will be instantiated in ViewController A that will present the view
ViewController A that will present ViewController B in a navigation controller
According to all the tutorials i've read after setting the delegate in the presentation of the view, I should see my custom transition. However, the default animation is still used instead. I've tried moving the setting of the delegate before and after presenting with no avail
Transition Manager Class
class TransitionManager: NSObject, UIViewControllerAnimatedTransitioning, UIViewControllerTransitioningDelegate {
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
// get reference to our fromView, toView and the container view that we should perform the transition in
let container = transitionContext.containerView
let fromView = transitionContext.view(forKey: .from)!
let toView = transitionContext.view(forKey: .to)!
// set up from 2D transforms that we'll use in the animation
let offScreenRight = CGAffineTransform(translationX: container.frame.width, y: 0)
let offScreenLeft = CGAffineTransform(translationX: -container.frame.width, y: 0)
// start the toView to the right of the screen
if self.presenting {
toView.transform = offScreenRight
} else {
toView.transform = offScreenLeft
}
// add the both views to our view controller
container.addSubview(toView)
container.addSubview(fromView)
// get the duration of the animation
// DON'T just type '0.5s' -- the reason why won't make sense until the next post
// but for now it's important to just follow this approach
let duration = self.transitionDuration(using: transitionContext)
// perform the animation!
// for this example, just slid both fromView and toView to the left at the same time
// meaning fromView is pushed off the screen and toView slides into view
// we also use the block animation usingSpringWithDamping for a little bounce
UIView.animate(withDuration: duration, delay: 0.0, usingSpringWithDamping: 0.5, initialSpringVelocity: 0.8, options: [], animations: {
if self.presenting {
toView.transform = offScreenLeft
} else {
toView.transform = offScreenRight
}
toView.transform = .identity
}, completion: { finished in
// tell our transitionContext object that we've finished animating
transitionContext.completeTransition(true)
})
}
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.5
}
// MARK: UIViewControllerTransitioningDelegate protocol methods
// return the animataor when presenting a viewcontroller
func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
self.presenting = true
return self
}
// return the animator used when dismissing from a viewcontroller
func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
self.presenting = false
return self
}
}
Class ViewController A
class ViewControllerA: UIViewController {
let transitionManager = TransitionManager()
func addButtonSelected() {
let vc = ViewControllerB()
let nav = UINavigationController(rootViewController: vc)
present(nav, animated: true, completion: nil)
nav.transitioningDelegate = self.transitionManager
}
}
Update your methods to swift3-:
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return Transition()
}
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return Transition()
}
You need to set the modalPresentationStyle to .custom, mentioned in the docs here and here. I'm not sure if it matters, but I've also always set the transitioning delegate before calling present:
nav.modalPresentationStyle = .custom
nav.transitioningDelegate = self.transitionManager
present(nav, animated: true, completion: nil)
I'm working with a few standard segues in storyboard and they each have the same background color. The issue I'm having is that when the segue transition nears completion there appears a dark shadow like background around the whole frame.
It's very faint, but enough to cause an issue. Has anyone come across this before?
The standard navigation controller push/pop animations darken the view that you're pushing from and the one you're popping to. If you don't like that, you can customize the transition, using an animation that just slides views in and out, but does no dimming of anything:
// this is the view controller you are pushing from
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
navigationController?.delegate = self
}
}
// make the view controller conform to `UINavigationControllerDelegate`
extension ViewController: UINavigationControllerDelegate {
func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationControllerOperation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return PushPopAnimator(operation: operation)
}
}
// The animation controller
class PushPopAnimator: NSObject, UIViewControllerAnimatedTransitioning {
let operation: UINavigationControllerOperation
init(operation: UINavigationControllerOperation) {
self.operation = operation
super.init()
}
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.5
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let from = transitionContext.viewController(forKey: .from)!
let to = transitionContext.viewController(forKey: .to)!
let rightTransform = CGAffineTransform(translationX: transitionContext.containerView.bounds.size.width, y: 0)
if operation == .push {
to.view.transform = rightTransform
transitionContext.containerView.addSubview(to.view)
UIView.animate(withDuration: transitionDuration(using: transitionContext), animations: {
to.view.transform = .identity
}, completion: { finished in
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
})
} else if operation == .pop {
to.view.transform = .identity
transitionContext.containerView.insertSubview(to.view, belowSubview: from.view)
UIView.animate(withDuration: transitionDuration(using: transitionContext), animations: {
from.view.transform = rightTransform
}, completion: { finished in
from.view.transform = .identity
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
})
}
}
}
For information on custom transitions with view controllers, see WWDC 2013 video Custom Transitions Using View Controllers.
I have a custom Viewcontroller Transition for the Navigation Controller in my app. When the transition is performed, it hasn't resized the content of the child viewcontroller properly. The default transition does resize it.
I have added an example project on Github to demonstrate the issue.
The VC embedded in the Navigation Controller
import UIKit
class PopoverVCViewController: UIViewController, UIViewControllerTransitioningDelegate, UINavigationControllerDelegate {
let animator = Animator()
override func viewDidLoad() {
super.viewDidLoad()
navigationController?.delegate = self
// Do any additional setup after loading the view.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func navigationController(navigationController: UINavigationController, animationControllerForOperation operation: UINavigationControllerOperation, fromViewController fromVC: UIViewController, toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
animator.reverse = operation == .Pop
return animator
}
}
The animator
class Animator: NSObject, UIViewControllerAnimatedTransitioning {
var reverse: Bool = false
func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
// get reference to our fromView, toView and the container view that we should perform the transition in
let container = transitionContext.containerView()!
let fromView = transitionContext.viewForKey(UITransitionContextFromViewKey)!
let toView = transitionContext.viewForKey(UITransitionContextToViewKey)!
// set up from 2D transforms that we'll use in the animation
let offScreenRight = CGAffineTransformMakeTranslation(container.frame.width, 0)
let offScreenLeft = CGAffineTransformMakeTranslation(-container.frame.width, 0)
// start the toView to the right of the screen
if (self.reverse == false) {
toView.transform = offScreenRight
}
else {
toView.transform = offScreenLeft
}
// add the both views to our view controller
container.addSubview(toView)
container.addSubview(fromView)
// get the duration of the animation
// DON'T just type '0.5s' -- the reason why won't make sense until the next post
// but for now it's important to just follow this approach
let duration = self.transitionDuration(transitionContext)
// perform the animation!
// for this example, just slid both fromView and toView to the left at the same time
// meaning fromView is pushed off the screen and toView slides into view
// we also use the block animation usingSpringWithDamping for a little bounce
UIView.animateWithDuration(duration, delay: 0.0, usingSpringWithDamping: 1.0, initialSpringVelocity: 0.0, options: UIViewAnimationOptions.CurveEaseInOut, animations: {
if (self.reverse == false) {
fromView.transform = offScreenLeft
}
else {
fromView.transform = offScreenRight
}
toView.transform = CGAffineTransformIdentity
}, completion: { finished in
// tell our transitionContext object that we've finished animating
transitionContext.completeTransition(true)
})
}
// return how many seconds the transiton animation will take
func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {
return 0.3
}
// MARK: UIViewControllerTransitioningDelegate protocol methods
// return the animataor when presenting a viewcontroller
// remmeber that an animator (or animation controller) is any object that aheres to the UIViewControllerAnimatedTransitioning protocol
func animationControllerForPresentedController(presented: UIViewController, presentingController presenting: UIViewController, sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return self
}
// return the animator used when dismissing from a viewcontroller
func animationControllerForDismissedController(dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return self
}
}
You can set the frame of toView to match the height and width of fromView :
let container = transitionContext.containerView()!
let fromView = transitionContext.viewForKey(UITransitionContextFromViewKey)!
let toView = transitionContext.viewForKey(UITransitionContextToViewKey)!
toView.frame = CGRectMake(toView.frame.origin.x, toView.frame.origin.y, fromView.frame.width, fromView.frame.height)
or like this:
UIViewController* toController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
UIViewController* fromController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
and then setup this near the end of your animation routine:
toController.view.frame=fromController.view.frame;