58

I am trying to present modal view controller on other viewcontroller sized to half parent view controller. But it always present in full screen view.

I have created freeform sized View controller in my storyboard with fixed frame size. 320 X 250.

var storyboard = UIStoryboard(name: "Main", bundle: nil)
var pvc = storyboard.instantiateViewControllerWithIdentifier("CustomTableViewController") as ProductsTableViewController
self.presentViewController(pvc, animated: true, completion: nil)

I have tried to set frame.superview and it doesn't help.

Picture example

Please advice.

Beau Nouvelle
  • 6,962
  • 3
  • 39
  • 54
Anton
  • 3,102
  • 2
  • 28
  • 47

10 Answers10

97

You can use a UIPresentationController to achieve this.

For this you let the presenting ViewController implement the UIViewControllerTransitioningDelegate and return your PresentationController for the half sized presentation:

func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
    return HalfSizePresentationController(presentedViewController: presented, presenting: presentingViewController)
}

When presenting you set the presentation style to .Custom and set your transitioning delegate:

pvc.modalPresentationStyle = .custom
pvc.transitioningDelegate = self

The presentation controller only returns the frame for your presented view controller:

class HalfSizePresentationController: UIPresentationController {
    override var frameOfPresentedViewInContainerView: CGRect {
        guard let bounds = containerView?.bounds else { return .zero }
        return CGRect(x: 0, y: bounds.height / 2, width: bounds.width, height: bounds.height / 2)
    }
}

Here is the working code in its entirety:

class ViewController: UIViewController, UIViewControllerTransitioningDelegate {

    @IBAction func tap(sender: AnyObject) {
        let storyboard = UIStoryboard(name: "Main", bundle: nil)
        let pvc = storyboard.instantiateViewController(withIdentifier: "CustomTableViewController") as! UITableViewController

        pvc.modalPresentationStyle = .custom
        pvc.transitioningDelegate = self
        pvc.view.backgroundColor = .red

        present(pvc, animated: true)
    }
    
    func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
        return HalfSizePresentationController(presentedViewController: presented, presenting: presentingViewController)
    }
}

class HalfSizePresentationController: UIPresentationController {
    override var frameOfPresentedViewInContainerView: CGRect {
        guard let bounds = containerView?.bounds else { return .zero }
        return CGRect(x: 0, y: bounds.height / 2, width: bounds.width, height: bounds.height / 2)
    }
}

DZoki019
  • 382
  • 2
  • 13
