43

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 ?

Umair Afzal
  • 4,947
  • 5
  • 25
  • 50

11 Answers11

50

If you want to present a view controller over half a screen I suggest using the UIPresentationController class it will allow you to set the frame of the view controller when it is presented. A word of advice, this method will stop the user interaction of the presentingViewController until you dismiss the presentedViewController, so if you want to show the view controller over half the screen while retaining user interaction with the presentingViewController you should use container views like the other answers suggested. This is an example of a UIPresentationController class that does what you want

import UIKit
class ForgotPasswordPresentationController: UIPresentationController{
    let blurEffectView: UIVisualEffectView!
    var tapGestureRecognizer: UITapGestureRecognizer = UITapGestureRecognizer()
    func dismiss(){
        self.presentedViewController.dismiss(animated: true, completion: nil)
    }
    override init(presentedViewController: UIViewController, presenting presentingViewController: UIViewController?) {
        let blurEffect = UIBlurEffect(style: UIBlurEffectStyle.dark)
        blurEffectView = UIVisualEffectView(effect: blurEffect)
        super.init(presentedViewController: presentedViewController, presenting: presentingViewController)
        tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.dismiss))
        blurEffectView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        self.blurEffectView.isUserInteractionEnabled = true
        self.blurEffectView.addGestureRecognizer(tapGestureRecognizer)
    }
    override var frameOfPresentedViewInContainerView: CGRect{
        return CGRect(origin: CGPoint(x: 0, y: self.containerView!.frame.height/2), size: CGSize(width: self.containerView!.frame.width, height: self.containerView!.frame.height/2))
    }
    override func dismissalTransitionWillBegin() {
        self.presentedViewController.transitionCoordinator?.animate(alongsideTransition: { (UIViewControllerTransitionCoordinatorContext) in
            self.blurEffectView.alpha = 0
        }, completion: { (UIViewControllerTransitionCoordinatorContext) in
            self.blurEffectView.removeFromSuperview()
        })
    }
    override func presentationTransitionWillBegin() {
        self.blurEffectView.alpha = 0
        self.containerView?.addSubview(blurEffectView)
        self.presentedViewController.transitionCoordinator?.animate(alongsideTransition: { (UIViewControllerTransitionCoordinatorContext) in
            self.blurEffectView.alpha = 1
        }, completion: { (UIViewControllerTransitionCoordinatorContext) in

        })
    }
    override func containerViewWillLayoutSubviews() {
        super.containerViewWillLayoutSubviews()
        presentedView!.layer.masksToBounds = true
        presentedView!.layer.cornerRadius = 10
    }
    override func containerViewDidLayoutSubviews() {
        super.containerViewDidLayoutSubviews()
        self.presentedView?.frame = frameOfPresentedViewInContainerView
        blurEffectView.frame = containerView!.bounds
    }
}

This also adds a blur view and a tap to dismiss when you tap outside the presentedViewController frame. You need to set the transitioningDelegate of the presentedViewController and implement the

presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController?

method in there. Don't forget to also set modalPresentationStyle = .custom of the presentedViewController

I find the usage of the UIPresentationController to be a much cleaner approach. Good luck

DatForis
  • 1,331
  • 12
  • 19
  • that's a great answer. How could I adapt it to when presented view controller already has a `UIPopoverPresentationController` but on the iPhone, I want the presented view controller to be displayed only on a half of the screen. – Jan Jan 16 '18 at 20:47
  • 15
    How am I now able to present it? The instructions below your code are very unclear - please consider the fact that you are answering a question for people who never worked with a UIPresentationController – linus_hologram Aug 05 '19 at 21:41
  • 3
    The UIPresentationController is not what is presented - it handles the presentation. Missing steps below: 1 - implement the UIViewControllerTransitioningDelegate protocol and in the presentationController(forPresented:presenting:source:) method return the UIPresentationController (e.g. ForgotPasswordPresentationController in this case) 2 - Present your view controller the way you normally would, but with some customizations, e.g. `myPresentedVC.modalPresentationStyle = .custom myPresetedVC.transitioningDelegate = myPresentedVC.modalPresentationStyle = .custom` – DCDC Jan 19 '21 at 16:01
40

iOS 15: There's a new class, UISheetPresentationController, which contains a property called detents. This lets you specify what type of sizing behavior you want.

