Custom transition - ios

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.

Related

UIViewControllerAnimatedTransitioning ... 'wrong' VC fetched via `.from` key

I'm working on my first custom animation/presentation transition.
I've configured:
UIPresentationController
NSObject conforming to UIViewControllerAnimatedTransitioning protocol.
I'm doing modal presentation of a presentedVc.
I want to display presentedVc with custom transition
I want it to be presented over presentingVc
Note that presentingVC is root view controller assigned to a UINavigationController.
The Problem:
In animator object: When I fetch .from and .to keys, .from controller is UINavigationController
(i.e. Instead of expected presentingVc)
I'm not sure what to do about that.
Not only do I need presentingVc to be recipient of presentedVc, but there are specific animations I need to perform with subviews of presentingVc.view
In presentedVc:
init(...) {
transitioningDelegate = self
modalPresentationStyle = .custom
}
From unrelated Vc:
let presentedVc = PresentedVc(originalFrame: CGRect(...))
presentingVc.present(presentedVc, animated: true, completion: nil)
In UIViewControllerAnimatedTransitioning class:
class ResizingAnimatedTransition : NSObject, UIViewControllerAnimatedTransitioning {
var originalFrame = CGRect.zero
init(originalFrame: CGRect) {
self.originalFrame = originalFrame
}
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 3.0
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard
let fromVC = transitionContext.viewController(forKey: .from),
let toVC = transitionContext.viewController(forKey: .to)
else {
return
}
.
. // At this point fromVC is UINavigationController
. // but I was expecting it to be UIViewController presentedVC
}
}

swift 4 - how do I run code in the destination view controller AFTER the segue animation is finished?

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.

How do I keep the source view controller from disappearing after a push segue?

First off, this is my view controller/segue setup:
The three rightmost view controllers' background views are UIVisualEffectViews through which the source view controllers should be visible. They were added in the various viewDidLoad()s like this:
let blurEffect = UIBlurEffect(style: .dark)
let blurEffectView = UIVisualEffectView(effect: blurEffect)
blurEffectView.frame = self.view.bounds
blurEffectView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
self.tableView.backgroundView = blurEffectView
Now, the main view controller (the "Garbage Day" one) is visible through the settings view controller, but the settings VC disappears whenever one of the two rightmost VCs is fully on screen. Here's a screen recording:
Screen recording of the source view controller dis- and reappearing
(Please ignore the glitches, the app I used to upload this apparently corrupted the video)
I get that technically, the Show segue doesn't have the "Over Current Context" thingy and therefore, I shouldn't expect the source VC to not disappear, but there has to be a way to make this work without custom segues.
I suggest you create a custom transition between view controllers.
I just wrote and tested this class:
class AnimationController: NSObject, UIViewControllerAnimatedTransitioning
{
var pushing = true
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 0.3
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let duration = transitionDuration(using: transitionContext)
let toVc = transitionContext.viewController(forKey: .to)!
let toView = transitionContext.view(forKey: .to)!
let fromView = transitionContext.view(forKey: .from)!
let container = transitionContext.containerView
if pushing {
container.addSubview(fromView)
container.addSubview(toView)
}
var finalFrame = transitionContext.finalFrame(for: toVc)
if pushing {
finalFrame.origin.x = finalFrame.width
toView.frame = finalFrame
finalFrame.origin.x = 0
} else {
finalFrame.origin.x = finalFrame.width
}
UIView.animate(withDuration: duration, delay: 0, options: .curveEaseOut, animations: {
if self.pushing {
toView.frame = finalFrame
} else {
fromView.frame = finalFrame
}
}) { (_) in
transitionContext.completeTransition(true)
if self.pushing {
container.insertSubview(fromView, belowSubview: toView)
} else {
fromView.removeFromSuperview()
}
}
}
}
In your UINavigationController class do the following:
class NavigationController: UINavigationController {
let animationController = AnimationController()
override func viewDidLoad() {
super.viewDidLoad()
delegate = self
}
}
And this extension:
extension NavigationController: UINavigationControllerDelegate
{
func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationControllerOperation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
animationController.pushing = operation == .push
return animationController
}
}
However, this makes you lose the interactive dismiss gesture (Swiping from the left of the screen to dismiss) So you would need to fix that yourself.

Custom Slide in animation when presenting navigation controller not working at all in swift

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)

Inside UIViewControllerAnimatedTransitioning delegate, the transitionContext.fromViewController is nil

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"

Resources