blob: 35ca8fcb72f3987d363f71fee3fcf6c3f8fe34ee [file] [log] [blame] [edit]
// Copyright 2018-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 "MDCActionSheetController.h"
#import "private/MDCActionSheetHeaderView.h"
#import "private/MDCActionSheetItemTableViewCell.h"
#import "private/MaterialActionSheetStrings.h"
#import "private/MaterialActionSheetStrings_table.h"
#import "MDCActionSheetAction.h"
#import "MDCActionSheetControllerDelegate.h"
#import "MaterialAvailability.h"
#import "MaterialBottomSheet.h"
#import "MaterialElevation.h"
#import "MaterialShadowElevations.h"
#import "MaterialTypography.h"
#import "MaterialMath.h"
static NSString *const kReuseIdentifier = @"BaseCell";
static const CGFloat kActionImageAlpha = (CGFloat)0.6;
static const CGFloat kActionTextAlpha = (CGFloat)0.87;
static const CGFloat kDividerDefaultAlpha = (CGFloat)0.12;
// The Bundle for string resources.
static NSString *const kMaterialActionSheetBundle = @"MaterialActionSheet.bundle";
@interface MDCActionSheetController () <MDCBottomSheetPresentationControllerDelegate,
UITableViewDelegate,
UITableViewDataSource>
@property(nonatomic, strong) UITableView *tableView;
@property(nonatomic, strong) MDCActionSheetHeaderView *header;
/** The view that divides the header from the table. */
@property(nonatomic, strong, nonnull) UIView *headerDividerView;
/**
Determines if a @c MDCActionSheetItemTableViewCell should add leading padding or not.
@note Defaults to @c NO.
*/
@property(nonatomic, assign) BOOL addLeadingPaddingToCell;
/**
Reloads the tables data and does a layout pass.
*/
- (void)updateTable;
@end
@interface MDCActionSheetAction ()
@property(nonatomic, nullable, copy) MDCActionSheetHandler completionHandler;
/**
The @c MDCActionSheetController responsible for presenting the action.
@note This is only set when the @c MDCActionSheetController's view is in the heirarchy else it is
@c nil.
*/
@property(nonatomic, weak, nullable) MDCActionSheetController *actionSheet;
@end
@implementation MDCActionSheetAction
+ (instancetype)actionWithTitle:(NSString *)title
image:(UIImage *)image
handler:(void (^__nullable)(MDCActionSheetAction *action))handler {
return [[MDCActionSheetAction alloc] initWithTitle:title image:image handler:handler];
}
- (instancetype)initWithTitle:(NSString *)title
image:(UIImage *)image
handler:(void (^__nullable)(MDCActionSheetAction *action))handler {
self = [super init];
if (self) {
_title = [title copy];
_image = [image copy];
_completionHandler = [handler copy];
_dividerColor = UIColor.clearColor;
}
return self;
}
- (id)copyWithZone:(__unused NSZone *)zone {
MDCActionSheetAction *action = [[self class] actionWithTitle:self.title
image:self.image
handler:self.completionHandler];
action.accessibilityIdentifier = self.accessibilityIdentifier;
action.accessibilityLabel = self.accessibilityLabel;
action.titleColor = self.titleColor;
action.tintColor = self.tintColor;
action.dividerColor = self.dividerColor;
action.showsDivider = self.showsDivider;
return action;
}
- (void)setImage:(UIImage *)image {
_image = image;
if (self.actionSheet) {
[self.actionSheet updateTable];
}
}
@end
@implementation MDCActionSheetController {
NSMutableArray<MDCActionSheetAction *> *_actions;
UIColor *_inkColor;
}
@synthesize mdc_overrideBaseElevation = _mdc_overrideBaseElevation;
@synthesize mdc_elevationDidChangeBlock = _mdc_elevationDidChangeBlock;
@synthesize adjustsFontForContentSizeCategory = _adjustsFontForContentSizeCategory;
+ (instancetype)actionSheetControllerWithTitle:(NSString *)title message:(NSString *)message {
return [[MDCActionSheetController alloc] initWithTitle:title message:message];
}
+ (instancetype)actionSheetControllerWithTitle:(NSString *)title {
return [MDCActionSheetController actionSheetControllerWithTitle:title message:nil];
}
- (instancetype)init {
return [MDCActionSheetController actionSheetControllerWithTitle:nil message:nil];
}
- (instancetype)initWithTitle:(NSString *)title message:(NSString *)message {
self = [super initWithNibName:nil bundle:nil];
if (self) {
_actions = [[NSMutableArray alloc] init];
_transitionController = [[MDCBottomSheetTransitionController alloc] init];
_transitionController.dismissOnBackgroundTap = YES;
_transitionController.delegate = self;
/**
"We must call super because we've made the setters on this class unavailable and overridden
their implementations to throw assertions."
*/
super.transitioningDelegate = _transitionController;
super.modalPresentationStyle = UIModalPresentationCustom;
_tableView = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain];
_transitionController.trackingScrollView = _tableView;
_tableView.autoresizingMask =
(UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight);
_tableView.delegate = self;
_tableView.dataSource = self;
_tableView.estimatedRowHeight = 56;
_tableView.rowHeight = UITableViewAutomaticDimension;
_tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
[_tableView registerClass:[MDCActionSheetItemTableViewCell class]
forCellReuseIdentifier:kReuseIdentifier];
_header = [[MDCActionSheetHeaderView alloc] initWithFrame:CGRectZero];
_header.title = [title copy];
_header.message = [message copy];
_backgroundColor = UIColor.whiteColor;
_header.backgroundColor = _backgroundColor;
_tableView.backgroundColor = _backgroundColor;
_actionTextColor = [UIColor.blackColor colorWithAlphaComponent:kActionTextAlpha];
_actionTintColor = [UIColor.blackColor colorWithAlphaComponent:kActionImageAlpha];
_imageRenderingMode = UIImageRenderingModeAlwaysTemplate;
_headerDividerView = [[UIView alloc] init];
_headerDividerView.backgroundColor =
[UIColor.blackColor colorWithAlphaComponent:kDividerDefaultAlpha];
_mdc_overrideBaseElevation = -1;
_elevation = MDCShadowElevationModalBottomSheet;
}
return self;
}
- (void)didDismissBottomSheetTransitionController:(MDCBottomSheetTransitionController *)controller {
if ([self.delegate respondsToSelector:@selector(actionSheetControllerDidDismiss:)]) {
[self.delegate actionSheetControllerDidDismiss:self];
}
}
- (void)addAction:(MDCActionSheetAction *)action {
[_actions addObject:action];
if (self.alwaysAlignTitleLeadingEdges && action.image) {
self.addLeadingPaddingToCell = YES;
}
[self updateTable];
}
- (UIView *)viewForAction:(MDCActionSheetAction *)action {
if (![self.actions containsObject:action]) {
return nil;
}
[self.view layoutIfNeeded];
NSUInteger rowIndex = [self.actions indexOfObject:action];
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:rowIndex inSection:0];
return [self.tableView cellForRowAtIndexPath:indexPath];
}
- (NSArray<MDCActionSheetAction *> *)actions {
return [_actions copy];
}
- (void)loadView {
[super loadView];
self.mdc_bottomSheetPresentationController.delegate = self;
}
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = self.backgroundColor;
self.tableView.frame = self.view.bounds;
self.tableView.cellLayoutMarginsFollowReadableWidth = NO;
self.view.preservesSuperviewLayoutMargins = YES;
self.view.insetsLayoutMarginsFromSafeArea = NO;
self.tableView.insetsLayoutMarginsFromSafeArea = NO;
[self.view addSubview:self.tableView];
[self.view addSubview:self.header];
[self.view addSubview:self.headerDividerView];
}
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
if (self.tableView.contentSize.height > (CGRectGetHeight(self.view.bounds) / 2)) {
self.mdc_bottomSheetPresentationController.preferredSheetHeight = [self openingSheetHeight];
} else {
self.mdc_bottomSheetPresentationController.preferredSheetHeight = 0;
}
CGSize size = [self.header sizeThatFits:CGRectStandardize(self.view.bounds).size];
self.header.frame = CGRectMake(0, 0, self.view.bounds.size.width, size.height);
CGFloat dividerHeight = self.showsHeaderDivider ? 1 : 0;
self.headerDividerView.frame =
CGRectMake(0, size.height, CGRectGetWidth(self.view.bounds), dividerHeight);
UIEdgeInsets insets = UIEdgeInsetsMake(size.height + dividerHeight, 0, 0, 0);
insets.bottom = self.tableView.adjustedContentInset.bottom;
self.tableView.contentInset = insets;
self.tableView.contentOffset = CGPointMake(0, -size.height);
}
- (CGFloat)openingSheetHeight {
// If there are too many options to fit on half of the screen then show as many options as
// possible minus half a cell, to allow for bleeding and signal to the user that the sheet is
// scrollable content.
CGFloat maxHeight = CGRectGetHeight(self.view.bounds) / 2;
CGFloat headerHeight = [self.header sizeThatFits:CGRectStandardize(self.view.bounds).size].height;
CGFloat cellHeight = self.tableView.contentSize.height / (CGFloat)_actions.count;
CGFloat maxTableHeight = maxHeight - headerHeight;
NSInteger amountOfCellsToShow = (NSInteger)(maxTableHeight / cellHeight);
// There is already a partially shown cell that is showing and more than half is visable
if (fmod(maxTableHeight, cellHeight) > (cellHeight * (CGFloat)0.5)) {
amountOfCellsToShow += 1;
}
CGFloat preferredHeight =
(((CGFloat)amountOfCellsToShow - (CGFloat)0.5) * cellHeight) + headerHeight;
// When updating the preferredSheetHeight the presentation controller takes into account the
// safe area so we have to remove that.
preferredHeight = preferredHeight - self.tableView.adjustedContentInset.bottom;
return ceil(preferredHeight);
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
[self.transitionController.trackingScrollView flashScrollIndicators];
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
for (MDCActionSheetAction *action in self.actions) {
action.actionSheet = self;
}
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
self.mdc_bottomSheetPresentationController.delegate = self;
#pragma clang diagnostic pop
self.mdc_bottomSheetPresentationController.dismissOnBackgroundTap =
self.transitionController.dismissOnBackgroundTap;
[self.view layoutIfNeeded];
NSString *key =
kMaterialActionSheetStringTable[kStr_MaterialActionSheetPresentedAccessibilityAnnouncement];
NSString *announcement = NSLocalizedStringFromTableInBundle(
key, kMaterialActionSheetStringsTableName, [[self class] bundle], @"Alert");
UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, announcement);
}
- (void)viewDidDisappear:(BOOL)animated {
for (MDCActionSheetAction *action in self.actions) {
action.actionSheet = nil;
}
}
- (BOOL)accessibilityPerformEscape {
if (!self.dismissOnBackgroundTap) {
return NO;
}
[self
dismissViewControllerAnimated:YES
completion:^{
if ([self.delegate
respondsToSelector:@selector
(actionSheetControllerDismissalAnimationCompleted:)]) {
[self.delegate actionSheetControllerDismissalAnimationCompleted:self];
}
}];
return YES;
}
- (void)preferredContentSizeDidChangeForChildContentContainer:(id<UIContentContainer>)container {
[super preferredContentSizeDidChangeForChildContentContainer:container];
[self.presentationController preferredContentSizeDidChangeForChildContentContainer:self];
}
- (UIScrollView *)trackingScrollView {
return self.transitionController.trackingScrollView;
}
- (void)setTrackingScrollView:(UIScrollView *)trackingScrollView {
self.transitionController.trackingScrollView = trackingScrollView;
}
- (BOOL)dismissOnBackgroundTap {
return self.transitionController.dismissOnBackgroundTap;
}
- (void)setDismissOnBackgroundTap:(BOOL)dismissOnBackgroundTap {
_transitionController.dismissOnBackgroundTap = dismissOnBackgroundTap;
self.mdc_bottomSheetPresentationController.dismissOnBackgroundTap = dismissOnBackgroundTap;
}
- (UIInterfaceOrientationMask)supportedInterfaceOrientations {
return self.presentingViewController.supportedInterfaceOrientations;
}
/* Disable setter. Always use internal transition controller */
- (void)setTransitioningDelegate:
(__unused id<UIViewControllerTransitioningDelegate>)transitioningDelegate {
NSAssert(NO, @"MDCActionSheetController.transitioningDelegate cannot be changed.");
return;
}
/* Disable setter. Always use custom presentation style */
- (void)setModalPresentationStyle:(__unused UIModalPresentationStyle)modalPresentationStyle {
NSAssert(NO, @"MDCActionSheetController.modalPresentationStyle cannot be changed.");
return;
}
#pragma mark - Table view
- (void)updateTable {
[self.tableView reloadData];
[self.tableView setNeedsLayout];
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
MDCActionSheetAction *action = self.actions[indexPath.row];
[self.presentingViewController dismissViewControllerAnimated:YES
completion:^(void) {
if (action.completionHandler) {
action.completionHandler(action);
}
}];
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return _actions.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath {
MDCActionSheetItemTableViewCell *cell =
[tableView dequeueReusableCellWithIdentifier:kReuseIdentifier forIndexPath:indexPath];
MDCActionSheetAction *action = _actions[indexPath.row];
cell.action = action;
cell.backgroundColor = self.backgroundColor;
cell.actionFont = self.actionFont;
cell.actionLabel.adjustsFontForContentSizeCategory = self.adjustsFontForContentSizeCategory;
cell.accessibilityIdentifier = action.accessibilityIdentifier;
cell.rippleColor = self.rippleColor;
cell.tintColor = action.tintColor ?: self.actionTintColor;
cell.imageRenderingMode = self.imageRenderingMode;
cell.addLeadingPadding = self.addLeadingPaddingToCell;
cell.actionTextColor = action.titleColor ?: self.actionTextColor;
cell.contentEdgeInsets = self.contentEdgeInsets;
cell.dividerColor = action.dividerColor;
cell.showsDivider = action.showsDivider;
return cell;
}
- (void)tableView:(UITableView *)tableView
willDisplayCell:(nonnull UITableViewCell *)cell
forRowAtIndexPath:(nonnull NSIndexPath *)indexPath {
if ([self.delegate respondsToSelector:@selector(actionSheetController:
willDisplayView:forRowAtIndexPath:)]) {
[self.delegate actionSheetController:self willDisplayView:cell forRowAtIndexPath:indexPath];
}
}
- (void)setContentEdgeInsets:(UIEdgeInsets)contentEdgeInsets {
if (UIEdgeInsetsEqualToEdgeInsets(_contentEdgeInsets, contentEdgeInsets)) {
return;
}
_contentEdgeInsets = contentEdgeInsets;
[self.tableView reloadData];
}
- (void)setTitle:(NSString *)title {
self.header.title = title;
[self.view setNeedsLayout];
}
- (NSString *)title {
return self.header.title;
}
- (void)setMessage:(NSString *)message {
self.header.message = message;
[self.view setNeedsLayout];
}
- (NSString *)message {
return self.header.message;
}
- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection {
[super traitCollectionDidChange:previousTraitCollection];
#if MDC_AVAILABLE_SDK_IOS(13_0)
if (@available(iOS 13.0, *)) {
if ([self.traitCollection
hasDifferentColorAppearanceComparedToTraitCollection:previousTraitCollection]) {
[self.tableView reloadData];
}
}
#endif // MDC_AVAILABLE_SDK_IOS(13_0)
if (self.traitCollectionDidChangeBlock) {
self.traitCollectionDidChangeBlock(self, previousTraitCollection);
}
}
- (void)setTitleFont:(UIFont *)titleFont {
self.header.titleFont = titleFont;
}
- (UIFont *)titleFont {
return self.header.titleFont;
}
- (void)setMessageFont:(UIFont *)messageFont {
self.header.messageFont = messageFont;
}
- (UIFont *)messageFont {
return self.header.messageFont;
}
- (void)setBackgroundColor:(UIColor *)backgroundColor {
_backgroundColor = backgroundColor;
self.view.backgroundColor = backgroundColor;
self.tableView.backgroundColor = backgroundColor;
self.header.backgroundColor = backgroundColor;
}
- (void)setTitleTextColor:(UIColor *)titleTextColor {
self.header.titleTextColor = titleTextColor;
}
- (UIColor *)titleTextColor {
return self.header.titleTextColor;
}
- (void)setMessageTextColor:(UIColor *)messageTextColor {
self.header.messageTextColor = messageTextColor;
}
- (UIColor *)messageTextColor {
return self.header.messageTextColor;
}
- (void)setHeaderDividerColor:(UIColor *)headerDividerColor {
self.headerDividerView.backgroundColor = headerDividerColor;
}
- (UIColor *)headerDividerColor {
return self.headerDividerView.backgroundColor;
}
- (void)setShowsHeaderDivider:(BOOL)showsHeaderDivider {
_showsHeaderDivider = showsHeaderDivider;
self.headerDividerView.hidden = !showsHeaderDivider;
}
#pragma mark - Dynamic Type
- (void)setAdjustsFontForContentSizeCategory:(BOOL)adjustsFontForContentSizeCategory {
_adjustsFontForContentSizeCategory = adjustsFontForContentSizeCategory;
self.header.adjustsFontForContentSizeCategory = adjustsFontForContentSizeCategory;
}
- (void)updateTableFonts {
UIFont *finalActionsFont =
_actionFont ?: [UIFont mdc_standardFontForMaterialTextStyle:MDCFontTextStyleSubheadline];
_actionFont = finalActionsFont;
[self updateTable];
}
- (void)updateFontsForDynamicType {
[self updateTableFonts];
[self.view setNeedsLayout];
}
#pragma mark - Table customization
- (void)setActionFont:(UIFont *)actionFont {
_actionFont = actionFont;
[self.tableView reloadData];
}
- (void)setActionTextColor:(UIColor *)actionTextColor {
_actionTextColor = actionTextColor;
[self.tableView reloadData];
}
- (void)setActionTintColor:(UIColor *)actionTintColor {
_actionTintColor = actionTintColor;
[self.tableView reloadData];
}
- (void)setImageRenderingMode:(UIImageRenderingMode)imageRenderingMode {
_imageRenderingMode = imageRenderingMode;
[self.tableView reloadData];
}
- (void)setAlwaysAlignTitleLeadingEdges:(BOOL)alignTitles {
_alwaysAlignTitleLeadingEdges = alignTitles;
if (alignTitles) {
// Check to make sure at least one action has an image. If not then all actions will align
// already and we don't need to add padding.
self.addLeadingPaddingToCell = [self anyActionHasAnImage];
} else {
self.addLeadingPaddingToCell = NO;
}
[self.tableView reloadData];
}
- (BOOL)anyActionHasAnImage {
for (MDCActionSheetAction *action in self.actions) {
if (action.image) {
return YES;
}
}
return NO;
}
- (void)setRippleColor:(UIColor *)rippleColor {
_rippleColor = rippleColor;
[self.tableView reloadData];
}
- (void)setElevation:(MDCShadowElevation)elevation {
if (MDCCGFloatEqual(elevation, _elevation)) {
return;
}
_elevation = elevation;
[self.view mdc_elevationDidChange];
}
- (CGFloat)mdc_currentElevation {
return self.elevation;
}
#pragma mark - MDCBottomSheetPresentationControllerDelegate
- (void)bottomSheetPresentationControllerDidDismissBottomSheet:
(nonnull MDCBottomSheetController *)controller {
if ([self.delegate respondsToSelector:@selector(actionSheetControllerDidDismiss:)]) {
[self.delegate actionSheetControllerDidDismiss:self];
}
}
- (void)bottomSheetPresentationControllerDismissalAnimationCompleted:
(MDCBottomSheetPresentationController *)bottomSheet {
if ([self.delegate
respondsToSelector:@selector(actionSheetControllerDismissalAnimationCompleted:)]) {
[self.delegate actionSheetControllerDismissalAnimationCompleted:self];
}
}
#pragma mark - Resource bundle
+ (NSBundle *)bundle {
static NSBundle *bundle = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
bundle = [NSBundle bundleWithPath:[self bundlePathWithName:kMaterialActionSheetBundle]];
});
return bundle;
}
+ (NSString *)bundlePathWithName:(NSString *)bundleName {
NSBundle *bundle = [NSBundle bundleForClass:[MDCActionSheetController class]];
NSString *resourcePath = [(nil == bundle ? [NSBundle mainBundle] : bundle) resourcePath];
return [resourcePath stringByAppendingPathComponent:bundleName];
}
@end