Jannis
  • 1,084
  • 9
  • 8
  • 6
    Thanks. it works. Now I need to dismiss my second view controller when user clicks on the parent view controller. But i can't get the tapping gesture in my parent view controller. Can please you guide me to solve this? – BB_Dev Jul 08 '15 at 05:32
  • Thanks @Jannis.. btw, I would like to place a circular button onto this second viewcontroller with 50% of the height of the button on the transparent view and 50% on for your example the red part. What would be your advice? – Stan92 Nov 25 '15 at 06:58
  • Doesn't work in iOS9: `fatal error: unexpectedly found nil while unwrapping an Optional value` in line `return HalfSizePresentationController(presentedViewController: presented, presentingViewController: presentingViewController!)` – TomSawyer Mar 10 '16 at 19:56
  • 3
    I edited the answer to fix the iOS 9 error, based on solution in [this thread](https://forums.developer.apple.com/thread/6129) – phatmann Mar 25 '16 at 23:50
  • Still not working in iOS10. Same error as stated above for iOS9. Fix it by using 'source' instead of 'presentingViewController' when calling 'HalfSizePresentationController()' – Zvi Sep 02 '16 at 22:51
  • @BB_Dev Did you solved getting the tapping gesture on the parent view? – Toydor Oct 18 '16 at 13:17
  • In `frameOfPresentedViewInContainerView` the `y` paramter should also be set to `height / 2`, otherwise the view is shown at the top. – mojuba Jan 31 '18 at 21:01
  • 1
    Renamed ```func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController?``` – Rachit Mishra Aug 25 '18 at 10:44
  • Note that `func frameOfPresentedViewInContainerView` is now `var frameOfPresentedViewInContainerView` and that rather than `containerView` one can use `presentingViewController.view` which is an inherit property of `UIPresentationController`. – pinglock Apr 09 '21 at 19:28
  • @Janis, thanks, may I ask how to set pop up container view as rounded corner? Like the default modelPresentView style. Also, after implement above code, drag on the popped model view would not take any effect. – Zhou Haibo Jun 13 '21 at 11:34
61

It would be a clean architect if you push some delegate methods of UIViewControllerTransitioningDelegate in your ViewController that want to be presented as half modal.

Assuming we have ViewControllerA present ViewControllerB with half modal.

in ViewControllerA just present ViewControllerB with custom modalPresentationStyle

func gotoVCB(_ sender: UIButton) {
    let vc = ViewControllerB()
    vc.modalPresentationStyle = .custom
    present(vc, animated: true, completion: nil)
}

And in ViewControllerB:

import UIKit

final class ViewControllerB: UIViewController {

lazy var backdropView: UIView = {
    let bdView = UIView(frame: self.view.bounds)
    bdView.backgroundColor = UIColor.black.withAlphaComponent(0.5)
    return bdView
}()

let menuView = UIView()
let menuHeight = UIScreen.main.bounds.height / 2
var isPresenting = false

init() {
    super.init(nibName: nil, bundle: nil)
    modalPresentationStyle = .custom
    transitioningDelegate = self
}

required init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
}

override func viewDidLoad() {
    super.viewDidLoad()
    
    view.backgroundColor = .clear
    view.addSubview(backdropView)
    view.addSubview(menuView)
    
    menuView.backgroundColor = .red
    menuView.translatesAutoresizingMaskIntoConstraints = false
    menuView.heightAnchor.constraint(equalToConstant: menuHeight).isActive = true
    menuView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
    menuView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
    menuView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
    
    let tapGesture = UITapGestureRecognizer(target: self, action: #selector(ViewControllerB.handleTap(_:)))
    backdropView.addGestureRecognizer(tapGesture)
}

@objc func handleTap(_ sender: UITapGestureRecognizer) {
    dismiss(animated: true, completion: nil)
}
}

extension ViewControllerB: UIViewControllerTransitioningDelegate, UIViewControllerAnimatedTransitioning {
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
    return self
}

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

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

func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
    let containerView = transitionContext.containerView
    let toViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)
    guard let toVC = toViewController else { return }
    isPresenting = !isPresenting
    
    if isPresenting == true {
        containerView.addSubview(toVC.view)
        
        menuView.frame.origin.y += menuHeight
        backdropView.alpha = 0
        
        UIView.animate(withDuration: 0.4, delay: 0, options: [.curveEaseOut], animations: {
            self.menuView.frame.origin.y -= self.menuHeight
            self.backdropView.alpha = 1
        }, completion: { (finished) in
            transitionContext.completeTransition(true)
        })
    } else {
        UIView.animate(withDuration: 0.4, delay: 0, options: [.curveEaseOut], animations: {
            self.menuView.frame.origin.y += self.menuHeight
            self.backdropView.alpha = 0
        }, completion: { (finished) in
            transitionContext.completeTransition(true)
        })
    }
}
}

The result:

enter image description here

All code is published on my Github

Twitter khuong291
  • 11,328
  • 15
  • 80
  • 116
  • 1
    Thank you! It's the best one implementation! – Włodzimierz Woźniak Nov 10 '18 at 20:34
  • 1
    this 100% works but on iOS 13 the presentation and dismissal transitions are no where as as smooth as your gif, they are both very sudden. But other than that it is perfect! – Lance Samaria Jul 23 '20 at 18:27
  • 1
    I'm getting this error `"init(coder:) has not been implemented"` – OhhhThatVarun Nov 11 '20 at 17:44
  • 1
    This is by far the cleanest approach I have come across. All is done programmatically. It's fully customizable in any way shape and form. I do thank you for the time and effort you put into this and also for share with us. – Jiraheta Aug 26 '21 at 22:36
