title: Trying to mimic the Mail.app compose animation keeping a layer in view
body: <p>I have been trying for a while but I cannot figure out how to create a Compose animation seen in the <code>iOS 10+</code> when you can drag the new composed email down, then it stays on the bottom and the rest of the app is normally accessed, then when you tap it, it re-shows.</p>
  
  <p>I have created a sample project in which I have a <code>UIViewController</code> that presents another <code>UIViewController</code> which has a <code>UIPanGestureRecognizer</code> in it's <code>UINavigationController</code> that fires the <code>pangesture</code> state analyzer.</p>
  
  <p>I can indeed drag to dismiss it , but I cannot find a way to keep it frame.</p>
  
  <p>Bellow there's a print screen of what I'm trying to accomplish and then my used code to where I'm stuck at.</p>
  
  <p><a href="https://i.stack.imgur.com/bip7Z.gif" rel="nofollow noreferrer"><img src="https://i.stack.imgur.com/bip7Z.gif" alt="enter image description here"></a></p>
  
  <p><code>UIViewController</code> that is the <code>presentingViewController</code> class</p>
  
  <pre><code>//
  //  ViewController.swift
  //  dismissLayerTest
  //
  //  Created by Ivan Cantarino on 27/09/17.
  //  Copyright © 2017 Ivan Cantarino. All rights reserved.
  //
  
  import UIKit
  
  class ViewController: UIViewController, UIViewControllerTransitioningDelegate {
  
  
      @objc let interactor = Interactor()
  
      lazy var presentButton: UIButton = {
          let b = UIButton(type: .custom)
          b.setTitle("Present", for: .normal)
          b.setTitleColor(.black, for: .normal)
          b.addTarget(self, action: #selector(didTapPresentButton), for: .touchUpInside)
          return b
      }()
  
      lazy var testbutton: UIButton = {
          let b = UIButton(type: .custom)
          b.setTitle("test", for: .normal)
          b.setTitleColor(.black, for: .normal)
          b.addTarget(self, action: #selector(test), for: .touchUpInside)
          return b
      }()
  
      override func viewDidLoad() {
          super.viewDidLoad()
          // Do any additional setup after loading the view, typically from a nib.
          view.backgroundColor = .white
          view.addSubview(presentButton)
          presentButton.anchor(top: nil, left: nil, bottom: nil, right: nil, paddingTop: 0, paddinfLeft: 0, paddingBottom: 0, paddingRight: 0, width: 100, height: 100)
          presentButton.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
          presentButton.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
  
          view.addSubview(testbutton)
          testbutton.anchor(top: nil, left: nil, bottom: presentButton.topAnchor, right: nil, paddingTop: 0, paddinfLeft: 0, paddingBottom: 100, paddingRight: 0, width: 100, height: 100)
      }
  
      override func didReceiveMemoryWarning() {
          super.didReceiveMemoryWarning()
          // Dispose of any resources that can be recreated.
      }
  
      @objc func didTapPresentButton() {
          let presentedVC = PresentedViewController()
          let navController = UINavigationController(rootViewController: presentedVC)
  
          navController.transitioningDelegate = self
          presentedVC.interactor = interactor // new
          navController.modalPresentationStyle = .custom
          navController.view.layer.masksToBounds = true
  
          present(navController, animated: true, completion: nil)
  
      }
  
      @objc func test() {
          print("test")
      }
  
      // Handles the presenting animation
      func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -&gt; UIViewControllerAnimatedTransitioning? {
          return CustomAnimationForPresentor()
      }
  
  
      // Handles the dismissing animation
      func animationController(forDismissed dismissed: UIViewController) -&gt; UIViewControllerAnimatedTransitioning? {
          return CustomAnimationForDismisser()
      }
  
  
      // interaction controller, only for dismissing the view;
      func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -&gt; UIViewControllerInteractiveTransitioning? {
          return interactor.hasStarted ? interactor : nil
      }
  
      // delegate do custom modal presentation style
      func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -&gt; UIPresentationController? {
              return CustomPresentationController(presentedViewController: presented, presenting: presenting)
          }
  
  }
  </code></pre>
  
  <p><code>UIViewController</code> 2 that is the <code>presentedViewController</code></p>
  
  <pre><code>import Foundation
  import UIKit
  
  
  class PresentedViewController: UIViewController, UIViewControllerTransitioningDelegate, UIGestureRecognizerDelegate {
  
  
  
      @objc var interactor: Interactor? = nil
      @objc var panGr = UIPanGestureRecognizer()
      @objc var panTapRecon = UITapGestureRecognizer()
  
      override func viewDidLoad() {
          super.viewDidLoad()
          view.backgroundColor = .green
  
          let leftB = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(didTapCancel))
          navigationItem.leftBarButtonItem = leftB
  
          panGr = UIPanGestureRecognizer(target: self, action: #selector(handleGesture))
          navigationController?.navigationBar.addGestureRecognizer(panGr)
  
          panTapRecon = UITapGestureRecognizer(target: self, action: #selector(handleNavControllerTapGR))
          navigationController?.navigationBar.addGestureRecognizer(panTapRecon)
      }
  
      @objc func didTapCancel() {
          guard let interactor = interactor else { return }
          interactorFinish(interactor: interactor)
          dismiss(animated: true, completion: nil)
      }
  
      @objc func handleNavControllerTapGR(_ sender: UITapGestureRecognizer) {
          print("tap detected")
      }
  
  
      // Swipe gesture recognizer handler
      @objc func handleGesture(_ sender: UIPanGestureRecognizer) {
  
          //percentThreshold: This variable sets how far down the user has to drag
          //in order to trigger the modal dismissal. In this case, it’s set to 40%.
          let percentThreshold:CGFloat = 0.30
  
          // convert y-position to downward pull progress (percentage)
          let translation = sender.translation(in: view)
          let verticalMovement = translation.y / view.bounds.height
          let downwardMovement = fmaxf(Float(verticalMovement), 0.0)
          let downwardMovementPercent = fminf(downwardMovement, 1.0)
          let progress = CGFloat(downwardMovementPercent)
  
          guard let interactor = interactor else { return }
  
          switch sender.state {
  
          case .began:
              interactor.hasStarted = true
              self.dismiss(animated: true, completion: nil)
  
          case .changed:
  
              // alterar se o tamanho do presentigViewController (MainTabBarController) for alterado no background
              let scaleX = 0.95 + (progress * (1 - 0.95))
              let scaleY = 0.95 + (progress * (1 - 0.95))
  
              // Não deixa ultrapassar os 100% de scale (tamanho original)
              if (scaleX &gt; 1 &amp;&amp; scaleY &gt; 1) { return }
              presentingViewController?.view.transform = CGAffineTransform.identity.scaledBy(x: scaleX, y: scaleY);
              presentingViewController?.view.layer.masksToBounds = true
  
              interactor.shouldFinish = progress &gt; percentThreshold
              interactor.update(progress)
  
          case .cancelled:
              interactor.hasStarted = false
              interactor.cancel()
  
          case .ended:
              interactor.hasStarted = false
              if (interactor.shouldFinish) {
                  interactorFinish(interactor: interactor)
              } else {
  
                  // repõe o MainTabBarController na posição dele atrás do NewPostController
                  UIView.animate(withDuration: 0.5, animations: {
                      self.presentingViewController?.view.transform = CGAffineTransform.identity.scaledBy(x: 0.95, y: 0.95);
                      self.presentingViewController?.view.layer.masksToBounds = true
                      let c = UIColor.black.withAlphaComponent(0.4)
                      let shadowView = self.presentingViewController?.view.viewWithTag(999)
                      shadowView?.backgroundColor = c
                  })
                  interactor.cancel()
              }
  
          default: break
          }
      }
  
  
      @objc func interactorFinish(interactor: Interactor) {
          removeShadow()
          interactor.finish()
      }
  
      // remove a shadow view
      @objc func removeShadow() {
          UIView.animate(withDuration: 0.2, animations: {
              self.presentingViewController?.view.transform = CGAffineTransform.identity.scaledBy(x: 1.0, y: 1.0);
              self.presentingViewController?.view.layer.masksToBounds = true
  
          }) { _ in
          }
      }
  }
  </code></pre>
  
  <p>Here's an Helper file that has the custom presentations:</p>
  
  <pre><code>//
  //  Helper.swift
  //  dismissLayerTest
  //
  //  Created by Ivan Cantarino on 27/09/17.
  //  Copyright © 2017 Ivan Cantarino. All rights reserved.
  //
  
  import Foundation
  import UIKit
  
  class Interactor: UIPercentDrivenInteractiveTransition {
      @objc var hasStarted = false
      @objc var shouldFinish = false
  }
  
  
  extension UIView {
      @objc func anchor(top: NSLayoutYAxisAnchor?, left: NSLayoutXAxisAnchor?, bottom: NSLayoutYAxisAnchor?, right: NSLayoutXAxisAnchor?, paddingTop: CGFloat, paddinfLeft: CGFloat, paddingBottom: CGFloat, paddingRight: CGFloat, width: CGFloat, height: CGFloat) {
          translatesAutoresizingMaskIntoConstraints = false
          if let top = top {
              topAnchor.constraint(equalTo: top, constant: paddingTop).isActive = true
          }
          if let left = left {
              leftAnchor.constraint(equalTo: left, constant: paddinfLeft).isActive = true
          }
          if let bottom = bottom {
              bottomAnchor.constraint(equalTo: bottom, constant: -paddingBottom).isActive = true
          }
          if let right = right {
              rightAnchor.constraint(equalTo: right, constant: -paddingRight).isActive = true
          }
          if width != 0 {
              widthAnchor.constraint(equalToConstant: width).isActive = true
          }
          if height != 0 {
              heightAnchor.constraint(equalToConstant: height).isActive = true
          }
      }
  
      @objc func roundCorners(corners:UIRectCorner, radius: CGFloat) {
          let path = UIBezierPath(roundedRect: self.bounds, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius))
          let mask = CAShapeLayer()
          mask.path = path.cgPath
          self.layer.mask = mask
      }
  }
  
  
  
  class CustomAnimationForDismisser: NSObject, UIViewControllerAnimatedTransitioning {
  
      // Tempo da animação
      func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -&gt; TimeInterval {
          return 0.27
      }
  
      func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
          // Get the set of relevant objects.
          let containerView = transitionContext.containerView
          guard let fromVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from) else {
              print("Returning animateTransition VC")
              return
          }
          // from view só existe no dismiss
          guard let fromView = transitionContext.view(forKey: UITransitionContextViewKey.from) else {
              print("Failed to instantiate fromView: CustomAnimationForDismisser()")
              return
          }
          // Set up some variables for the animation.
          let containerFrame: CGRect = containerView.frame
          var fromViewFinalFrame: CGRect = transitionContext.finalFrame(for: fromVC)
          fromViewFinalFrame = CGRect(x: 0, y: containerFrame.size.height, width: containerFrame.size.width, height: containerFrame.size.height)
  
          // Animate using the animator's own duration value.
          UIView.animate(withDuration: 0.4, delay: 0, options: .curveEaseOut, animations: {
              fromView.frame = fromViewFinalFrame
          }) { (finished) in
              let success = !(transitionContext.transitionWasCancelled)
              // Notify UIKit that the transition has finished
              transitionContext.completeTransition(success)
          }
      }
  }
  
  
  
  class CustomAnimationForPresentor: NSObject, UIViewControllerAnimatedTransitioning {
      // Tempo da animação
      func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -&gt; TimeInterval {
          return 0.2
      }
      func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
          // Get the set of relevant objects.
          let containerView = transitionContext.containerView
  
          // obtém os VCs para não o perder na apresentação (default desaparece por trás)
          guard let fromVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from) else {//, let toVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to) else {
              print("Returning animateTransition VC")
              return
          }
          // gets the view of the presented object
          guard let toView = transitionContext.view(forKey: UITransitionContextViewKey.to) else { return }
  
          // Set up animation parameters.
          toView.transform = CGAffineTransform(translationX: 0, y: containerView.bounds.height)
  
          // Always add the "to" view to the container.
          containerView.addSubview(toView)
  
          // Animate using the animator's own duration value.
          UIView.animate(withDuration: 0.35, delay: 0, options: .curveEaseOut, animations: {
              // Zooms out da MainTabBarController - o VC
              fromVC.view.transform = CGAffineTransform(scaleX: 0.95, y: 0.95)
              // propriedades declaradas no CustomPresentationController() // Anima o presented view
              toView.transform = .identity
          }, completion: { (finished) in
              let success = !(transitionContext.transitionWasCancelled)
              // So it avoids view stacks and overlap issues
              if (!success) { toView.removeFromSuperview() }
              // Notify UIKit that the transition has finished
              transitionContext.completeTransition(success)
          })
      }
  }
  
  class CustomPresentationController: UIPresentationController {
      override init(presentedViewController: UIViewController, presenting presentingViewController: UIViewController!) {
          super.init(presentedViewController: presentedViewController, presenting: presentingViewController)
      }
  
      // Tamanho desejado para o NewPostController
      override var frameOfPresentedViewInContainerView: CGRect {
          guard let containerBounds = containerView?.bounds else {
              print("Failed to instantiate container bounds: CustomPresentationController")
              return .zero
          }
          return CGRect(x: 0.0, y: 0.0, width: containerBounds.width, height: containerBounds.height)
      }
      // Garante que o frame do view controller a mostrar, se mantém conforme desenhado na função frameOfPresentedViewInContainerView
      override func containerViewWillLayoutSubviews() {
          presentedView?.frame = frameOfPresentedViewInContainerView
      }
  }
  </code></pre>
  
  <p>This desired effect can also be seen in other apps, such like <code>Music app</code>, <code>Stack Exchange/Overflow iOS App</code></p>
  
  <p>Does anyone have a hint on how can this be accomplished? I feel like I'm really close to achieve it, but I can't find a way to keep the <code>dismissed</code> view with a layer on screen.</p>
  
  <p>The project above can be found <a href="https://www.dropbox.com/s/vqzoibfujncpt0k/dismissLayerTest.zip?dl=0" rel="nofollow noreferrer">here</a> </p>
  
  <p>Thank you very much.
  Regards.</p>