class ViewController: UIViewController {
    @IBAction func nextButtonPressed(_ sender: Any) {
        let storyboard = UIStoryboard(name: "Main", bundle: nil)
        let viewController = storyboard.instantiateViewController(withIdentifier: "NextViewController")
        
        if let presentationController = viewController.presentationController as? UISheetPresentationController {
            presentationController.detents = [.medium()] /// change to [.medium(), .large()] for a half *and* full screen sheet
        }
        
        self.present(viewController, animated: true)
    }
}
Half-screen sheet Half and full-screen sheet
Half screen sheet Sheet is draggable between half screen and full screen
aheze
  • 24,434
  • 8
  • 68
  • 125
  • Hello seems good but I am getting error can you provide me the demo complete code for both controllers Thanks Error: Cannot find type 'UISheetPresentationController' in scope code attatched: let vc = storyboard?.instantiateViewController(identifier: "AddressListVC") as! AddressListVC if let presentationController = vc.presentationController as? UISheetPresentationController { presentationController.detents = [.medium()] /// change to [.medium(), .large()] for a half *and* full screen sheet } self.present(vc, animated: true) – Faraz Ahmed Dec 23 '21 at 17:10
  • 1
    I boiled the ocean to get to this. F*&* – p0lAris Jun 22 '23 at 01:31
34

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

Sreekuttan
  • 1,579
  • 13
  • 19
9

I'd recommend to implement this feature by using Container Views. Take a look here for reference.

This means you can show a UIViewController (and its subclasses) embedded in a UIView within another view controller. Then you can animate the fade-in or whatever you want.

shim
  • 9,289
  • 12
  • 69
  • 108
regetskcob
  • 1,172
  • 1
  • 13
  • 35
6

There is the updated code to achieve this functionality. On action where you want to present ViewController

@IBAction func btnShow(_ sender: Any) {
    let storyboard = UIStoryboard(name: "Main", bundle: nil)
    let pvc = storyboard.instantiateViewController(withIdentifier: "SubViewController") as! SubViewController
    pvc.modalPresentationStyle = UIModalPresentationStyle.overCurrentContext
    self.present(pvc, animated: true, completion: nil)
}

Go to StoryBoard select subViewController and add a UIView in it.

For blur effect set its constraint to

(top:0,Bottom:0,Leading:0,Trailing:0)

for all sides and change its color to black with the alpha you want.

And after that add a other UIView for options, set its constraints to

(top:-,Bottom:0,Leading:0,Trailing:0)

Set its height constraint to equal height with superview(self.View) and change its multipler to 0.33 or 0.34.

Tomerikoo
  • 18,379
  • 16
  • 47
  • 61
Abdul Rehman
  • 2,386
  • 1
  • 20
  • 34
6

Thanks to @aheze and @Sreekuttan Provides iOS15 solutions

I provide iOS13-15 solutions

iOS15:

Use UISheetPresentationController

    let vc = HalfScreenVC()
    if #available(iOS 15.0, *) {
        if let sheet = vc.sheetPresentationController {
            sheet.detents = [.medium()]
            sheet.preferredCornerRadius = 20
        }
    }
    present(vc, animated: true, completion: nil)

iOS13-iOS14

 class HalfScreenVC: UIViewController {
        
        override func viewDidLoad() {
            super.viewDidLoad()
            view.backgroundColor = .red
        }
        
        override func viewDidLayoutSubviews() {
            super.viewDidLayoutSubviews()
            
            if #available(iOS 15.0, *) {
                
            } else {
                // Below iOS 15, change frame here
                self.view.frame = CGRect(x: 0, y: UIScreen.main.bounds.height / 5 * 2, width: self.view.bounds.width, height: UIScreen.main.bounds.height / 5 * 3)
                self.view.layer.cornerRadius = 20
                self.view.layer.masksToBounds = true
            }
        } 

    }

iPhone 13.0 - 15.4 enter image description here iPad 13.0 - 15.4 enter image description here

