diff --git a/examples/CustomPresentationExample.swift b/examples/CustomPresentationExample.swift index c3d3b45..7bab6ef 100644 --- a/examples/CustomPresentationExample.swift +++ b/examples/CustomPresentationExample.swift @@ -68,7 +68,7 @@ final class VerticalSheetTransition: NSObject, Transition { // When provided, the transition will use a presentation controller to customize the presentation // of the transition. - var calculateFrameOfPresentedViewInContainerView: CalculateFrame? + var calculateFrameOfPresentedViewInContainerView: TransitionFrameCalculation? func start(with context: TransitionContext) { CATransaction.begin() @@ -122,104 +122,14 @@ extension VerticalSheetTransition: TransitionWithPresentation, TransitionWithFal presenting: UIViewController, source: UIViewController?) -> UIPresentationController? { if let calculateFrameOfPresentedViewInContainerView = calculateFrameOfPresentedViewInContainerView { - return DimmingPresentationController(presentedViewController: presented, - presenting: presenting, - calculateFrameOfPresentedViewInContainerView: calculateFrameOfPresentedViewInContainerView) + return TransitionPresentationController(presentedViewController: presented, + presenting: presenting, + calculateFrameOfPresentedView: calculateFrameOfPresentedViewInContainerView) } return nil } } -// What follows is a fairly typical presentation controller implementation that adds a dimming view -// and fades the dimming view in/out during the transition. -// -// Note that we've conformed to the Transition type: this allows the presentation controller to -// add any custom animations during the transition. The presentation controller's `start` method -// will be invoked before the Transition object's `start` method. - -final class DimmingPresentationController: UIPresentationController { - - init(presentedViewController: UIViewController, - presenting presentingViewController: UIViewController, - calculateFrameOfPresentedViewInContainerView: @escaping CalculateFrame) { - let dimmingView = UIView() - dimmingView.backgroundColor = UIColor(white: 0, alpha: 0.3) - dimmingView.alpha = 0 - dimmingView.autoresizingMask = [.flexibleWidth, .flexibleHeight] - self.dimmingView = dimmingView - - self.calculateFrameOfPresentedViewInContainerView = calculateFrameOfPresentedViewInContainerView - - super.init(presentedViewController: presentedViewController, presenting: presentingViewController) - } - - override var frameOfPresentedViewInContainerView: CGRect { - // We delegate out our frame calculation here: - return calculateFrameOfPresentedViewInContainerView(self) - } - - override func presentationTransitionWillBegin() { - guard let containerView = containerView else { return } - - dimmingView.frame = containerView.bounds - containerView.insertSubview(dimmingView, at: 0) - - // This autoresizing mask assumes that the calculated frame is centered in the screen. This - // assumption won't hold true if the frame is aligned to a particular edge. We could improve - // this implementation by allowing the creator of the transition to customize the - // autoresizingMask in some manner. - presentedViewController.view.autoresizingMask = [.flexibleLeftMargin, - .flexibleTopMargin, - .flexibleRightMargin, - .flexibleBottomMargin] - } - - override func presentationTransitionDidEnd(_ completed: Bool) { - if !completed { - dimmingView.removeFromSuperview() - } - } - - override func dismissalTransitionWillBegin() { - // We fall back to an alongside fade out when there is no active transition instance because - // our start implementation won't be invoked in this case. - if presentedViewController.transitionController.activeTransition == nil { - presentedViewController.transitionCoordinator?.animate(alongsideTransition: { context in - self.dimmingView.alpha = 0 - }) - } - } - - override func dismissalTransitionDidEnd(_ completed: Bool) { - if completed { - dimmingView.removeFromSuperview() - } else { - dimmingView.alpha = 1 - } - } - - private let calculateFrameOfPresentedViewInContainerView: CalculateFrame - fileprivate let dimmingView: UIView -} - -extension DimmingPresentationController: Transition { - func start(with context: TransitionContext) { - let fade = CABasicAnimation(keyPath: "opacity") - fade.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut) - fade.fromValue = 0 - fade.toValue = 1 - if context.direction == .backward { - let swap = fade.fromValue - fade.fromValue = fade.toValue - fade.toValue = swap - } - dimmingView.layer.add(fade, forKey: fade.keyPath) - dimmingView.layer.setValue(fade.toValue, forKeyPath: fade.keyPath!) - } -} - -typealias CalculateFrame = (UIPresentationController) -> CGRect - // MARK: Supplemental code extension CustomPresentationExampleViewController { diff --git a/src/MDMTransitionNavigationControllerDelegate.m b/src/MDMTransitionNavigationControllerDelegate.m index 55996fc..da377bb 100644 --- a/src/MDMTransitionNavigationControllerDelegate.m +++ b/src/MDMTransitionNavigationControllerDelegate.m @@ -17,7 +17,7 @@ #import "MDMTransitionNavigationControllerDelegate.h" #import "MDMTransitionContext.h" -#import "private/MDMPresentationTransitionController.h" +#import "private/MDMViewControllerTransitionController.h" #import "private/MDMViewControllerTransitionContext.h" @interface MDMTransitionNavigationControllerDelegate () diff --git a/src/MDMTransitionPresentationController.h b/src/MDMTransitionPresentationController.h new file mode 100644 index 0000000..1def830 --- /dev/null +++ b/src/MDMTransitionPresentationController.h @@ -0,0 +1,91 @@ +/* + Copyright 2017-present The Material Motion Authors. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +#import +#import + +@protocol MDMTransitionContext; +@protocol MDMTransitionPresentationAnimationControlling; + +NS_SWIFT_NAME(TransitionFrameCalculation) +typedef CGRect (^MDMTransitionFrameCalculation)(UIPresentationController * _Nonnull); + +/** + A transition presentation controller implementation that supports animation delegation, a darkened + overlay view, and custom presentation frames. + + The presentation controller will create and manage the lifecycle of the scrim view, ensuring that + it is removed upon a completed dismissal of the presented view controller. + */ +NS_SWIFT_NAME(TransitionPresentationController) +@interface MDMTransitionPresentationController : UIPresentationController + +/** + Initializes a presentation controller with the standard values and a frame calculation block. + + The frame calculation block is expected to return the desired frame of the presented view + controller. + */ +- (nonnull instancetype)initWithPresentedViewController:(nonnull UIViewController *)presentedViewController + presentingViewController:(nonnull UIViewController *)presentingViewController + calculateFrameOfPresentedView:(nullable MDMTransitionFrameCalculation)calculateFrameOfPresentedView +NS_DESIGNATED_INITIALIZER; + +/** + The presentation controller's scrim view. + */ +@property(nonatomic, strong, nullable, readonly) UIView * scrimView; + +/** + The animation controller is able to customize animations in reaction to view controller + presentation and dismissal events. + + The animation controller is explicitly nil'd upon completion of the dismissal transition. + */ +@property(nonatomic, strong, nullable) id animationController; + +@end + +/** + An animation controller receives additional presentation- and dismissal-related events during a + view controller transition. + */ +NS_SWIFT_NAME(TransitionPresentationAnimationControlling) +@protocol MDMTransitionPresentationAnimationControlling +@optional + +/** + Allows the receiver to register animations for the given transition context. + + Invoked prior to the Transition instance's startWithContext. + + If not implemented, the scrim view will be faded in during presentation and out during dismissal. + */ +- (void)presentationController:(nonnull MDMTransitionPresentationController *)presentationController + startWithContext:(nonnull NSObject *)context; + +/** + Informs the receiver that the dismissal transition is about to begin. + */ +- (void)dismissalTransitionWillBeginWithPresentationController:(nonnull MDMTransitionPresentationController *)presentationController; + +/** + Informs the receiver that the dismissal transition has completed. + */ +- (void)presentationController:(nonnull MDMTransitionPresentationController *)presentationController + dismissalTransitionDidEnd:(BOOL)completed; + +@end diff --git a/src/MDMTransitionPresentationController.m b/src/MDMTransitionPresentationController.m new file mode 100644 index 0000000..684875a --- /dev/null +++ b/src/MDMTransitionPresentationController.m @@ -0,0 +1,120 @@ +/* + Copyright 2017-present The Material Motion Authors. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +#import "MDMTransitionPresentationController.h" + +#import "MDMTransition.h" +#import "MDMTransitionContext.h" +#import "MDMTransitionController.h" +#import "UIViewController+TransitionController.h" + +@interface MDMTransitionPresentationController () +@end + +@implementation MDMTransitionPresentationController { + CGRect (^_calculateFrameOfPresentedView)(UIPresentationController *); +} + +- (instancetype)initWithPresentedViewController:(UIViewController *)presentedViewController + presentingViewController:(UIViewController *)presentingViewController + calculateFrameOfPresentedView:(MDMTransitionFrameCalculation)calculateFrameOfPresentedView { + self = [super initWithPresentedViewController:presentedViewController + presentingViewController:presentingViewController]; + if (self) { + _calculateFrameOfPresentedView = [calculateFrameOfPresentedView copy]; + } + return self; +} + +- (instancetype)initWithPresentedViewController:(UIViewController *)presentedViewController presentingViewController:(UIViewController *)presentingViewController { + return [self initWithPresentedViewController:presentedViewController + presentingViewController:presentingViewController + calculateFrameOfPresentedView:nil]; +} + +- (CGRect)frameOfPresentedViewInContainerView { + if (_calculateFrameOfPresentedView) { + return _calculateFrameOfPresentedView(self); + } else { + return self.containerView.bounds; + } +} + +- (BOOL)shouldRemovePresentersView { + // We don't have access to the container view when this method is called, so we can only guess as + // to whether we'll be presenting full screen by checking for the presence of a frame calculation + // block. + BOOL definitelyFullscreen = _calculateFrameOfPresentedView == nil; + + // Returning true here will cause UIKit to invoke viewWillDisappear and viewDidDisappear on the + // presenting view controller, and the presenting view controller's view will be removed on + // completion of the transition. + return definitelyFullscreen; +} + +- (void)dismissalTransitionWillBegin { + if (!self.presentedViewController.mdm_transitionController.activeTransition) { + [self.presentedViewController.transitionCoordinator animateAlongsideTransition:^(id _Nonnull context) { + self.scrimView.alpha = 0; + } completion:nil]; + + if ([self.animationController respondsToSelector:@selector(dismissalTransitionWillBeginWithPresentationController:)]) { + [self.animationController dismissalTransitionWillBeginWithPresentationController:self]; + } + } +} + +- (void)dismissalTransitionDidEnd:(BOOL)completed { + if (completed) { + [self.scrimView removeFromSuperview]; + _scrimView = nil; + + } else { + self.scrimView.alpha = 1; + } + + if ([self.animationController respondsToSelector:@selector(presentationController:dismissalTransitionDidEnd:)]) { + [self.animationController presentationController:self dismissalTransitionDidEnd:completed]; + } + + if (completed) { + // Break any potential memory cycles due to our strong ownership of the animation controller. + self.animationController = nil; + } +} + +- (void)startWithContext:(NSObject *)context { + if (!self.scrimView) { + _scrimView = [[UIView alloc] initWithFrame:context.containerView.bounds]; + self.scrimView.autoresizingMask = (UIViewAutoresizingFlexibleWidth + | UIViewAutoresizingFlexibleHeight); + self.scrimView.backgroundColor = [UIColor colorWithWhite:0 alpha:0.3f]; + [context.containerView insertSubview:self.scrimView + belowSubview:context.foreViewController.view]; + } + + if ([self.animationController respondsToSelector:@selector(presentationController:startWithContext:)]) { + [self.animationController presentationController:self startWithContext:context]; + } else { + self.scrimView.alpha = context.direction == MDMTransitionDirectionForward ? 0 : 1; + + [UIView animateWithDuration:context.duration animations:^{ + self.scrimView.alpha = context.direction == MDMTransitionDirectionForward ? 1 : 0; + }]; + } +} + +@end diff --git a/src/MotionTransitioning.h b/src/MotionTransitioning.h index c2caf73..24d0c4e 100644 --- a/src/MotionTransitioning.h +++ b/src/MotionTransitioning.h @@ -18,4 +18,5 @@ #import "MDMTransitionContext.h" #import "MDMTransitionController.h" #import "MDMTransitionNavigationControllerDelegate.h" +#import "MDMTransitionPresentationController.h" #import "UIViewController+TransitionController.h" diff --git a/src/UIViewController+TransitionController.m b/src/UIViewController+TransitionController.m index 5f9de4a..cae45cc 100644 --- a/src/UIViewController+TransitionController.m +++ b/src/UIViewController+TransitionController.m @@ -16,7 +16,7 @@ #import "UIViewController+TransitionController.h" -#import "private/MDMPresentationTransitionController.h" +#import "private/MDMViewControllerTransitionController.h" #import @@ -27,9 +27,9 @@ @implementation UIViewController (MDMTransitionController) - (id)mdm_transitionController { const void *key = [self mdm_transitionControllerKey]; - MDMPresentationTransitionController *controller = objc_getAssociatedObject(self, key); + MDMViewControllerTransitionController *controller = objc_getAssociatedObject(self, key); if (!controller) { - controller = [[MDMPresentationTransitionController alloc] initWithViewController:self]; + controller = [[MDMViewControllerTransitionController alloc] initWithViewController:self]; [self mdm_setTransitionController:controller]; } return controller; @@ -37,11 +37,11 @@ @implementation UIViewController (MDMTransitionController) #pragma mark - Private -- (void)mdm_setTransitionController:(MDMPresentationTransitionController *)controller { +- (void)mdm_setTransitionController:(MDMViewControllerTransitionController *)controller { const void *key = [self mdm_transitionControllerKey]; // Clear the previous delegate if we'd previously set one. - MDMPresentationTransitionController *existingController = objc_getAssociatedObject(self, key); + MDMViewControllerTransitionController *existingController = objc_getAssociatedObject(self, key); id delegate = self.transitioningDelegate; if (existingController == delegate) { self.transitioningDelegate = nil; diff --git a/src/private/MDMViewControllerTransitionContext.m b/src/private/MDMViewControllerTransitionContext.m index ed5d1a0..7cecd79 100644 --- a/src/private/MDMViewControllerTransitionContext.m +++ b/src/private/MDMViewControllerTransitionContext.m @@ -69,7 +69,7 @@ - (void)animateTransition:(id)transitionCo // TODO(featherless): Implement interactive transitioning. Need to implement // UIViewControllerInteractiveTransitioning here and isInteractive and interactionController* in -// MDMPresentationTransitionController. +// MDMViewControllerTransitionController. #pragma mark - MDMTransitionContext diff --git a/src/private/MDMPresentationTransitionController.h b/src/private/MDMViewControllerTransitionController.h similarity index 87% rename from src/private/MDMPresentationTransitionController.h rename to src/private/MDMViewControllerTransitionController.h index 22fe219..ed046d5 100644 --- a/src/private/MDMPresentationTransitionController.h +++ b/src/private/MDMViewControllerTransitionController.h @@ -19,7 +19,7 @@ #import "MDMTransitionController.h" -@interface MDMPresentationTransitionController : NSObject +@interface MDMViewControllerTransitionController : NSObject - (nonnull instancetype)initWithViewController:(nonnull UIViewController *)viewController NS_DESIGNATED_INITIALIZER; diff --git a/src/private/MDMPresentationTransitionController.m b/src/private/MDMViewControllerTransitionController.m similarity index 95% rename from src/private/MDMPresentationTransitionController.m rename to src/private/MDMViewControllerTransitionController.m index a34ef26..a5b2fae 100644 --- a/src/private/MDMPresentationTransitionController.m +++ b/src/private/MDMViewControllerTransitionController.m @@ -14,15 +14,15 @@ limitations under the License. */ -#import "MDMPresentationTransitionController.h" +#import "MDMViewControllerTransitionController.h" #import "MDMTransition.h" #import "MDMViewControllerTransitionContext.h" -@interface MDMPresentationTransitionController () +@interface MDMViewControllerTransitionController () @end -@implementation MDMPresentationTransitionController { +@implementation MDMViewControllerTransitionController { // We expect the view controller to hold a strong reference to its transition controller, so keep // a weak reference to the view controller here. __weak UIViewController *_associatedViewController;