how to present modal UIViewController dialog from

2020-06-30 01:14发布

问题:

How would one present a UIViewController (from Storyboard say) that is modal, and slides up from the bottom of the presenting view controller. Requirements would be:

  • slides up from bottom, with widths aligning with the width of the presenting view controller
  • does NOT take up whole screen or whole parent presenting view controller (rather only is as high as required to show itself)
  • can be shown within the context of a view controller which doesn't take the whole screen

回答1:

I do not use storyboards so I wrote it all out. You can copy paste this into a brand new project and run it to see it working.

Your PresentingController needs to conform to two things. The first protocol is: UIViewControllerTransitioningDelegate which allows the controller to provide a custom presenter (namely itself in our case below). Whatever you return here (be it self, or some other object) needs to conform to UIViewControllerAnimatedTransitioning and provide the custom animations. For this self-contained example, I chose the current viewController to be the presenter and animator.

Next, it needs to conform to protocol: UIViewControllerAnimatedTransitioning which provides the custom animation for any presenting or dismissing controllers.

In other words, when we present or dismiss a viewController, animateTransition from the UIViewControllerAnimatedTransitioning protocol will be called to determine how the child controller should animate into perspective or dismiss from the view-port.

Example (With Transition Animation):

//
//  ViewController.m
//  SO
//
//  Created by Brandon T on 2017-01-23.
//  Copyright © 2017 XIO. All rights reserved.
//

#import "ViewController.h"


//Some view controller that will be presented modally.
//I have coloured it red.
@interface ModalController : UIViewController
@end

@implementation ModalController
- (void)viewDidLoad {
    [super viewDidLoad];

    self.view.backgroundColor = [UIColor redColor];
}
@end



//The view controller that will present or dismiss some other view controller modally.
//I have coloured it white.
@interface ViewController () <UIViewControllerTransitioningDelegate, UIViewControllerAnimatedTransitioning>
@property (nonatomic, assign) bool presentingModalController;
@property (nonnull, nonatomic, strong) ModalController *modalController;
@property (nonnull, nonatomic, strong) UIButton *button;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    self.view.backgroundColor = [UIColor whiteColor];

    //For this example, I add a button to present and dismiss the redViewController.
    self.button = [[UIButton alloc] initWithFrame:CGRectMake(15, self.view.center.y - 100, self.view.frame.size.width - 30, 45)];
    [self.button setTitle:@"Present" forState:UIControlStateNormal];
    [self.button setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
    [self.button setBackgroundColor:[UIColor lightGrayColor]];
    [self.button.layer setCornerRadius:5.0];
    [self.button addTarget:self action:@selector(onButtonClicked:) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:self.button];


    //Create the redViewController and set its transitioning delegate to self (this controller will be providing the animation and presenter).
    //We also set the style to OverFullScreen because we don't want this controller to disappear.
    //When a view controller is presented, the one that presented it usually disappears or gets removed from the hierarchy until the child is dismissed. In the case of alerts, or controllers that need to display OVER the current one, we need to set the modalPresentationStyle.
    self.modalController = [[ModalController alloc] init];
    self.modalController.transitioningDelegate = self;
    self.modalController.modalPresentationStyle = UIModalPresentationOverFullScreen;
}


- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
}

- (void)onButtonClicked:(UIButton *)button {
    if (self.modalController.view.window == nil) {
        [self presentViewController:self.modalController animated:YES completion:nil];
        [self.button setTitle:@"Dismiss" forState:UIControlStateNormal];

        //not a good idea but meh.. I need to keep this example short.
        [self.view.window addSubview:self.button];
    }
    else {
        [self.modalController dismissViewControllerAnimated:YES completion:nil];
        [self.button setTitle:@"Present" forState:UIControlStateNormal];
        [self.view addSubview:self.button];
    }
}


//Custom Animations and Presenters.
- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source {
    self.presentingModalController = true; //We are presenting the controller.
    return self; //Who is animating it? We are.
}

- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed {
    self.presentingModalController = false; //We are dismissing the view controller.
    return self; //Who animated it? We did.
}