41

Just in case someone is looking to do this with Swift 4, as I was.

class MyViewController : UIViewController {
    ...
    @IBAction func dictionaryButtonTouchUp(_ sender: UIButton) {
        let modalViewController = ...
        modalViewController.transitioningDelegate = self
        modalViewController.modalPresentationStyle = .custom

        self.present(modalViewController, animated: true, completion: nil)
    }
}

extension MyViewController : UIViewControllerTransitioningDelegate {
    func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
        return HalfSizePresentationController(presentedViewController: presented, presenting: presenting)
    }
}

Where the HalfSizePresentationController class is composed of:

class HalfSizePresentationController : UIPresentationController {
    override var frameOfPresentedViewInContainerView: CGRect {
        get {
            guard let theView = containerView else {
                return CGRect.zero
            }

            return CGRect(x: 0, y: theView.bounds.height/2, width: theView.bounds.width, height: theView.bounds.height/2)
        }
    }
}

Cheers!

Francois Nadeau
  • 7,023
  • 2
  • 49
  • 58
10

Jannis captured the overall strategy well. It didn't work for me in iOS 9.x with swift 3. On the presenting VC, the action to launch the presented VC is similar to what was presented above with some very minor changes as below:

let storyboard = UIStoryboard(name: "Main", bundle: nil)
let pvc = storyboard.instantiateViewController(withIdentifier: "SomeScreen") as SomeViewController

pvc.modalPresentationStyle = .custom
pvc.transitioningDelegate = self

present(pvc, animated: true, completion: nil)

To implement UIViewControllerTransitioningDelegate on the same presenting VC, the syntax is quite different as highlighted in SO answer in https://stackoverflow.com/a/39513247/2886158. This is was the most tricky part for me. Here is the protocol implementation:

func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
    return HalfSizePresentationController(presentedViewController:presented, presenting: presenting)
}

For the UIPresentationController class, I had to override the variable frameOfPresentedViewInContainerView, not method, as below:

class HalfSizePresentationController: UIPresentationController {
    override var frameOfPresentedViewInContainerView: CGRect {
        return CGRect(x: 0, y: 0, width: containerView!.bounds.width, height: containerView!.bounds.height/2)
    }
}

There were some questions about how to dismiss the view after presentation. You can implement all the usual logic on your presented VC like any other VC. I implementation an action to dismiss the view in SomeViewController when a user tabs outside the presented VC.

Heelara
  • 861
  • 9
  • 17
  • 1
    I think the y coordinate of the HalfSizeController should be started from half screen, it’ll be like this: ```return CGRect(x: 0, y: (containerView!.bounds.height/2), width: containerView!.bounds.width, height: containerView!.bounds.height/2``` – Farras Doko May 28 '20 at 13:31
6

Details

  • Xcode 12.2 (12B45b)
  • Swift 5.3

Solution 1. Default transition

Idea:

Hide root view of the ChildViewController and add new view that will be used as the root view.

Main logic:

class ChildViewController: UIViewController { 
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .clear

        let contentView = UIView()
        contentView.backgroundColor = .lightGray
        view.addSubview(contentView)
        //...
    }
}

Solution 1. Full sample

import UIKit

// MARK: ParentViewController

class ParentViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        let button = UIButton(frame: CGRect(x: 50, y: 50, width: 200, height: 60))
        button.setTitle("Present VC", for: .normal)
        button.setTitleColor(.blue, for: .normal)
        button.addTarget(self, action: #selector(touchedUpInside), for: .touchUpInside)
        view.addSubview(button)
    }

    @objc func touchedUpInside(source: UIButton) {
        let viewController = ChildViewController()
        present(viewController, animated: true, completion: nil)
    }
}

