I've a simple sample project in which I'm playing around some view controller custom animations, to integrate in a bigger project, but I've found some issues with them.
I have 2 simple view controllers, the first one with a button to present the second one, with custom animation; and the second one has some labels and a dismiss button which animates the view controller out 'of the scene'
When I'm presenting the view controller, after I click the present button, the subviews of the presented view controller, appear on the screen before the view controller animates in, but when I'm dismissing the VC, all the subviews go with the dismissed view controller.
On my view controller 2 (presented) I've the views with default auto-layout (reset to suggested constraints)
I can't find a justification why the subviews doesn't animate in, inside the view controller as I expected to.
Below goes a gif showing what's happening, and the source-code:
CODE: view controller 1 (presenting VC)
import UIKit
class ViewController: UIViewController, UIViewControllerTransitioningDelegate {
var animator = Animator()
override func viewDidLoad() {
super.viewDidLoad()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
@IBAction func presentButton(_ sender: Any) {
let storyboard = UIStoryboard(name: "Main", bundle: nil);
let vc = storyboard.instantiateViewController(withIdentifier: "vc2") as! ViewController2
vc.transitioningDelegate = self
vc.modalPresentationStyle = .custom // chama as funções à parte
present(vc, animated: true, completion: nil)
}
// REMARK: UIViewControllerTransitioningDelegate
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
animator.transitioningMode = .Present // sabe que está em presenting mode
return animator
}
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
animator.transitioningMode = .Dismiss // Sabe que está em dismissing mode
return animator
}
func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
return CustomPresentationController(presentedViewController: presented, presenting: presenting)
}
}
CODE: view controller 2 (presented VC)
import UIKit
class ViewController2: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
@IBAction func dismissButton(_ sender: Any) {
dismiss(animated: true, completion: nil)
}
}
CODE: CustomPresentationController
import UIKit
import Foundation
class CustomPresentationController: UIPresentationController {
override init(presentedViewController: UIViewController, presenting presentingViewController: UIViewController!) {
super.init(presentedViewController: presentedViewController, presenting: presentingViewController)
}
override var frameOfPresentedViewInContainerView: CGRect {
// arranca a 0
var presentedViewFrame = CGRect.zero
// Calcula os bounds do container
let containerBounds = self.containerView?.bounds
// Recalcula o size
presentedViewFrame.size = CGSize(width: (containerBounds?.size.width)! , height: ((containerBounds?.size.height)! * 0.90))
presentedViewFrame.origin.x = 0
presentedViewFrame.origin.y = (containerBounds?.size.height)! * 0.1
return presentedViewFrame
}
}
CODE: Animator class
import Foundation
import UIKit
class Animator: NSObject, UIViewControllerAnimatedTransitioning {
enum Status {
case Present
case Dismiss
}
var transitioningMode: Status = .Present
var presentDuration = 1.0
var dismissDuration = 0.3
// Tempo da animação
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
if (transitioningMode == .Present) {
return presentDuration
} else {
return dismissDuration
}
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
// Get the set of relevant objects.
let containerView = transitionContext.containerView
guard
let fromVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from),
let toVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)
else {
print("Returning animateTransition VC")
return
}
let toView = transitionContext.view(forKey: UITransitionContextViewKey.to)
let fromView = transitionContext.view(forKey: UITransitionContextViewKey.from)
// Set up some variables for the animation.
let containerFrame: CGRect = containerView.frame
var toViewStartFrame: CGRect = transitionContext.initialFrame(for: toVC)
let toViewFinalFrame: CGRect = transitionContext.finalFrame(for: toVC)
var fromViewFinalFrame: CGRect = transitionContext.finalFrame(for: fromVC)
// Set up animation parameters.
if (transitioningMode == .Present) {
// Modify the frame of the presented view so that it starts
// offscreen at the lower-right corner of the container.
toViewStartFrame.origin.x = 0//containerFrame.size.width
toViewStartFrame.origin.y = containerFrame.size.height * 0.1
} else {
// Modify the frame of the dismissed view so it ends in
// the lower-right corner of the container view.
fromViewFinalFrame = CGRect(x: containerFrame.size.width,
y: containerFrame.size.height,
width: (toVC.view.frame.size.width),
height: (toVC.view.frame.size.height))
}
if (transitioningMode == .Present) {
// Always add the "to" view to the container.
// And it doesn't hurt to set its start frame.
containerView.addSubview(toView!)
toView?.frame = toViewStartFrame
}
// Animate using the animator's own duration value.
UIView.animate(withDuration: presentDuration, animations: {
if (self.transitioningMode == .Present) {
// Move the presented view into position.
toView?.frame = toViewFinalFrame
}
else {
// Move the dismissed view offscreen.
fromView?.frame = fromViewFinalFrame
}
}) { (finished) in
let success = !(transitionContext.transitionWasCancelled)
// After a failed presentation or successful dismissal, remove the view.
if ((self.transitioningMode == .Present && !success) || (self.transitioningMode == .Dismiss && success)) {
toView?.removeFromSuperview()
}
// Notify UIKit that the transition has finished
transitionContext.completeTransition(success)
}
}
}
Alright I am going to take a stab at helping you. First the reason why it is not working is autolayout has position your views but you have a frame of size zero basically. To show you this go to the controller in question and have it clip its subviews and they will not show up during the transition. Now I probably would not change the frame to move it in since you just want to slide it in and you can probably get rid of some code. Here is what it looks like for me.
import UIKit
class ViewController: UIViewController,UIViewControllerTransitioningDelegate {
var animator = Animator()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
@IBAction func presentModally(_ sender: Any) {
self.performSegue(withIdentifier: "modal", sender: nil)
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "modal"{
let dvc = segue.destination
dvc.transitioningDelegate = self
dvc.modalPresentationStyle = .overCurrentContext
}
}
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
animator.transitioningMode = .Present // sabe que está em presenting mode
return animator
}
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
animator.transitioningMode = .Dismiss // Sabe que está em dismissing mode
return animator
}
}
Delete the Custom Presentation Controller. I honestly don't see a need for it.
Now the animator.
import UIKit
class Animator: NSObject,UIViewControllerAnimatedTransitioning {
enum Status {
case Present
case Dismiss
}
var transitioningMode: Status = .Present
var presentDuration = 1.0
var dismissDuration = 0.3
// Tempo da animação
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
if (transitioningMode == .Present) {
return presentDuration
} else {
return dismissDuration
}
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
// Get the set of relevant objects.
let containerView = transitionContext.containerView
guard
let fromVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from),
let toVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)
else {
print("Returning animateTransition VC")
return
}
let toView = transitionContext.view(forKey: UITransitionContextViewKey.to)
let fromView = transitionContext.view(forKey: UITransitionContextViewKey.from)
// Set up some variables for the animation.
let containerFrame: CGRect = containerView.frame
let toViewFinalFrame: CGRect = transitionContext.finalFrame(for: toVC)
var fromViewFinalFrame: CGRect = transitionContext.finalFrame(for: fromVC)
// Set up animation parameters.
if (transitioningMode == .Present) {
let anchor = toViewFinalFrame.origin
toView?.layer.anchorPoint = anchor
toView?.layer.position = anchor
toView?.transform = CGAffineTransform(scaleX: 0, y: 1)
//another posibility
//toView?.transform = CGAffineTransform(translationX: -containerView.bounds.width, y: -containerView.bounds.height)
} else {
// Modify the frame of the dismissed view so it ends in
// the lower-right corner of the container view.
fromViewFinalFrame = CGRect(x: containerFrame.size.width,
y: containerFrame.size.height,
width: (toVC.view.frame.size.width),
height: (toVC.view.frame.size.height))
}
if (transitioningMode == .Present) {
// Always add the "to" view to the container.
// And it doesn't hurt to set its start frame.
containerView.addSubview(toView!)
// toView?.frame = toViewStartFrame
}
// Animate using the animator's own duration value.
UIView.animate(withDuration: presentDuration, animations: {
if (self.transitioningMode == .Present) {
// Move the presented view into position.
toView?.transform = .identity
}
else {
// Move the dismissed view offscreen.
fromView?.frame = fromViewFinalFrame
}
}) { (finished) in
let success = !(transitionContext.transitionWasCancelled)
// After a failed presentation or successful dismissal, remove the view.
if ((self.transitioningMode == .Present && !success) || (self.transitioningMode == .Dismiss && success)) {
toView?.removeFromSuperview()
}
// Notify UIKit that the transition has finished
transitionContext.completeTransition(success)
}
}
}
That's all you need. Cheers.