blob: 09b0ffd3e6fad944c86c1f0acd7ca0d5d3f8a9b3 [file] [log] [blame]
// 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 "MaterialMath.h"
#import "MaterialTypography.h"
#import "private/MDCActionSheetHeaderView.h"
#import "private/MDCActionSheetItemTableViewCell.h"
static NSString *const kReuseIdentifier = @"BaseCell";
static const CGFloat kActionImageAlpha = (CGFloat)0.6;
static const CGFloat kActionTextAlpha = (CGFloat)0.87;
@interface MDCActionSheetAction ()
@property(nonatomic, nullable, copy) MDCActionSheetHandler completionHandler;
@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];
}
return self;
}
- (id)copyWithZone:(__unused NSZone *)zone {
MDCActionSheetAction *action = [[self class] actionWithTitle:self.title
image:self.image
handler:self.completionHandler];
action.accessibilityIdentifier = self.accessibilityIdentifier;
return action;
}
@end
@interface MDCActionSheetController () <MDCBottomSheetPresentationControllerDelegate,
UITableViewDelegate, UITableViewDataSource>
@property(nonatomic, strong) UITableView *tableView;
@property(nonatomic, strong) MDCActionSheetHeaderView *header;
@end
@implementation MDCActionSheetController {
NSMutableArray<MDCActionSheetAction *> *_actions;
}
@synthesize mdc_adjustsFontForContentSizeCategory = _mdc_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;
/**
"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;
}
return self;
}
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)addAction:(MDCActionSheetAction *)action {
[_actions addObject:action];
[self updateTable];
}
- (NSArray<MDCActionSheetAction *> *)actions {
return [_actions copy];
}
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = self.backgroundColor;
self.tableView.frame = self.view.bounds;
[self.view addSubview:self.tableView];
[self.view addSubview:self.header];
}
- (void)viewWillLayoutSubviews {
[super viewWillLayoutSubviews];
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);
UIEdgeInsets insets = UIEdgeInsetsMake(self.header.frame.size.height, 0, 0, 0);
if (@available(iOS 11.0, *)) {
insets.bottom = self.view.safeAreaInsets.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.
if (@available(iOS 11.0, *)) {
preferredHeight = preferredHeight - self.view.safeAreaInsets.bottom;
}
return MDCCeil(preferredHeight);
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
#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];
}
- (BOOL)accessibilityPerformEscape {
if (!self.dismissOnBackgroundTap) {
return NO;
}
[self dismissViewControllerAnimated:YES completion:nil];
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;
}
/* 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.mdc_adjustsFontForContentSizeCategory = self.mdc_adjustsFontForContentSizeCategory;
cell.backgroundColor = self.backgroundColor;
cell.actionFont = self.actionFont;
cell.accessibilityIdentifier = action.accessibilityIdentifier;
cell.inkColor = self.inkColor;
cell.tintColor = self.actionTintColor;
cell.imageRenderingMode = self.imageRenderingMode;
cell.actionTextColor = self.actionTextColor;
return cell;
}
- (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)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;
}
#pragma mark - Dynamic Type
- (void)mdc_setAdjustsFontForContentSizeCategory:(BOOL)adjusts {
_mdc_adjustsFontForContentSizeCategory = adjusts;
self.header.mdc_adjustsFontForContentSizeCategory = adjusts;
[self updateFontsForDynamicType];
if (_mdc_adjustsFontForContentSizeCategory) {
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(updateFontsForDynamicType)
name:UIContentSizeCategoryDidChangeNotification
object:nil];
} else {
[[NSNotificationCenter defaultCenter] removeObserver:self
name:UIContentSizeCategoryDidChangeNotification
object:nil];
}
[self.view setNeedsLayout];
}
- (void)updateTableFonts {
UIFont *finalActionsFont = _actionFont ?:
[UIFont mdc_standardFontForMaterialTextStyle:MDCFontTextStyleSubheadline];
if (self.mdc_adjustsFontForContentSizeCategory) {
finalActionsFont =
[finalActionsFont mdc_fontSizedForMaterialTextStyle:MDCFontTextStyleSubheadline
scaledForDynamicType:self.mdc_adjustsFontForContentSizeCategory];
}
_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)setInkColor:(UIColor *)inkColor {
_inkColor = inkColor;
[self.tableView reloadData];
}
@end