Present modal view controller in half size parent controller
Asked Answered
T

10

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.

Tribe answered 23/3, 2015 at 20:25 Comment(7)
have you tried setting the presentation style to over current context?Buller
@Buller yes I have tried each oneTribe
Have you messed around with the layout? Like changing it to regular, all, or compact.Buller
@Buller I have not use auto layout. Resize view from NIB is unchecked.Tribe
How did you get the view to work with keyboard up? Like in the title/location fields, where you would have to bring up the keyboard.Sanctum
@BlackFlam3 it's my really old question, I didn't check it so far. but resolved my issue without this solutuonTribe
I made a Github repo BonsaiController just for that.Pretender
F
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)
    }
}

Fifield answered 23/3, 2015 at 21:43 Comment(10)
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?Hartman
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?Mersey
Doesn't work in iOS9: fatal error: unexpectedly found nil while unwrapping an Optional value in line return HalfSizePresentationController(presentedViewController: presented, presentingViewController: presentingViewController!)Suhail
I edited the answer to fix the iOS 9 error, based on solution in this threadKeeshakeeshond
Still not working in iOS10. Same error as stated above for iOS9. Fix it by using 'source' instead of 'presentingViewController' when calling 'HalfSizePresentationController()'Hebe
@Hartman Did you solved getting the tapping gesture on the parent view?Megaton
In frameOfPresentedViewInContainerView the y paramter should also be set to height / 2, otherwise the view is shown at the top.Volturno
Renamed func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController?Neper
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.Winnie
@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.Allix
C
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

Corinecorinna answered 5/8, 2017 at 18:39 Comment(4)
Thank you! It's the best one implementation!Raster
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!Fissionable
I'm getting this error "init(coder:) has not been implemented"Shag
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.Matronna
T
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!

Tena answered 26/3, 2018 at 19:51 Comment(1)
@/francois-nadeau in your example how do you gray/opaque the 1/3 of the background view?Hyacinthus
M
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://mcmap.net/q/331382/-xcode-8-swift-3-modal-presentation-transitioning-delegate-not-called. 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.

Medium answered 18/9, 2017 at 12:53 Comment(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/2Hurtless
P
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)
    }
}
Pox answered 17/11, 2020 at 20:3 Comment(2)
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.Marquita
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.Marquita
V
3

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

Virgilvirgilia answered 3/10, 2021 at 12:46 Comment(2)
This should be accepted answer;Giantism
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 propertyMarquita
F
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)
Frutescent answered 8/7, 2020 at 22:13 Comment(1)
Which class is full screen and which one is half screen?Imperfection
D
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.

Dyne answered 14/1, 2016 at 18:27 Comment(0)
T
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)
Tinhorn answered 26/12, 2017 at 8:3 Comment(0)
B
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.

Bathometer answered 15/4, 2021 at 12:18 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.