// MARK: ChildViewController

class ChildViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .clear

        let contentView = UIView()
        contentView.backgroundColor = .lightGray
        view.addSubview(contentView)

        contentView.translatesAutoresizingMaskIntoConstraints = false
        contentView.heightAnchor.constraint(equalTo: view.heightAnchor, multiplier: 0.5).isActive = true
        contentView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor).isActive = true
        contentView.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor).isActive = true
        contentView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true
    }
}

Solution 2. Custom transition

Idea:

Change size of the root view of the ChildViewController.

Main logic:

ModalPresentationController

protocol ModalPresentationControllerDelegate: class {
    func updateFrameOfPresentedViewInContainerView(frame: CGRect) -> CGRect
}

class ModalPresentationController: UIPresentationController {
    private weak var modalPresentationDelegate: ModalPresentationControllerDelegate!

    convenience
    init(delegate: ModalPresentationControllerDelegate,
         presentedViewController: UIViewController,
         presenting presentingViewController: UIViewController?) {
        self.init(presentedViewController: presentedViewController,
                  presenting: presentingViewController)
        self.modalPresentationDelegate = delegate
    }

    override var frameOfPresentedViewInContainerView: CGRect {
        get { modalPresentationDelegate.updateFrameOfPresentedViewInContainerView(frame: super.frameOfPresentedViewInContainerView) }
    }
}

Update root view size

class ChildViewController: UIViewController {
    init() {
        //...
        transitioningDelegate = self
        modalPresentationStyle = .custom
    }
}

extension ChildViewController: UIViewControllerTransitioningDelegate {
    func presentationController(forPresented presented: UIViewController,
                                presenting: UIViewController?,
                                source: UIViewController) -> UIPresentationController? {
        ModalPresentationController(delegate: self, presentedViewController: presented, presenting: presenting)
    }
}

extension ChildViewController: ModalPresentationControllerDelegate {
    func updateFrameOfPresentedViewInContainerView(frame: CGRect) -> CGRect {
        CGRect(x: 0, y: frame.height/2, width: frame.width, height: frame.height/2)
    }
}

Solution 2. Full sample

Do not forget to paste here ModalPresentationController that defined above

import UIKit

// MARK: ParentViewController

class ParentViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        let button = UIButton(frame: CGRect(x: 50, y: 50, width: 200, height: 60))
        button.setTitle("Present VC", for: .normal)
        button.setTitleColor(.blue, for: .normal)
        button.addTarget(self, action: #selector(touchedUpInside), for: .touchUpInside)
        view.addSubview(button)
    }

    @objc func touchedUpInside(source: UIButton) {
        let viewController = ChildViewController()
        present(viewController, animated: true, completion: nil)
    }
}

// MARK: ChildViewController

class ChildViewController: UIViewController {
    init() {
        super.init(nibName: nil, bundle: nil)
        transitioningDelegate = self
        modalPresentationStyle = .custom
        view.backgroundColor = .lightGray
    }

    required init?(coder: NSCoder) { super.init(coder: coder) }
}

extension ChildViewController: UIViewControllerTransitioningDelegate {
    func presentationController(forPresented presented: UIViewController,
                                presenting: UIViewController?,
                                source: UIViewController) -> UIPresentationController? {
        ModalPresentationController(delegate: self, presentedViewController: presented, presenting: presenting)
    }
}