//How fast should it present? I chose 0.5 seconds.
- (NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext {
    return 0.5;
}

//The actual animation code.
- (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext {
    if (self.presentingModalController) { 
        //If we are presenting, we need to add the new controller's view as a sub-view.

        UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];

        //We need a starting frame for the animation.
        CGRect startingFrame = transitionContext.containerView.bounds;
        startingFrame.origin.y = startingFrame.size.height; //Starts from the bottom of the parent.
        startingFrame.size.height = 100; //Has a height of 100.

        //We need an end frame for the animation.
        CGRect finalFrame = transitionContext.containerView.bounds;
        finalFrame.origin.y = finalFrame.size.height - 100; //100 from the bottom of the parent.
        finalFrame.size.height = 100; //Present with a size of 100 height.

        //Add the controller's view as a subview of the context.
        [transitionContext.containerView addSubview:toViewController.view];
        [toViewController.view setFrame:startingFrame];

        //Start animating from "startFrame" --> "endFrame" with 0.5 seconds duration and no delay. I chose easeIn style.
        [UIView animateWithDuration:[self transitionDuration:transitionContext] delay:0.0 options:UIViewAnimationOptionCurveEaseIn animations:^{
            [toViewController.view setFrame:finalFrame];
        } completion:^(BOOL finished) {
            //We are finished animating, complete the transition!
            [transitionContext completeTransition:YES];
        }];
    }
    else {
        //If we are dismissing the view controller, we need to animate it down the screen and then remove its view from the context.

        UIViewController *fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];

        //We only need one frame. This is the first frame. We are animating from "endFrame" --> "startingFrame" (backwards/reverse animation).
        CGRect startingFrame = transitionContext.containerView.bounds;
        startingFrame.origin.y = startingFrame.size.height; //Starts from the bottom of the parent.
        startingFrame.size.height = 100; //Has a height of 100.


        //Start the animation with 0.5 seconds duration and I chose easeOut style.
        [UIView animateWithDuration:[self transitionDuration:transitionContext] delay:0.0 options:UIViewAnimationOptionCurveEaseOut animations:^{
            [fromViewController.view setFrame:startingFrame];
        } completion:^(BOOL finished) {

            //Remove the view controller's view from the context and complete the transition!
            [fromViewController.view removeFromSuperview];
            [transitionContext completeTransition:YES];
        }];
    }
}
@end

Example (Without Transition Animation):

//
//  ViewController.m
//  SO2
//
//  Created by Brandon Thomas on 2017-01-23.
//  Copyright © 2017 XIO. All rights reserved.
//

#import "ViewController.h"


@interface ModalController : UIViewController
@end

@implementation ModalController
- (void)viewDidLoad {
    [super viewDidLoad];

    self.view.backgroundColor = [UIColor redColor];
}
@end



@interface ViewController ()
@property (nonatomic, assign) bool presentingModalController;
@property (nonnull, nonatomic, strong) ModalController *modalController;
@property (nonnull, nonatomic, strong) UIButton *button;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    self.view.backgroundColor = [UIColor whiteColor];

    self.button = [[UIButton alloc] initWithFrame:CGRectMake(15, self.view.center.y - 100, self.view.frame.size.width - 30, 45)];
    [self.button setTitle:@"Present" forState:UIControlStateNormal];
    [self.button setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
    [self.button setBackgroundColor:[UIColor lightGrayColor]];
    [self.button.layer setCornerRadius:5.0];
    [self.button addTarget:self action:@selector(onButtonClicked:) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:self.button];


    self.modalController = [[ModalController alloc] init];
}


- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
}

- (void)onButtonClicked:(UIButton *)button {
    if (self.modalController.view.window == nil) {
        //Present
        CGRect startingFrame = self.view.bounds;
        startingFrame.origin.y = startingFrame.size.height; //Starts from the bottom of the parent.
        startingFrame.size.height = 100; //Has a height of 100.

        CGRect finalFrame = self.view.bounds;
        finalFrame.origin.y = finalFrame.size.height - 100; //100 from the bottom of the parent.
        finalFrame.size.height = 100; //Present with a size of 100 height.

        [self.modalController.view setFrame:startingFrame];

        [self.modalController willMoveToParentViewController:self];
        [self addChildViewController:self.modalController];
        [self.view addSubview:self.modalController.view];
        [self.modalController didMoveToParentViewController:self];

        [UIView animateWithDuration:0.5 animations:^{
            [self.modalController.view setFrame:finalFrame];
        } completion:^(BOOL finished) {

        }];
    }
    else {
        //Dismiss
        CGRect startingFrame = self.view.bounds;
        startingFrame.origin.y = startingFrame.size.height; //Starts from the bottom of the parent.
        startingFrame.size.height = 100; //Has a height of 100.

        [UIView animateWithDuration:0.5 animations:^{
            [self.modalController.view setFrame:startingFrame];
        } completion:^(BOOL finished) {
            [self.modalController.view removeFromSuperview];
            [self.modalController willMoveToParentViewController:nil];
            [self.modalController removeFromParentViewController];
            [self.modalController didMoveToParentViewController:nil];
        }];
    }
}
@end


回答2:

Check out the Apple documentation for this:

Presenting a View Controller Using Custom Animations

To present a view controller using custom animations, do the following in an action method of your existing view controllers:

Create the view controller that you want to present. Create your custom transitioning delegate object and assign it to the view controller’s transitioningDelegate property. The methods of your transitioning delegate should create and return your custom animator objects when asked. Call the presentViewController:animated:completion: method to present the view controller. When you call the presentViewController:animated:completion: method, UIKit initiates the presentation process. Presentations start during the next run loop iteration and continue until your custom animator calls the completeTransition: method. Interactive transitions allow you to process touch events while the transition is ongoing, but noninteractive transitions run for the duration specified by the animator object.

EDIT:

Your alternative option is to create a container with your custom sizes and animate your UIViewController added as a child view of your UIViewController:

[self addChildViewController:content];
content.view.frame = [self frameForContentController];
[self.view addSubview:self.currentClientView];
[content didMoveToParentViewController:self];

Taken from this Thread