If we use textfield, we want to type in full screen

    class HalfPresentVC: UIViewController {
        
        let textfield = UITextField(frame: CGRect(x: 20, y: 30, width: 200, height: 40))
        var initialBounds: CGRect?
        
        override func viewDidLoad() {
            super.viewDidLoad()
            view.backgroundColor = .red
            
            textfield.borderStyle = .roundedRect
            textfield.placeholder = "enter text"
            view.addSubview(textfield)
        }
        
        override func viewDidLayoutSubviews() {
            super.viewDidLayoutSubviews()
            if #available(iOS 15.0, *) {
               
            } else if initialBounds == nil {
                NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name: UIResponder.keyboardWillShowNotification, object: nil)
                NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide), name: UIResponder.keyboardWillHideNotification, object: nil)
                
                initialBounds = view.bounds
                view.layer.cornerRadius = 20
                view.layer.masksToBounds = true
                view.frame = CGRect(x: 0, y: view.bounds.height / 5 * 2, width: view.bounds.width, height: view.bounds.height / 5 * 3)
            }
        }
        
        @objc func keyboardWillShow() {
            guard let initialBounds = initialBounds else {
                return
            }
            view.frame = initialBounds
        }
        
        @objc func keyboardWillHide() {
            guard let initialBounds = initialBounds else {
                return
            }
            view.frame = CGRect(x: 0, y: initialBounds.height / 5 * 2, width: initialBounds.width, height: initialBounds.height / 5 * 3)
        }
        
        deinit {
            NotificationCenter.default.removeObserver(self)
        }

    }

type full screen

wlixcc
  • 1,132
  • 10
  • 14
5

You can use a UIPresentationController to achieve this. Implement the UIViewControllerTransitioningDelegate method on presenting ViewController and return your PresentationController from delegate method

func presentationController(forPresented presented: UIViewController, 
                          presenting: UIViewController?, 
                              source: UIViewController) -> UIPresentationController? 

You can refer this answer which has similar requirement. Alternatively you can use UIView animation or embedded view controller as suggested in the other answers.

Edit:

sample project found in Github

https://github.com/martinnormark/HalfModalPresentationController

Community
  • 1
  • 1
Suhit Patil
  • 11,748
  • 3
  • 50
  • 60
1

If you want the functionality from the @DatForis answer but don't want to mess with overriding the presentation controller you can create a sort of obvious hack when using a full screen modal as shown here:

modal_example

What you can do is present a full screen modal just like you normally would, however when you design the modal create two top views, one for the modal and a second view that would be placed over the background. The view that you place over the background should be transparent so that you can see the view from your presenting view controller.

Then to dismiss it you'd do something like this:

@IBOutlet weak var transparentView: UIView!

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(false)

    let tapGesture = UITapGestureRecognizer(target: self, action: #selector(backgroundTapped(gesture:)))
    transparentView.addGestureRecognizer(tapGesture)
}

@objc func backgroundTapped(gesture: UIGestureRecognizer) {
    self.dismiss(animated: true, completion: nil)
}

Hope that makes sense / helps someone!

Braden Holt
  • 1,544
  • 1
  • 18
  • 32
  • It helped me created a custom modal "alert" using a full-screen view controller without the hassle of implementing a presentation controller. – Carl Smith Apr 03 '21 at 08:19
1

please write below code on presented view controller .

 override var popupPresentDuration: Double { return 0.30  }

override var popupDismissDuration: Double { return 0.30 }
override var popupHeight: CGFloat { return CGFloat(UIScreen.main.bounds.height - 100) }

It will work on all ios versions.

  • You forgot to mention that its a [pod](https://github.com/ergunemr/BottomPopup) 'BottomPopup' – arunit21 Dec 02 '21 at 18:15
  • I'm trying to present a UIColorPickerViewController. You're saying I *really* have to subclass that just to make it present properly? What a bummer. – clearlight May 09 '22 at 22:39
0

simply you can do the following first you will design your screen to be 2 parts as in my case I want to hide the topView and I made it 0.3 of my whole screen and this code will be inside the viewDidLoad function and then you should present your viewController and set the modalPresentationStyle to be equal .formSheet

    topView.backgroundColor = UIColor(white: 1, alpha: 0.000001)
    self.view.backgroundColor = UIColor(white: 1, alpha: 0.000001)
-1

You can also achieve the effect by presenting view controller modally and setting the presentation style property to over full screen, transition style property to cover vertical and setting the alpha component of view's background color to 0.1.

Pranav Pravakar
  • 205
  • 3
  • 7