14

How would I go about presenting a "half view" controller over the top of main view controller?

Requirements: - Present a second view controller that slides over the top of main view controller. - Second view controller should only show over half of the main view controller - Main view controller should remain visible behind second view controller (transparent background, not showing black underneath) - Second view controller should animate in with animation similar to modal vertical cover, or iOS 7 custom transition - User can still interact with buttons on main view controller when second view controller is active (i.e. second view controller does not cover the entire main view controller)r - Second view controller has its own complex logic (cannot be a simple view) - Storyboards, Segues, iOS 7 only - iPhone only, not iPad.

I have tried with modal view controller, but this does not allow interaction with main view controller. Can someone provide an example of how to do this with iOS7 custom transition or another method.

johnsampson
  • 165
  • 1
  • 1
  • 6
  • Have a look at UIActionSheet , that might solve your problem . Here is the link you can start off with : http://eureka.ykyuen.info/2010/04/14/iphone-uiactionsheet-example/ – Syed Shoaib Abidi Apr 05 '14 at 00:38

2 Answers2

26

One way to do it is to add the "half modal" controller as a child view controller, and animate its view into place. For this example, I created the "half modal" controller in the storyboard with a frame that's half the height of a 4" iPhone screen. You could use more dynamic methods to account for different screen sizes, but this should get you started.

@interface ViewController ()
@property (strong,nonatomic) UIViewController *modal;
@end

@implementation ViewController


- (IBAction)toggleHalfModal:(UIButton *)sender {
    if (self.childViewControllers.count == 0) {
        self.modal = [self.storyboard instantiateViewControllerWithIdentifier:@"HalfModal"];
        [self addChildViewController:self.modal];
        self.modal.view.frame = CGRectMake(0, 568, 320, 284);
        [self.view addSubview:self.modal.view];
        [UIView animateWithDuration:1 animations:^{
            self.modal.view.frame = CGRectMake(0, 284, 320, 284);;
        } completion:^(BOOL finished) {
            [self.modal didMoveToParentViewController:self];
        }];
    }else{
        [UIView animateWithDuration:1 animations:^{
            self.modal.view.frame = CGRectMake(0, 568, 320, 284);
        } completion:^(BOOL finished) {
            [self.modal.view removeFromSuperview];
            [self.modal removeFromParentViewController];
            self.modal = nil;
        }];
    }
}
rdelmar
  • 103,982
  • 12
  • 207
  • 218
  • 1
    This works well. Good solution. What would be the best way to dismiss this modal from *within* the modal view controller? (not a toggle from main view controller) – johnsampson Apr 05 '14 at 02:27
  • @johnsampson, you can get a pointer to the main view controller with self.parentViewController, and use that to call toggleHalfModal. – rdelmar Apr 05 '14 at 02:51
  • sorry. I am a real beginner. I got the modal working thanks to your code. How would I call toggleHalfModal from the modal using self.parentViewController? – johnsampson Apr 05 '14 at 03:12
  • 1
    @johnsampson, the same way you call any method -- [objectToSendMessageTo message]; In this case the object you want to send the message to is self.parentViewController (you'll have to cast it so the complier knows what it is, and import its .h file). So that gives you [(MainViewController *)self.parentViewController toggleHalfModal]; Replace MainViewController with whatever the class name is of that controller. – rdelmar Apr 05 '14 at 05:01
  • Thank you @rdelmar, it was most useful and worked perfectly, I have a question though, how can I make the rest of screen (the screen area - the modalview area) opaque? similar to how the background gets opaque when a UIAlertView or UIActionSheet is shown? Many thanks in advance – Septronic Aug 22 '14 at 14:20
  • 2
    @Septronic, One way would be to add a semi-opaque (black with a low alpha value) view overtop the original view before animating in the new view. – rdelmar Aug 22 '14 at 14:51
  • @rdelmar Thanks for the suggestion. It worked perfect with my containerview in the middle :) – Septronic Aug 24 '14 at 18:43
  • Thanks rdelmar! A very nice and elegant solution! – Ragen Dazs Apr 16 '15 at 00:43
  • @rdelmar or maybe someone else, can you explain the necessity/benefit of the child viewController here? as in, can't you just animate a UIView without a second viewController around? – manroe Apr 01 '16 at 18:46
-1

The new way to show controller in half screen is ios native style controller.sheetPresentationController but this work only for ios 15 and later, for previously versions you need to set .custom modalPresentationStype

Below code snippet help you for both versions

    controller.modalPresentationStyle = .pageSheet
    if #available(iOS 15.0, *) {

        if let sheet = controller.sheetPresentationController {
            sheet.detents = [.medium()]
        }
        
    } else {
        
        controller.modalPresentationStyle = .custom
        controller.transitioningDelegate = self
    }
    self.present(controller, animated: true, completion: nil)

// MARK: - UIViewControllerTransitioningDelegate

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

and Add Presentation controller as given

  class PresentationController: UIPresentationController {


  let blurEffectView: UIVisualEffectView!
  var tapGestureRecognizer: UITapGestureRecognizer = UITapGestureRecognizer()
  
  override init(presentedViewController: UIViewController, presenting presentingViewController: UIViewController?) {
      let blurEffect = UIBlurEffect(style: .dark)
      blurEffectView = UIVisualEffectView(effect: blurEffect)
      super.init(presentedViewController: presentedViewController, presenting: presentingViewController)
      tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(dismissController))
      blurEffectView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
      self.blurEffectView.isUserInteractionEnabled = true
      self.blurEffectView.addGestureRecognizer(tapGestureRecognizer)
  }
  
  override var frameOfPresentedViewInContainerView: CGRect {
      CGRect(origin: CGPoint(x: 0, y: self.containerView!.frame.height * 0.4),
             size: CGSize(width: self.containerView!.frame.width, height: self.containerView!.frame.height *
              0.6))
  }

  override func presentationTransitionWillBegin() {
      self.blurEffectView.alpha = 0
      self.containerView?.addSubview(blurEffectView)
      self.presentedViewController.transitionCoordinator?.animate(alongsideTransition: { (UIViewControllerTransitionCoordinatorContext) in
          self.blurEffectView.alpha = 0.7
      }, completion: { (UIViewControllerTransitionCoordinatorContext) in })
  }
  
  override func dismissalTransitionWillBegin() {
      self.presentedViewController.transitionCoordinator?.animate(alongsideTransition: { (UIViewControllerTransitionCoordinatorContext) in
          self.blurEffectView.alpha = 0
      }, completion: { (UIViewControllerTransitionCoordinatorContext) in
          self.blurEffectView.removeFromSuperview()
      })
  }
  
  override func containerViewWillLayoutSubviews() {
      super.containerViewWillLayoutSubviews()
    presentedView!.roundCorners([.topLeft, .topRight], radius: 22)
  }

  override func containerViewDidLayoutSubviews() {
      super.containerViewDidLayoutSubviews()
      presentedView?.frame = frameOfPresentedViewInContainerView
      blurEffectView.frame = containerView!.bounds
  }

  @objc func dismissController(){
      self.presentedViewController.dismiss(animated: true, completion: nil)
  }
}

extension UIView {
  func roundCorners(_ corners: UIRectCorner, radius: CGFloat) {
      let path = UIBezierPath(roundedRect: bounds, byRoundingCorners: corners,
                              cornerRadii: CGSize(width: radius, height: radius))
      let mask = CAShapeLayer()
      mask.path = path.cgPath
      layer.mask = mask
  }
}
Jagveer Singh
  • 2,258
  • 19
  • 34