Walkthrough to make a secondary scene in a storyboard act as an animated modal dialog, similar to the Alert view.
Prerequisites:
1. Create a single-view app project.
2. In Project Settings/General/Deployment Info make sure the storyboard name (Main.storyboard) appears in Main Interface popover.
3. Add/remove remove view controllers (VCs) to the storyboard as needed.
4. To make a VC the very first controller - click it to highlight blue, then in Attributes Inspector check the Initial Scene. This will set origin arrow.
5. Create a modal Manual Segue ("seg-way"). In the IB zoom out by double-click on an empty space. Then Ctrl-drag from the presenting VC to the modal VC. Select "modal" in a dialog.
6. To make sure the transition is animated select the segue, check the Animates attribute in the Attribute Inspector.
Now, to use the default animation provided by Xcode - do the following:
1. Add a new View Controller (VC), set it as a CustomClass/Class attribute in Identity Inspector for the presenting view (from-view). Add a button and a tap event handler to the presenting view.
//
// ALYViewController.m
//
#import "ALYViewController.h"
#import "ALYZoomAnimator.h"
NSString *const MySegueID = @"MySegueID";
@interface ALYViewController ()
@property (strong, nonatomic) ALYZoomAnimator *zoomAnimator;
- (IBAction)showModalViewTapped:(id)sender;
@end
@implementation ALYViewController
- (IBAction)showModalViewTapped:(id)sender {
[self performSegueWithIdentifier: MySegueID sender: nil];
}
@end
2. Do the same for the modal view, except the code will be dismissing this dialog.
//
// ALYModalViewController.m
//
#import "ALYModalViewController.h"
@interface ALYModalViewController ()
- (IBAction)dismissTapped:(id)sender;
@end
@implementation ALYModalViewController
- (IBAction)dismissTapped:(id)sender {
[self dismissViewControllerAnimated: YES completion: nil];
}
@end
3. Run the app. Click the button to reveal the modal dialog.
To add a custom transition with added benefit of controlling size and appearance of the modal dialog (for interactivity, cancellation, etc. in later posts), do the following:
1. Implement <UIViewControllerTransitioningDelegate> and <UIViewControllerAnimatedTransitioning> in a separate animator class. This will handle in/out transitions for a modal dialog. Unfortunately there is no context property to tell which transition is being initiated, in or out. One workaround is to record a VC being presented and then compare it against the to-VC of the transitionContext.
// // ALYZoomAnimator.h // #import <Foundation/Foundation.h> @interface ALYZoomAnimator : NSObject <UIViewControllerTransitioningDelegate, UIViewControllerAnimatedTransitioning> @end
//
// ALYZoomAnimator.m
//
#import "ALYZoomAnimator.h"
#define IN_DURATION 1.0
#define OUT_DURATION 0.3
@interface ALYZoomAnimator()
@property (weak, nonatomic) UIViewController *presentedVC;
@end
@implementation ALYZoomAnimator
// Returns true while the IN animation is active to reveal the modal sub-dialog.
- (BOOL) isBeingPresented: (id<UIViewControllerContextTransitioning>) transitionContext
{
UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
return toVC == self.presentedVC;
}
#pragma mark - <UIViewControllerTransitioningDelegate>
- (id<UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source
{
self.presentedVC = presented;
return self;
}
- (id<UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed
{
return self;
}
#pragma mark - UIViewControllerAnimatedTransitioning
- (NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext
{
return [self isBeingPresented: transitionContext] ? IN_DURATION : OUT_DURATION;
}
- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
UIView *container = transitionContext.containerView;
UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
UIView *fromView = fromVC.view;
UIView *toView = toVC.view;
CGRect cb = container.bounds;
CGRect startFrame = CGRectMake(cb.size.width / 2, cb.size.height / 2 - 50, 0, 0);
CGRect endFrame = CGRectInset(startFrame, -100, -100);
UIView *snapshot;
if ([self isBeingPresented: transitionContext]) {
fromView.userInteractionEnabled = NO;
fromView.tintColor = [UIColor grayColor];
toView.frame = endFrame;
snapshot = [toView snapshotViewAfterScreenUpdates:YES];
snapshot.frame = startFrame;
snapshot.alpha = 0.0;
[container addSubview: snapshot];
[UIView animateWithDuration: [self transitionDuration: transitionContext]
delay: 0
usingSpringWithDamping: 500 initialSpringVelocity: 15
options: 0
animations: ^{
fromView.tintAdjustmentMode = UIViewTintAdjustmentModeDimmed;
snapshot.frame = endFrame;
snapshot.alpha = 1.0;
}
completion: ^(BOOL finished) {
toView.frame = endFrame;
[container addSubview: toView];
[snapshot removeFromSuperview];
[transitionContext completeTransition:YES];
}];
} else {
snapshot = [fromView snapshotViewAfterScreenUpdates:YES];
snapshot.frame = endFrame;
[container addSubview: snapshot];
[fromView removeFromSuperview];
[UIView animateWithDuration: [self transitionDuration: transitionContext]
delay: 0
usingSpringWithDamping: 500 initialSpringVelocity: 15
options: 0
animations: ^{
toView.tintAdjustmentMode = UIViewTintAdjustmentModeNormal;
snapshot.frame = startFrame;
snapshot.alpha = 0.0;
}
completion: ^(BOOL finished) {
[snapshot removeFromSuperview];
toView.userInteractionEnabled = YES;
[transitionContext completeTransition:YES];
}];
}
}
@end
2. Provide -prepareForSegue method in the presenting VC.
//
// ALYViewController.m
//
#import "ALYViewController.h"
#import "ALYZoomAnimator.h"
NSString *const MySegueID = @"MySegueID";
@interface ALYViewController ()
@property (strong, nonatomic) ALYZoomAnimator *zoomAnimator;
- (IBAction)showModalViewTapped:(id)sender;
@end
@implementation ALYViewController
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if (![segue.identifier isEqualToString: MySegueID]) {
return;
}
if (!self.zoomAnimator) {
self.zoomAnimator = [[ALYZoomAnimator alloc] init];
}
[segue.destinationViewController setTransitioningDelegate: self.zoomAnimator];
[segue.destinationViewController setModalPresentationStyle: UIModalPresentationCustom];
}
- (IBAction)showModalViewTapped:(id)sender {
[self performSegueWithIdentifier: MySegueID sender: nil];
}
@end
3. Run the app. Observe the modal dialog animations, transparency, presenting view in the background providing a context to help users keep track of their position in app. This is unlike the normal transitions that will hide the presenting view.





