| // Copyright 2017 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #import "ios/chrome/browser/ui/presenters/vertical_animation_container.h" |
| |
| #import "base/check.h" |
| #import "ios/chrome/browser/ui/presenters/contained_presenter_delegate.h" |
| #import "ios/chrome/common/ui/util/constraints_ui_util.h" |
| |
| #if !defined(__has_feature) || !__has_feature(objc_arc) |
| #error "This file requires ARC support." |
| #endif |
| |
| namespace { |
| NSTimeInterval kAnimationDuration = 0.2; |
| } |
| |
| @interface VerticalAnimationContainer () |
| @property(nonatomic) NSArray<NSLayoutConstraint*>* dismissedConstraints; |
| @property(nonatomic) NSArray<NSLayoutConstraint*>* presentedConstraints; |
| @end |
| |
| @implementation VerticalAnimationContainer |
| |
| // Synthesize ContainedPresenter properties. |
| @synthesize baseViewController = _baseViewController; |
| @synthesize presentedViewController = _presentedViewController; |
| @synthesize delegate = _delegate; |
| // Synthesize private properties. |
| @synthesize dismissedConstraints = _dismissedConstraints; |
| @synthesize presentedConstraints = _presentedConstraints; |
| |
| - (void)prepareForPresentation { |
| DCHECK(self.presentedViewController); |
| |
| [self.baseViewController addChildViewController:self.presentedViewController]; |
| [self.baseViewController.view addSubview:self.presentedViewController.view]; |
| |
| // Shorter names for more readable constraint code. |
| UIView* container = self.baseViewController.view; |
| UIView* contents = self.presentedViewController.view; |
| |
| // The contents view will be sized and positioned by constraints. |
| contents.translatesAutoresizingMaskIntoConstraints = NO; |
| |
| // The horizontal position of the contents in the container also doesn't |
| // change. |
| [contents.centerXAnchor constraintEqualToAnchor:container.centerXAnchor] |
| .active = YES; |
| |
| // When dismissed, the top of the contents is just below the bottom of the |
| // container. |
| self.dismissedConstraints = @[ |
| [contents.topAnchor constraintEqualToAnchor:container.bottomAnchor], |
| ]; |
| |
| // When presented, the bottom of the contents matches the bottom of the |
| // container. |
| self.presentedConstraints = @[ |
| [contents.bottomAnchor constraintEqualToAnchor:container.bottomAnchor], |
| ]; |
| |
| // The contents start off dismissed. |
| [NSLayoutConstraint activateConstraints:self.dismissedConstraints]; |
| |
| // Ensure the contents are actually positioned in the dismissed positions. |
| [self.baseViewController.view layoutIfNeeded]; |
| |
| [self.presentedViewController |
| didMoveToParentViewController:self.baseViewController]; |
| } |
| |
| - (void)presentAnimated:(BOOL)animated { |
| // An error if -prepareForPresentation hasn't been called yet. |
| // Simple coherence test: the presented view controller's view has a |
| // superview. |
| DCHECK(self.presentedViewController.view.superview); |
| |
| // No-op if already presented. |
| if (self.presentedConstraints[0].active) |
| return; |
| |
| auto animations = ^{ |
| [NSLayoutConstraint deactivateConstraints:self.dismissedConstraints]; |
| [NSLayoutConstraint activateConstraints:self.presentedConstraints]; |
| [self.baseViewController.view layoutIfNeeded]; |
| }; |
| auto completion = ^(BOOL finished) { |
| if ([self.delegate |
| respondsToSelector:@selector(containedPresenterDidPresent:)]) { |
| [self.delegate containedPresenterDidPresent:self]; |
| } |
| }; |
| |
| if (animated) { |
| [UIView animateWithDuration:kAnimationDuration |
| animations:animations |
| completion:completion]; |
| } else { |
| animations(); |
| completion(YES); |
| } |
| } |
| |
| - (void)dismissAnimated:(BOOL)animated { |
| DCHECK(self.presentedViewController); |
| // If animated, the base view controller must still be in the view hierarchy. |
| DCHECK(!animated || self.baseViewController.view.superview); |
| |
| // No-op if already dismissed. |
| if (self.dismissedConstraints[0].active) |
| return; |
| |
| auto animations = ^{ |
| [NSLayoutConstraint deactivateConstraints:self.presentedConstraints]; |
| [NSLayoutConstraint activateConstraints:self.dismissedConstraints]; |
| [self.baseViewController.view layoutIfNeeded]; |
| }; |
| auto completion = ^(BOOL finished) { |
| [self cleanUpAfterDismissal]; |
| if ([self.delegate |
| respondsToSelector:@selector(containedPresenterDidDismiss:)]) { |
| [self.delegate containedPresenterDidDismiss:self]; |
| } |
| }; |
| |
| if (animated) { |
| // Trigger a layout pass before the animation, so that any pending updates |
| // aren't animated along with the dismissal. |
| [self.baseViewController.view layoutIfNeeded]; |
| |
| [UIView animateWithDuration:kAnimationDuration |
| animations:animations |
| completion:completion]; |
| } else { |
| // Just execute the completion block synchronously if the dismissal isn't |
| // animated. `animations` isn't called because (a) -cleanupAfterDismissal |
| // removes the presented view controller from the view hierarchy, and (b) |
| // in some contexts a non-animated dismissal may occur when the base view |
| // controller is no longer on screen, and the constraint activation in |
| // `animations` will crash. |
| completion(YES); |
| } |
| } |
| |
| #pragma mark - private |
| |
| - (void)cleanUpAfterDismissal { |
| if (self.presentedViewController.parentViewController != |
| self.baseViewController) { |
| return; |
| } |
| [self.presentedViewController willMoveToParentViewController:nil]; |
| [self.presentedViewController.view removeFromSuperview]; |
| [self.presentedViewController removeFromParentViewController]; |
| } |
| |
| @end |