Search code examples
iosswiftuiviewcontrollerswift3

How to present a ViewController on Half screen


I have a UIViewController which have only a UIView which covers 1/3 of the viewController from bottom. Like this

enter image description here

I want to present this viewController on an other ViewController. It should appear from bottom animated and it should dismiss to the bottom animated.

But I do not want it to cover the whole Screen. The viewController on which it is presented should be visible in the back.

It seems like a basic question But I am unable to get it done. Can someone please point me to the direction ?

Edit:

This is what I have tried so Far. I have created these classes

// MARK: -

class MyFadeInFadeOutTransitioning: NSObject, UIViewControllerTransitioningDelegate {
var backgroundColorAlpha: CGFloat = 0.5
var shoulDismiss = false

func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {

    let fadeInPresentAnimationController = MyFadeInPresentAnimationController()
        fadeInPresentAnimationController.backgroundColorAlpha = backgroundColorAlpha

    return fadeInPresentAnimationController
}

func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {

    let fadeOutDismissAnimationController = MyFadeOutDismissAnimationController()

    return fadeOutDismissAnimationController
}

}

// MARK: -

class MYFadeInPresentAnimationController: NSObject, UIViewControllerAnimatedTransitioning {

let kPresentationDuration = 0.5
var backgroundColorAlpha: CGFloat?

func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
    return kPresentationDuration
}

func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
    let toViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)!

    toViewController.view.backgroundColor = UIColor.clear

    let toViewFrame = transitionContext.finalFrame(for: toViewController)
    let containerView = transitionContext.containerView

    if let pickerContainerView = toViewController.view.viewWithTag(kContainerViewTag) {
        let transform = CGAffineTransform(translationX: 0.0, y: pickerContainerView.frame.size.height)
        pickerContainerView.transform = transform
    }

    toViewController.view.frame = toViewFrame
    containerView.addSubview(toViewController.view)

    UIView.animate(withDuration: 0.3, delay: 0.0, options: .curveLinear , animations: {
        toViewController.view.backgroundColor = UIColor(white: 0.0, alpha: self.backgroundColorAlpha!)

        if let pickerContainerView = toViewController.view.viewWithTag(kContainerViewTag) {
            pickerContainerView.transform = CGAffineTransform.identity
        }

    }) { (finished) in
        transitionContext.completeTransition(true)
    }
}

}

// MARK: -

class MYFadeOutDismissAnimationController: NSObject, UIViewControllerAnimatedTransitioning {
let kDismissalDuration = 0.15

func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
    return kDismissalDuration
}

func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
    let fromViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from)!
    let toViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)!
    let containerView = transitionContext.containerView

    containerView.addSubview(toViewController.view)
    containerView.sendSubview(toBack: toViewController.view)

    UIView.animate(withDuration: kDismissalDuration, delay: 0.0, options: .curveLinear, animations: {
        //            fromViewController.view.backgroundColor = UIColor.clearColor()
        //            if let pickerContainerView = toViewController.view.viewWithTag(kContainerViewTag) {
        //                let transform = CGAffineTransformMakeTranslation(0.0, pickerContainerView.frame.size.height)
        //                pickerContainerView.transform = transform
        //            }
        fromViewController.view.alpha = 0.0

    }) { (finished) in
        let canceled: Bool = transitionContext.transitionWasCancelled
        transitionContext.completeTransition(true)

        if !canceled {
            UIApplication.shared.keyWindow?.addSubview(toViewController.view)
        }
    }
}

}

And in the viewController which is being presented, I am doing as follows

var customTransitioningDelegate: MYFadeInFadeOutTransitioning? = MYFadeInFadeOutTransitioning()

    init() {
    super.init(nibName: "SomeNibName", bundle: Bundle.main)
    transitioningDelegate = customTransitioningDelegate
    modalPresentationStyle = .custom

    customTransitioningDelegate?.backgroundColorAlpha = 0.0
} 

