How to present a ViewController on Half screen
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 |
---|---|
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
Comments
-
Umair Afzal over 2 years
I have a
UIViewController
which have only aUIView
which covers 1/3 of the viewController from bottom. Like thisI 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 ?