extension ChildViewController: ModalPresentationControllerDelegate {
    func updateFrameOfPresentedViewInContainerView(frame: CGRect) -> CGRect {
        CGRect(x: 0, y: frame.height/2, width: frame.width, height: frame.height/2)
    }
}
Vasily Bodnarchuk
  • 24,482
  • 9
  • 132
  • 127
  • 1
    Second example is pretty clever. It's a bit complicated over the usual UIPresentationController subclassing, which is where one generally sets/adjusts size of presented VC, where you use a delegate to let the presented VC set its own size. So you've made it a more flexible generic solution, which makes it harder to understand for a noobie, but it is very well thought out. – clearlight Sep 27 '22 at 21:43
  • I encourage anyone new to this to look at various examples of subclassing UIPresentationViewController to get an idea how the basic/minimal stuff is usually handled before trying to understand this example, which requires a pretty solid grasp of everything involved. – clearlight Sep 27 '22 at 21:44
3

Starting with iOS 15, UISheetPresentationController now has a medium appearance that presents the view controller for half of the screen.

Curiosity
  • 544
  • 1
  • 15
  • 29
  • 1
    This should be accepted answer; – mehdok Mar 18 '22 at 14:56
  • 1
    Exactly. BTW: As of iOS 16 you can create custom detent sizes (e.g. not just stuck with `.medium()` and `.large()`). Note: Custom detents *do* automatically accommodate soft-keyboard (e.g. will shift presented VC vertically when kb appears), as UISheetController manual page saids .medium() detent does) For iOS 15, custom detents *are* possible using UISheetController subclass using *private* method & _detent property. For iOS 15. it is probably better, if you need custom detent to subclass UIPresentationController subclass & set size in `frameOfPresentedViewInContainerView` property – clearlight Sep 27 '22 at 21:52
1

Here is Swift 4.0 some class name is change frameOfPresentedViewInContainerView get method

Step 1: Set Delegate

class ViewController: UIViewController, UIViewControllerTransitioningDelegate 

Step 2: Set Delegate Method

func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
         return SetSizePresentationController(presentedViewController: presented, presenting: presenting)
}

Step 3: Here you can create your own Class for set size (CGRect)

class SetSizePresentationController : UIPresentationController {
    override var frameOfPresentedViewInContainerView: CGRect {
        get {
             return CGRect(x: 0, y: (containerView?.bounds.height ?? 0)/2, width: containerView?.bounds.width ?? 0, height: (containerView?.bounds.height ?? 0)/2)
        }
    }
}

Step 4: here 2 lines important transitioningdelegate & UIModalPresentationStyle.custom

let storyboard = UIStoryboard(name: "User", bundle: nil)
let vc = storyboard.instantiateViewController(withIdentifier: "LicenceViewController") as! LicenceViewController
vc.modalPresentationStyle = UIModalPresentationStyle.custom
vc.transitioningDelegate = self
present(vc, animated: true)
Harshil Kotecha
  • 2,846
  • 4
  • 27
  • 41
0

To add to Jannis' answer:

In case your pop-view is a UIViewController to which you ADD a Table on load/setup, you will need to ensure that the table frame you create matches the desired width of the actual view.

For example:

let tableFrame: CGRect = CGRectMake(0, 0, chosenWidth, CGFloat(numOfRows) * rowHeight)

where chosenWidth is the width you set in your custom class (in the above: containerView.bounds.width)

You do not need to enforce anything on the Cell itself as the table container (at least in theory) should force the cell to the right width.

zevij
  • 2,416
  • 1
  • 23
  • 32
0

I use below logic to present half screen ViewController

 let storyboard = UIStoryboard(name: "Main", bundle: nil)
    let expVC = storyboard.instantiateViewController(withIdentifier: "AddExperinceVC") as! AddExperinceVC
    expVC.modalPresentationStyle = UIModalPresentationStyle.overCurrentContext

    self.present(expVC, animated: true, completion: nil)
Josh Gray
  • 49
  • 3
0

Present normally, then use systemLayoutSizeFitting in viewDidLayoutSubviews to adjust the frame to the minimum required size. This retains the visuals and physics provided by Apple –which you will lose using a custom presentation.

See the sample code on this answer.

Jano
  • 62,815
  • 21
  • 164
  • 192