blob: 89851f4e99111a42ff4efc22c2f638d15a9a601f [file] [log] [blame] [edit]
// Copyright 2017-present the Material Components for iOS 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 "MDCBottomSheetPresentationController.h"
#import <WebKit/WebKit.h>
#import "MaterialMath.h"
#import "private/MDCSheetContainerView.h"
static UIScrollView *MDCBottomSheetGetPrimaryScrollView(UIViewController *viewController) {
UIScrollView *scrollView = nil;
// Ensure the view is loaded - occasionally during non-animated transitions the view may not be
// loaded yet (but the scrollview is still needed for scroll-tracking to work properly).
if (![viewController isViewLoaded]) {
(void)viewController.view;
}
if ([viewController isKindOfClass:[MDCBottomSheetController class]]) {
viewController = ((MDCBottomSheetController *)viewController).contentViewController;
}
if ([viewController.view isKindOfClass:[UIScrollView class]]) {
scrollView = (UIScrollView *)viewController.view;
} else if ([viewController.view isKindOfClass:[WKWebView class]]) {
scrollView = ((WKWebView *)viewController.view).scrollView;
} else if ([viewController isKindOfClass:[UICollectionViewController class]]) {
scrollView = ((UICollectionViewController *)viewController).collectionView;
}
return scrollView;
}
@interface MDCBottomSheetPresentationController () <MDCSheetContainerViewDelegate>
@end
@interface MDCBottomSheetPresentationController ()
@property(nonatomic, strong) MDCSheetContainerView *sheetView;
@end
@implementation MDCBottomSheetPresentationController {
UIView *_dimmingView;
@private
UIColor *_scrimColor;
@private
BOOL _scrimIsAccessibilityElement;
@private
NSString *_scrimAccessibilityLabel;
@private
NSString *_scrimAccessibilityHint;
@private
UIAccessibilityTraits _scrimAccessibilityTraits;
}
@synthesize delegate;
- (UIView *)presentedView {
return self.sheetView;
}
- (CGRect)frameOfPresentedViewInContainerView {
CGSize containerSize = self.containerView.frame.size;
CGSize preferredSize = self.presentedViewController.preferredContentSize;
if (preferredSize.width > 0 && preferredSize.width < containerSize.width) {
// We only customize the width and not the height here. MDCSheetContainerView lays out the
// presentedView taking the preferred height in to account.
CGFloat width = preferredSize.width;
CGFloat leftPad = (containerSize.width - width) / 2;
return CGRectMake(leftPad, 0, width, containerSize.height);
} else {
return [super frameOfPresentedViewInContainerView];
}
}
- (void)presentationTransitionWillBegin {
id<MDCBottomSheetPresentationControllerDelegate> strongDelegate = self.delegate;
if ([strongDelegate respondsToSelector:@selector(prepareForBottomSheetPresentation:)]) {
[strongDelegate prepareForBottomSheetPresentation:self];
}
UIView *containerView = [self containerView];
_dimmingView = [[UIView alloc] initWithFrame:self.containerView.bounds];
_dimmingView.backgroundColor =
_scrimColor ? _scrimColor : [UIColor colorWithWhite:0 alpha:(CGFloat)0.4];
_dimmingView.translatesAutoresizingMaskIntoConstraints = NO;
_dimmingView.autoresizingMask =
UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
_dimmingView.accessibilityTraits |= UIAccessibilityTraitButton;
_dimmingView.isAccessibilityElement = _scrimIsAccessibilityElement;
_dimmingView.accessibilityTraits = _scrimAccessibilityTraits;
_dimmingView.accessibilityLabel = _scrimAccessibilityLabel;
_dimmingView.accessibilityHint = _scrimAccessibilityHint;
_dismissOnDraggingDownSheet = YES;
UIScrollView *scrollView = self.trackingScrollView;
if (scrollView == nil) {
scrollView = MDCBottomSheetGetPrimaryScrollView(self.presentedViewController);
}
CGRect sheetFrame = [self frameOfPresentedViewInContainerView];
self.sheetView = [[MDCSheetContainerView alloc] initWithFrame:sheetFrame
contentView:self.presentedViewController.view
scrollView:scrollView];
self.sheetView.delegate = self;
self.sheetView.autoresizingMask = UIViewAutoresizingFlexibleHeight;
self.sheetView.dismissOnDraggingDownSheet = self.dismissOnDraggingDownSheet;
[containerView addSubview:_dimmingView];
[containerView addSubview:self.sheetView];
[self updatePreferredSheetHeight];
// Add tap handler to dismiss the sheet.
UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc]
initWithTarget:self
action:@selector(dismissPresentedControllerIfNecessary:)];
tapGesture.cancelsTouchesInView = NO;
containerView.userInteractionEnabled = YES;
[containerView addGestureRecognizer:tapGesture];
id<UIViewControllerTransitionCoordinator> transitionCoordinator =
[[self presentingViewController] transitionCoordinator];
// Fade in the dimming view during the transition.
_dimmingView.alpha = 0.0;
[transitionCoordinator
animateAlongsideTransition:^(
__unused id<UIViewControllerTransitionCoordinatorContext> context) {
self->_dimmingView.alpha = 1.0;
}
completion:nil];
}
- (void)presentationTransitionDidEnd:(BOOL)completed {
if (!completed) {
[_dimmingView removeFromSuperview];
}
}
- (void)dismissalTransitionWillBegin {
id<UIViewControllerTransitionCoordinator> transitionCoordinator =
[[self presentingViewController] transitionCoordinator];
[transitionCoordinator
animateAlongsideTransition:^(
__unused id<UIViewControllerTransitionCoordinatorContext> context) {
self->_dimmingView.alpha = 0.0;
}
completion:nil];
}
- (void)dismissalTransitionDidEnd:(BOOL)completed {
if (completed) {
[_dimmingView removeFromSuperview];
}
}
- (void)preferredContentSizeDidChangeForChildContentContainer:(id<UIContentContainer>)container {
[super preferredContentSizeDidChangeForChildContentContainer:container];
self.sheetView.frame = [self frameOfPresentedViewInContainerView];
[self.sheetView layoutIfNeeded];
[self updatePreferredSheetHeight];
}
- (void)viewWillTransitionToSize:(CGSize)size
withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator {
[super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
[coordinator
animateAlongsideTransition:^(
__unused id<UIViewControllerTransitionCoordinatorContext> _Nonnull context) {
self.sheetView.frame = [self frameOfPresentedViewInContainerView];
[self.sheetView layoutIfNeeded];
[self updatePreferredSheetHeight];
}
completion:nil];
}
/**
Sets the new value of @c sheetView.preferredSheetHeight.
If @c preferredContentHeight is non-positive, it will set it to half of sheetView's
frame's height.
*/
- (void)updatePreferredSheetHeight {
// If |preferredSheetHeight| has not been specified, use half of the current height.
CGFloat preferredSheetHeight;
if (self.preferredSheetHeight > 0) {
preferredSheetHeight = self.preferredSheetHeight;
} else {
preferredSheetHeight = self.presentedViewController.preferredContentSize.height;
}
if (MDCCGFloatEqual(preferredSheetHeight, 0)) {
preferredSheetHeight = MDCRound(CGRectGetHeight(self.sheetView.frame) / 2);
}
self.sheetView.preferredSheetHeight = preferredSheetHeight;
}
- (void)dismissPresentedControllerIfNecessary:(UITapGestureRecognizer *)tapRecognizer {
if (!_dismissOnBackgroundTap) {
return;
}
// Only dismiss if the tap is outside of the presented view.
UIView *contentView = self.presentedViewController.view;
CGPoint pointInContentView = [tapRecognizer locationInView:contentView];
if ([contentView pointInside:pointInContentView withEvent:nil]) {
return;
}
[self.presentingViewController dismissViewControllerAnimated:YES completion:nil];
id<MDCBottomSheetPresentationControllerDelegate> strongDelegate = self.delegate;
if ([strongDelegate
respondsToSelector:@selector(bottomSheetPresentationControllerDidDismissBottomSheet:)]) {
[strongDelegate bottomSheetPresentationControllerDidDismissBottomSheet:self];
}
}
#pragma mark - Properties
- (void)setScrimColor:(UIColor *)scrimColor {
_scrimColor = scrimColor;
_dimmingView.backgroundColor = scrimColor;
}
- (UIColor *)scrimColor {
return _scrimColor;
}
- (void)setIsScrimAccessibilityElement:(BOOL)isScrimAccessibilityElement {
_scrimIsAccessibilityElement = isScrimAccessibilityElement;
_dimmingView.isAccessibilityElement = isScrimAccessibilityElement;
}
- (BOOL)isScrimAccessibilityElement {
return _scrimIsAccessibilityElement;
}
- (void)setScrimAccessibilityLabel:(NSString *)scrimAccessibilityLabel {
_scrimAccessibilityLabel = scrimAccessibilityLabel;
_dimmingView.accessibilityLabel = scrimAccessibilityLabel;
}
- (NSString *)scrimAccessibilityLabel {
return _scrimAccessibilityLabel;
}
- (void)setScrimAccessibilityHint:(NSString *)scrimAccessibilityHint {
_scrimAccessibilityHint = scrimAccessibilityHint;
_dimmingView.accessibilityHint = scrimAccessibilityHint;
}
- (NSString *)scrimAccessibilityHint {
return _scrimAccessibilityHint;
}
- (void)setScrimAccessibilityTraits:(UIAccessibilityTraits)scrimAccessibilityTraits {
_scrimAccessibilityTraits = scrimAccessibilityTraits;
_dimmingView.accessibilityTraits = scrimAccessibilityTraits;
}
- (UIAccessibilityTraits)scrimAccessibilityTraits {
return _scrimAccessibilityTraits;
}
- (void)setPreferredSheetHeight:(CGFloat)preferredSheetHeight {
_preferredSheetHeight = preferredSheetHeight;
[self updatePreferredSheetHeight];
}
- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection {
[super traitCollectionDidChange:previousTraitCollection];
if (self.traitCollectionDidChangeBlock) {
self.traitCollectionDidChangeBlock(self, previousTraitCollection);
}
}
- (void)setDismissOnDraggingDownSheet:(BOOL)dismissOnDraggingDownSheet {
_dismissOnDraggingDownSheet = dismissOnDraggingDownSheet;
if (self.sheetView) {
self.sheetView.dismissOnDraggingDownSheet = dismissOnDraggingDownSheet;
}
}
#pragma mark - MDCSheetContainerViewDelegate
- (void)sheetContainerViewDidHide:(nonnull __unused MDCSheetContainerView *)containerView {
[self.presentingViewController dismissViewControllerAnimated:YES completion:nil];
id<MDCBottomSheetPresentationControllerDelegate> strongDelegate = self.delegate;
if ([strongDelegate
respondsToSelector:@selector(bottomSheetPresentationControllerDidDismissBottomSheet:)]) {
[strongDelegate bottomSheetPresentationControllerDidDismissBottomSheet:self];
}
}
- (void)sheetContainerViewWillChangeState:(nonnull MDCSheetContainerView *)containerView
sheetState:(MDCSheetState)sheetState {
id<MDCBottomSheetPresentationControllerDelegate> strongDelegate = self.delegate;
if ([strongDelegate respondsToSelector:@selector(bottomSheetWillChangeState:sheetState:)]) {
[strongDelegate bottomSheetWillChangeState:self sheetState:sheetState];
}
}
- (void)sheetContainerViewDidChangeYOffset:(nonnull MDCSheetContainerView *)containerView
yOffset:(CGFloat)yOffset {
id<MDCBottomSheetPresentationControllerDelegate> strongDelegate = self.delegate;
if ([strongDelegate respondsToSelector:@selector(bottomSheetDidChangeYOffset:yOffset:)]) {
[strongDelegate bottomSheetDidChangeYOffset:self yOffset:yOffset];
}
}
@end