blob: beb96ecb37cb720b72fbdd868e74df5a02403d8f [file] [log] [blame]
// Copyright 2016-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 "MDCAlertController.h"
#import <MDFInternationalization/MDFInternationalization.h>
#import "MDCAlertControllerView.h"
#import "MDCDialogPresentationController.h"
#import "MDCDialogTransitionController.h"
#import "MaterialButtons.h"
#import "MaterialTypography.h"
#import "UIViewController+MaterialDialogs.h"
#import "private/MDCAlertActionManager.h"
#import "private/MDCAlertControllerView+Private.h"
#import "private/MaterialDialogsStrings.h"
#import "private/MaterialDialogsStrings_table.h"
// The Bundle for string resources.
static NSString *const kMaterialDialogsBundle = @"MaterialDialogs.bundle";
@interface MDCAlertAction ()
@property(nonatomic, nullable, copy) MDCActionHandler completionHandler;
@end
@implementation MDCAlertAction
+ (instancetype)actionWithTitle:(nonnull NSString *)title
handler:(void (^__nullable)(MDCAlertAction *action))handler {
return [[MDCAlertAction alloc] initWithTitle:title emphasis:MDCActionEmphasisLow handler:handler];
}
+ (instancetype)actionWithTitle:(nonnull NSString *)title
emphasis:(MDCActionEmphasis)emphasis
handler:(void (^__nullable)(MDCAlertAction *action))handler {
return [[MDCAlertAction alloc] initWithTitle:title emphasis:emphasis handler:handler];
}
- (instancetype)initWithTitle:(nonnull NSString *)title
emphasis:(MDCActionEmphasis)emphasis
handler:(void (^__nullable)(MDCAlertAction *action))handler {
self = [super init];
if (self) {
_title = [title copy];
_emphasis = emphasis;
_completionHandler = [handler copy];
}
return self;
}
- (id)copyWithZone:(__unused NSZone *)zone {
MDCAlertAction *action = [[self class] actionWithTitle:self.title
emphasis:self.emphasis
handler:self.completionHandler];
action.accessibilityIdentifier = self.accessibilityIdentifier;
return action;
}
@end
@interface MDCAlertController ()
@property(nonatomic, nullable, weak) MDCAlertControllerView *alertView;
@property(nonatomic, strong) MDCDialogTransitionController *transitionController;
@property(nonatomic, nonnull, strong) MDCAlertActionManager *actionManager;
- (nonnull instancetype)initWithTitle:(nullable NSString *)title
message:(nullable NSString *)message;
@end
@implementation MDCAlertController {
// This is because title is overlapping with view controller title, However Apple alertController
// redefines title as well.
NSString *_alertTitle;
CGSize _previousLayoutSize;
BOOL _mdc_adjustsFontForContentSizeCategory;
}
+ (instancetype)alertControllerWithTitle:(nullable NSString *)alertTitle
message:(nullable NSString *)message {
MDCAlertController *alertController =
[[MDCAlertController alloc] initWithTitle:alertTitle message:message];
return alertController;
}
- (instancetype)init {
return [self initWithTitle:nil message:nil];
}
- (nonnull instancetype)initWithTitle:(nullable NSString *)title
message:(nullable NSString *)message {
self = [super initWithNibName:nil bundle:nil];
if (self) {
_transitionController = [[MDCDialogTransitionController alloc] init];
_alertTitle = [title copy];
_message = [message copy];
_titleAlignment = NSTextAlignmentNatural;
_actionManager = [[MDCAlertActionManager alloc] init];
super.transitioningDelegate = _transitionController;
super.modalPresentationStyle = UIModalPresentationCustom;
}
return self;
}
/* Disable setter. Always use internal transition controller */
- (void)setTransitioningDelegate:
(__unused id<UIViewControllerTransitioningDelegate>)transitioningDelegate {
NSAssert(NO, @"MDCAlertController.transitioningDelegate cannot be changed.");
return;
}
/* Disable setter. Always use custom presentation style */
- (void)setModalPresentationStyle:(__unused UIModalPresentationStyle)modalPresentationStyle {
NSAssert(NO, @"MDCAlertController.modalPresentationStyle cannot be changed.");
return;
}
- (void)setTitle:(NSString *)title {
_alertTitle = [title copy];
if (self.alertView) {
self.alertView.titleLabel.text = title;
self.preferredContentSize =
[self.alertView calculatePreferredContentSizeForBounds:CGRectInfinite.size];
}
}
- (NSString *)title {
return _alertTitle;
}
- (void)setMessage:(NSString *)message {
_message = [message copy];
if (self.alertView) {
self.alertView.messageLabel.text = message;
self.preferredContentSize =
[self.alertView calculatePreferredContentSizeForBounds:CGRectInfinite.size];
}
}
- (NSArray<MDCAlertAction *> *)actions {
return self.actionManager.actions;
}
- (void)addAction:(MDCAlertAction *)action {
[self.actionManager addAction:action];
[self addButtonToAlertViewForAction:action];
}
- (nullable MDCButton *)buttonForAction:(nonnull MDCAlertAction *)action {
MDCButton *button = [self.actionManager buttonForAction:action];
if (!button && [self.actionManager hasAction:action]) {
button = [self.actionManager createButtonForAction:action
target:self
selector:@selector(actionButtonPressed:)];
[MDCAlertControllerView styleAsTextButton:button];
}
return button;
}
- (void)addButtonToAlertViewForAction:(MDCAlertAction *)action {
if (self.alertView) {
MDCButton *button = [self buttonForAction:action];
[self.alertView addActionButton:button];
self.preferredContentSize =
[self.alertView calculatePreferredContentSizeForBounds:CGRectInfinite.size];
[self.alertView setNeedsLayout];
}
}
- (void)setTitleFont:(UIFont *)titleFont {
_titleFont = titleFont;
if (self.alertView) {
self.alertView.titleFont = titleFont;
}
}
- (void)setMessageFont:(UIFont *)messageFont {
_messageFont = messageFont;
if (self.alertView) {
self.alertView.messageFont = messageFont;
}
}
// b/117717380: Will be deprecated
- (void)setButtonFont:(UIFont *)buttonFont {
_buttonFont = buttonFont;
if (self.alertView) {
self.alertView.buttonFont = buttonFont;
}
}
- (void)setTitleColor:(UIColor *)titleColor {
_titleColor = titleColor;
if (self.alertView) {
self.alertView.titleColor = titleColor;
}
}
- (void)setMessageColor:(UIColor *)messageColor {
_messageColor = messageColor;
if (self.alertView) {
self.alertView.messageColor = messageColor;
}
}
// b/117717380: Will be deprecated
- (void)setButtonTitleColor:(UIColor *)buttonColor {
_buttonTitleColor = buttonColor;
if (self.alertView) {
self.alertView.buttonColor = buttonColor;
}
}
- (void)setTitleAlignment:(NSTextAlignment)titleAlignment {
_titleAlignment = titleAlignment;
if (self.alertView) {
self.alertView.titleAlignment = titleAlignment;
}
}
- (void)setTitleIcon:(UIImage *)titleIcon {
_titleIcon = titleIcon;
if (self.alertView) {
self.alertView.titleIcon = titleIcon;
}
}
- (void)setTitleIconTintColor:(UIColor *)titleIconTintColor {
_titleIconTintColor = titleIconTintColor;
if (self.alertView) {
self.alertView.titleIconTintColor = titleIconTintColor;
}
}
- (void)setScrimColor:(UIColor *)scrimColor {
_scrimColor = scrimColor;
self.mdc_dialogPresentationController.scrimColor = scrimColor;
}
- (void)setCornerRadius:(CGFloat)cornerRadius {
_cornerRadius = cornerRadius;
if (self.alertView) {
self.alertView.cornerRadius = cornerRadius;
}
self.mdc_dialogPresentationController.dialogCornerRadius = cornerRadius;
}
- (void)setElevation:(MDCShadowElevation)elevation {
_elevation = elevation;
self.mdc_dialogPresentationController.dialogElevation = elevation;
}
- (void)mdc_setAdjustsFontForContentSizeCategory:(BOOL)adjusts {
_mdc_adjustsFontForContentSizeCategory = adjusts;
if (self.alertView) {
self.alertView.mdc_adjustsFontForContentSizeCategory = adjusts;
[self updateFontsForDynamicType];
}
if (_mdc_adjustsFontForContentSizeCategory) {
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(contentSizeCategoryDidChange:)
name:UIContentSizeCategoryDidChangeNotification
object:nil];
} else {
[[NSNotificationCenter defaultCenter] removeObserver:self
name:UIContentSizeCategoryDidChangeNotification
object:nil];
}
}
// Handles UIContentSizeCategoryDidChangeNotifications
- (void)contentSizeCategoryDidChange:(__unused NSNotification *)notification {
[self updateFontsForDynamicType];
}
// Update the fonts used based on mdc_preferredFontForMaterialTextStyle and recalculate the
// preferred content size.
- (void)updateFontsForDynamicType {
if (self.alertView) {
[self.alertView updateFonts];
// Our presentation controller reacts to changes to preferredContentSize to determine our
// frame at the presented controller.
self.preferredContentSize =
[self.alertView calculatePreferredContentSizeForBounds:CGRectInfinite.size];
}
}
- (void)actionButtonPressed:(id)button {
MDCAlertAction *action = [self.actionManager actionForButton:button];
// We call our action.completionHandler after we dismiss the existing alert in case the handler
// also presents a view controller. Otherwise we get a warning about presenting on a controller
// which is already presenting.
[self.presentingViewController dismissViewControllerAnimated:YES completion:^(void){
if (action.completionHandler) {
action.completionHandler(action);
}
}];
}
#pragma mark - UIViewController
- (void)loadView {
self.view = [[MDCAlertControllerView alloc] initWithFrame:CGRectZero];
self.alertView = (MDCAlertControllerView *)self.view;
// sharing MDCActionManager with with the alert view
self.alertView.actionManager = self.actionManager;
}
- (void)viewDidLoad {
[super viewDidLoad];
[self setupAlertView];
_previousLayoutSize = CGSizeZero;
CGSize idealSize = [self.alertView calculatePreferredContentSizeForBounds:CGRectInfinite.size];
self.preferredContentSize = idealSize;
self.preferredContentSize =
[self.alertView calculatePreferredContentSizeForBounds:CGRectInfinite.size];
[self.view setNeedsLayout];
NSString *key =
kMaterialDialogsStringTable[kStr_MaterialDialogsPresentedAccessibilityAnnouncement];
NSString *announcement = NSLocalizedStringFromTableInBundle(key,
kMaterialDialogsStringsTableName,
[[self class] bundle],
@"Alert");
UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification,
announcement);
}
- (void)setupAlertView {
// Explicitly overwrite the view default if true
if (_mdc_adjustsFontForContentSizeCategory) {
self.alertView.mdc_adjustsFontForContentSizeCategory = YES;
}
self.alertView.titleLabel.text = self.title;
self.alertView.messageLabel.text = self.message;
self.alertView.titleFont = self.titleFont;
self.alertView.messageFont = self.messageFont;
self.alertView.titleColor = self.titleColor;
self.alertView.messageColor = self.messageColor;
if (self.buttonTitleColor) {
// Avoid reset title color to white when setting it to nil. only set it for an actual UIColor.
self.alertView.buttonColor = self.buttonTitleColor; // b/117717380: Will be deprecated
}
self.alertView.buttonFont = self.buttonFont; // b/117717380: Will be deprecated
if (self.buttonInkColor) {
// Avoid reset ink color to white when setting it to nil. only set it for an actual UIColor.
self.alertView.buttonInkColor = self.buttonInkColor; // b/117717380: Will be deprecated
}
self.alertView.titleAlignment = self.titleAlignment;
self.alertView.titleIcon = self.titleIcon;
self.alertView.titleIconTintColor = self.titleIconTintColor;
self.alertView.cornerRadius = self.cornerRadius;
// Create buttons for the actions (if not already created) and apply default styling
for (MDCAlertAction *action in self.actions) {
[self addButtonToAlertViewForAction:action];
}
}
- (void)viewDidLayoutSubviews {
// Recalculate preferredSize, which is based on width available, if the viewSize has changed.
if (CGRectGetWidth(self.view.bounds) != _previousLayoutSize.width ||
CGRectGetHeight(self.view.bounds) != _previousLayoutSize.height) {
CGSize currentPreferredContentSize = self.preferredContentSize;
CGSize calculatedPreferredContentSize =
[self.alertView calculatePreferredContentSizeForBounds:CGRectStandardize(self.alertView.bounds).size];
if (!CGSizeEqualToSize(currentPreferredContentSize, calculatedPreferredContentSize)) {
// NOTE: Setting the preferredContentSize can lead to a change to self.view.bounds.
self.preferredContentSize = calculatedPreferredContentSize;
}
_previousLayoutSize = CGRectStandardize(self.alertView.bounds).size;
}
}
#pragma mark - Resource bundle
+ (NSBundle *)bundle {
static NSBundle *bundle = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
bundle = [NSBundle bundleWithPath:[self bundlePathWithName:kMaterialDialogsBundle]];
});
return bundle;
}
+ (NSString *)bundlePathWithName:(NSString *)bundleName {
// In iOS 8+, we could be included by way of a dynamic framework, and our resource bundles may
// not be in the main .app bundle, but rather in a nested framework, so figure out where we live
// and use that as the search location.
NSBundle *bundle = [NSBundle bundleForClass:[MDCAlertController class]];
NSString *resourcePath = [(nil == bundle ? [NSBundle mainBundle] : bundle) resourcePath];
return [resourcePath stringByAppendingPathComponent:bundleName];
}
- (void)viewWillLayoutSubviews {
[super viewWillLayoutSubviews];
// Recalculate preferredSize, which is based on width available, if the viewSize has changed.
if (CGRectGetWidth(self.view.bounds) != _previousLayoutSize.width ||
CGRectGetHeight(self.view.bounds) != _previousLayoutSize.height) {
CGSize currentPreferredContentSize = self.preferredContentSize;
CGSize contentSize = CGRectStandardize(self.alertView.bounds).size;
CGSize calculatedPreferredContentSize =
[self.alertView calculatePreferredContentSizeForBounds:contentSize];
if (!CGSizeEqualToSize(currentPreferredContentSize, calculatedPreferredContentSize)) {
// NOTE: Setting the preferredContentSize can lead to a change to self.view.bounds.
self.preferredContentSize = calculatedPreferredContentSize;
}
_previousLayoutSize = CGRectStandardize(self.alertView.bounds).size;
}
}
- (void)viewWillTransitionToSize:(CGSize)size
withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator {
[super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
[coordinator animateAlongsideTransition:
^(__unused id<UIViewControllerTransitionCoordinatorContext> _Nonnull context) {
// Reset preferredContentSize on viewWIllTransition to take advantage of additional width
self.preferredContentSize =
[self.alertView calculatePreferredContentSizeForBounds:CGRectInfinite.size];
}
completion:nil];
}
#pragma mark - UIAccessibilityAction
- (BOOL)accessibilityPerformEscape {
MDCDialogPresentationController *dialogPresentationController =
self.mdc_dialogPresentationController;
if (dialogPresentationController.dismissOnBackgroundTap) {
[self.presentingViewController dismissViewControllerAnimated:YES completion:NULL];
return YES;
}
return NO;
}
@end