Present modal view controller in half size parent controller
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)
}
}
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.
It would be a 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:
All code is published at my Github
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!