It do present the viewController and I can see the background viewController as well. But I want it to be presented from bottom with animation. And dismiss to bottom with animation. How can I do that ?


Solution

  • iOS 15 *

    Use UISheetPresentationController

    How to use

    let yourVC = YourViewController()
    
    if let sheet = yourVC.sheetPresentationController {
        sheet.detents = [.medium()]
    }
    self.present(yourVC, animated: true, completion: nil)
    

    Read more Here

    Below iOS 15

    Use UIPresentationController, It will also work on ios 15 as well. But if your app support only iOS 15 use the code above.

    import UIKit
    
    class PresentationController: UIPresentationController {
    
      let blurEffectView: UIVisualEffectView!
      var tapGestureRecognizer: UITapGestureRecognizer = UITapGestureRecognizer()
      
      override init(presentedViewController: UIViewController, presenting presentingViewController: UIViewController?) {
          let blurEffect = UIBlurEffect(style: .dark)
          blurEffectView = UIVisualEffectView(effect: blurEffect)
          super.init(presentedViewController: presentedViewController, presenting: presentingViewController)
          tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(dismissController))
          blurEffectView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
          self.blurEffectView.isUserInteractionEnabled = true
          self.blurEffectView.addGestureRecognizer(tapGestureRecognizer)
      }
      
      override var frameOfPresentedViewInContainerView: CGRect {
          CGRect(origin: CGPoint(x: 0, y: self.containerView!.frame.height * 0.4),
                 size: CGSize(width: self.containerView!.frame.width, height: self.containerView!.frame.height *
                  0.6))
      }
    
      override func presentationTransitionWillBegin() {
          self.blurEffectView.alpha = 0
          self.containerView?.addSubview(blurEffectView)
          self.presentedViewController.transitionCoordinator?.animate(alongsideTransition: { (UIViewControllerTransitionCoordinatorContext) in
              self.blurEffectView.alpha = 0.7
          }, completion: { (UIViewControllerTransitionCoordinatorContext) in })
      }
      
      override func dismissalTransitionWillBegin() {
          self.presentedViewController.transitionCoordinator?.animate(alongsideTransition: { (UIViewControllerTransitionCoordinatorContext) in
              self.blurEffectView.alpha = 0
          }, completion: { (UIViewControllerTransitionCoordinatorContext) in
              self.blurEffectView.removeFromSuperview()
          })
      }
      
      override func containerViewWillLayoutSubviews() {
          super.containerViewWillLayoutSubviews()
        presentedView!.roundCorners([.topLeft, .topRight], radius: 22)
      }
    
      override func containerViewDidLayoutSubviews() {
          super.containerViewDidLayoutSubviews()
          presentedView?.frame = frameOfPresentedViewInContainerView
          blurEffectView.frame = containerView!.bounds
      }
    
      @objc func dismissController(){
          self.presentedViewController.dismiss(animated: true, completion: nil)
      }
    }
    
    extension UIView {
      func roundCorners(_ corners: UIRectCorner, radius: CGFloat) {
          let path = UIBezierPath(roundedRect: bounds, byRoundingCorners: corners,
                                  cornerRadii: CGSize(width: radius, height: radius))
          let mask = CAShapeLayer()
          mask.path = path.cgPath
          layer.mask = mask
      }
    }
    

    How to use

    Add UIViewControllerTransitioningDelegate to your presenting ViewController

    // MARK: - UIViewControllerTransitioningDelegate
    extension PresentingViewController: UIViewControllerTransitioningDelegate {
        
        func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
            PresentationController(presentedViewController: presented, presenting: presenting)
        }
    }
    

    Present yourVC in PresentingViewController

    let yourVC = YourViewController()
    yourVC.modalPresentationStyle = .custom
    yourVC.transitioningDelegate = self
    self.present(yourVC, animated: true, completion: nil)
    

    Code reference