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.
No comments:
Post a Comment