How to present a ViewController on Half screen

47,442

Solution 1

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

Solution 2

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

Solution 3

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.

Solution 4

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.

Solution 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

Share:
47,442
Umair Afzal
Author by

Umair Afzal

Happy Coding :-)

Updated on December 22, 2021

Comments

  • Umair Afzal
    Umair Afzal over 2 years

    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 ?