blob: f6dfbdc69f832797a6681d615de65999811bcb71 [file] [log] [blame] [edit]
// 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 <UIKit/UIKit.h>
#import "MDCAlertController+Customize.h"
#import "private/MDCAlertActionManager.h"
#import "private/MDCAlertControllerView+Private.h"
#import "private/MaterialDialogsStrings.h"
#import "private/MaterialDialogsStrings_table.h"
#import "MDCButton.h"
#import "MDCAlertControllerDelegate.h"
#import "MDCAlertControllerView.h"
#import "MDCDialogPresentationController.h"
#import "MDCDialogPresentationControllerDelegate.h"
#import "MDCDialogTransitionController.h"
#import "UIViewController+MaterialDialogs.h"
#import "UIView+MaterialElevationResponding.h"
#import "M3CButton.h"
#import "MDCShadowElevations.h"
#import "MDCMath.h"
NS_ASSUME_NONNULL_BEGIN
const int MAX_LAYOUT_PASSES = 10;
// The Bundle for string resources.
static NSString *const kMaterialDialogsBundle = @"MaterialDialogs.bundle";
@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];
_dismissOnAction = YES;
_emphasis = emphasis;
_tapHandler = [handler copy];
}
return self;
}
#pragma mark - NSCopying
- (id)copyWithZone:(__unused NSZone *_Nullable)zone {
MDCAlertAction *action = [[self class] actionWithTitle:self.title
emphasis:self.emphasis
handler:self.tapHandler];
action.accessibilityIdentifier = self.accessibilityIdentifier;
return action;
}
#pragma mark - NSObject
- (BOOL)isEqual:(id)object {
if (![object isKindOfClass:[MDCAlertAction class]]) {
return NO;
}
MDCAlertAction *anotherAction = (MDCAlertAction *)object;
if (self == anotherAction) {
return YES;
}
return
[self.title isEqualToString:anotherAction.title] && self.emphasis == anotherAction.emphasis;
}
- (NSUInteger)hash {
return self.title.hash ^ self.emphasis;
}
@end
@interface MDCAlertController () <UITextViewDelegate>
/**
A flag to determine whether to use `M3CButton` in place of `MDCButton`.
Defaults to NO, but we eventually want to default it to YES and remove this property altogether.
*/
@property(nonatomic) BOOL enableM3CButton;
@property(nonatomic, nullable, weak) MDCAlertControllerView *alertView;
@property(nonatomic, strong) MDCDialogTransitionController *transitionController;
@property(nonatomic, nonnull, strong) MDCAlertActionManager *actionManager;
@property(nonatomic, nullable, strong) UIView *titleIconView;
/**
This counter caps the maximum number of layout passes that can be done in a single layout cycle.
This variable is added as a direct fix for b/345505157.
*/
@property(nonatomic) int layoutPassCounter;
- (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;
NSString *_imageAccessibilityLabel;
BOOL _alignIconWithTitle;
}
@synthesize mdc_overrideBaseElevation = _mdc_overrideBaseElevation;
@synthesize mdc_elevationDidChangeBlock = _mdc_elevationDidChangeBlock;
@synthesize adjustsFontForContentSizeCategory = _adjustsFontForContentSizeCategory;
@synthesize titleIconView = _titleIconView;
@synthesize actionsHorizontalAlignment = _actionsHorizontalAlignment;
@synthesize actionsHorizontalAlignmentInVerticalLayout =
_actionsHorizontalAlignmentInVerticalLayout;
@synthesize orderVerticalActionsByEmphasis = _orderVerticalActionsByEmphasis;
@synthesize imageAccessibilityLabel = _imageAccessibilityLabel;
+ (instancetype)alertControllerWithTitle:(nullable NSString *)alertTitle
message:(nullable NSString *)message {
MDCAlertController *alertController = [[MDCAlertController alloc] initWithTitle:alertTitle
message:message];
return alertController;
}
+ (nonnull instancetype)alertControllerWithTitle:(nullable NSString *)alertTitle
attributedMessage:(nullable NSAttributedString *)attributedMessage {
MDCAlertController *alertController =
[[MDCAlertController alloc] initWithTitle:alertTitle attributedMessage:attributedMessage];
return alertController;
}
- (instancetype)init {
return [self initWithTitle:nil message:nil];
}
- (nonnull instancetype)initWithTitle:(nullable NSString *)title
message:(nullable NSString *)message {
self = [self initWithTitle:title];
if (self) {
_shouldPlaceAccessoryViewAboveMessage = NO;
_message = [message copy];
}
return self;
}
- (nonnull instancetype)initWithTitle:(nullable NSString *)title
attributedMessage:(nullable NSAttributedString *)attributedMessage {
self = [self initWithTitle:title];
if (self) {
_attributedMessage = [attributedMessage copy];
}
return self;
}
- (nonnull instancetype)initWithTitle:(nullable NSString *)title {
self = [super initWithNibName:nil bundle:nil];
if (self) {
_transitionController = [[MDCDialogTransitionController alloc] init];
_layoutPassCounter = 0;
_alertTitle = [title copy];
_titleAlignment = NSTextAlignmentNatural;
_messageAlignment = NSTextAlignmentNatural;
_titleIconAlignment = _titleAlignment;
_alignIconWithTitle = YES;
_orderVerticalActionsByEmphasis = NO;
_actionsHorizontalAlignment = MDCContentHorizontalAlignmentTrailing;
_actionsHorizontalAlignmentInVerticalLayout = MDCContentHorizontalAlignmentCenter;
_actionManager = [[MDCAlertActionManager alloc] init];
_shadowColor = UIColor.blackColor;
_mdc_overrideBaseElevation = -1;
_shouldAutorotateOverride = super.shouldAutorotate;
_supportedInterfaceOrientationsOverride = super.supportedInterfaceOrientations;
_preferredInterfaceOrientationForPresentationOverride =
super.preferredInterfaceOrientationForPresentation;
_modalTransitionStyleOverride = super.modalTransitionStyle;
_titlePinsToTop = YES;
_M3CButtonEnabled = NO;
[_actionManager setM3CButtonEnabled:_M3CButtonEnabled];
super.transitioningDelegate = _transitionController;
super.modalPresentationStyle = UIModalPresentationCustom;
}
return self;
}
- (void)setM3CButtonEnabled:(BOOL)enable {
if (_M3CButtonEnabled == enable) {
return;
}
_M3CButtonEnabled = enable;
[self.actionManager setM3CButtonEnabled:_M3CButtonEnabled];
[self.alertView setM3CButtonEnabled:_M3CButtonEnabled];
}
- (void)traitCollectionDidChange:(nullable UITraitCollection *)previousTraitCollection {
[super traitCollectionDidChange:previousTraitCollection];
if (self.traitCollectionDidChangeBlock) {
self.traitCollectionDidChangeBlock(self, previousTraitCollection);
}
if (![self.traitCollection.preferredContentSizeCategory
isEqualToString:previousTraitCollection.preferredContentSizeCategory]) {
self.preferredContentSize =
[self.alertView calculatePreferredContentSizeForBounds:CGRectInfinite.size];
[self.view setNeedsLayout];
}
}
/* Disable setter. Always use internal transition controller */
- (void)setTransitioningDelegate:
(__unused __nullable 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:(nullable NSString *)title {
_alertTitle = [title copy];
if (self.alertView) {
self.alertView.titleLabel.text = title;
self.preferredContentSize =
[self.alertView calculatePreferredContentSizeForBounds:CGRectInfinite.size];
}
}
- (nullable NSString *)title {
return _alertTitle;
}
- (void)setTitlePinsToTop:(BOOL)titlePinsToTop {
_titlePinsToTop = titlePinsToTop;
if (self.alertView) {
self.alertView.titlePinsToTop = titlePinsToTop;
}
}
- (void)setTitleAccessibilityLabel:(nullable NSString *)titleAccessibilityLabel {
_titleAccessibilityLabel = [titleAccessibilityLabel copy];
if (self.alertView && titleAccessibilityLabel) {
self.alertView.titleLabel.accessibilityLabel = titleAccessibilityLabel;
}
}
- (void)setMessage:(nullable NSString *)message {
_message = [message copy];
if (self.alertView) {
[self messageDidChange];
}
}
- (void)setAttributedMessage:(nullable NSAttributedString *)attributedMessage {
_attributedMessage = [attributedMessage copy];
if (self.alertView) {
[self messageDidChange];
}
}
- (void)setAttributedLinkColor:(nullable UIColor *)attributedLinkColor {
if ([_attributedLinkColor isEqual:attributedLinkColor]) {
return;
}
_attributedLinkColor = attributedLinkColor;
if (self.alertView) {
self.alertView.messageTextView.tintColor = attributedLinkColor;
}
}
- (void)messageDidChange {
if (self.attributedMessage.length > 0) {
self.alertView.messageTextView.attributedText = self.attributedMessage;
} else {
self.alertView.messageTextView.text = self.message;
}
self.preferredContentSize =
[self.alertView calculatePreferredContentSizeForBounds:CGRectInfinite.size];
self.alertView.messageTextView.accessibilityLabel =
self.messageAccessibilityLabel ?: self.message;
if ([self shouldUseAttributedStringForMessageA11Y]) {
self.alertView.messageTextView.accessibilityLabel = self.attributedMessage.string;
} else {
self.alertView.messageTextView.accessibilityValue = @"";
}
}
- (void)setMessageAccessibilityLabel:(nullable NSString *)messageAccessibilityLabel {
_messageAccessibilityLabel = [messageAccessibilityLabel copy];
if (self.alertView && messageAccessibilityLabel) {
self.alertView.messageTextView.accessibilityLabel = messageAccessibilityLabel;
}
}
- (void)setImageAccessibilityLabel:(nullable NSString *)imageAccessibilityLabel {
if ([_imageAccessibilityLabel isEqual:imageAccessibilityLabel]) {
return;
}
_imageAccessibilityLabel = [imageAccessibilityLabel copy];
if (self.alertView) {
self.alertView.titleIconImageView.accessibilityLabel = _imageAccessibilityLabel;
self.alertView.titleIconView.accessibilityLabel = _imageAccessibilityLabel;
}
}
- (nullable NSString *)imageAccessibilityLabel {
if (_imageAccessibilityLabel) {
return _imageAccessibilityLabel;
}
if (!self.alertView) {
return nil;
}
return (self.alertView.titleIconImageView != nil)
? self.alertView.titleIconImageView.accessibilityLabel
: self.alertView.titleIconView.accessibilityLabel;
}
- (void)setAccessoryView:(nullable UIView *)accessoryView {
if (_accessoryView == accessoryView) {
return;
}
_accessoryView = accessoryView;
if (self.alertView) {
self.alertView.accessoryView = accessoryView;
[self setAccessoryViewNeedsLayout];
}
}
- (void)setShouldPlaceAccessoryViewAboveMessage:(BOOL)shouldPlaceAccessoryViewAboveMessage {
if (_shouldPlaceAccessoryViewAboveMessage != shouldPlaceAccessoryViewAboveMessage) {
_shouldPlaceAccessoryViewAboveMessage = shouldPlaceAccessoryViewAboveMessage;
self.alertView.shouldPlaceAccessoryViewAboveMessage = shouldPlaceAccessoryViewAboveMessage;
}
}
- (void)setAccessoryViewNeedsLayout {
[self.alertView setNeedsLayout];
self.preferredContentSize =
[self.alertView calculatePreferredContentSizeForBounds:CGRectInfinite.size];
}
- (MDCDialogTransitionController *)dialogTransitionController {
return (MDCDialogTransitionController *)self.transitioningDelegate;
}
- (NSTimeInterval)presentationOpacityAnimationDuration {
return [self dialogTransitionController].opacityAnimationDuration;
}
- (void)setPresentationOpacityAnimationDuration:
(NSTimeInterval)presentationOpacityAnimationDuration {
[self dialogTransitionController].opacityAnimationDuration = presentationOpacityAnimationDuration;
}
- (NSTimeInterval)presentationScaleAnimationDuration {
return [self dialogTransitionController].scaleAnimationDuration;
}
- (void)setPresentationScaleAnimationDuration:(NSTimeInterval)presentationScaleAnimationDuration {
[self dialogTransitionController].scaleAnimationDuration = presentationScaleAnimationDuration;
}
- (CGFloat)presentationInitialScaleFactor {
return [self dialogTransitionController].dialogInitialScaleFactor;
}
- (void)setDialogEdgeInsets:(UIEdgeInsets)dialogEdgeInsets {
[self dialogTransitionController].dialogEdgeInsets = dialogEdgeInsets;
}
- (UIEdgeInsets)dialogEdgeInsets {
return [self dialogTransitionController].dialogEdgeInsets;
}
- (void)setPresentationInitialScaleFactor:(CGFloat)presentationInitialScaleFactor {
[self dialogTransitionController].dialogInitialScaleFactor = presentationInitialScaleFactor;
}
- (NSArray<MDCAlertAction *> *)actions {
return self.actionManager.actions;
}
- (void)addAction:(MDCAlertAction *)action {
[self.actionManager addAction:action];
[self addButtonToAlertViewForAction:action];
}
- (void)addActions:(NSArray<MDCAlertAction *> *)actions {
for (MDCAlertAction *action in actions) {
[self addAction:action];
}
}
- (nullable MDCButton *)buttonForAction:(nonnull MDCAlertAction *)action {
UIButton *button = [self.actionManager buttonForAction:action];
if (!button && [self.actionManager hasAction:action] && !self.isM3CButtonEnabled) {
button = [self.actionManager createButtonForAction:action
target:self
selector:@selector(actionButtonPressed:forEvent:)];
[MDCAlertControllerView styleAsTextButton:(MDCButton *)button];
}
if ([button isKindOfClass:[MDCButton class]]) {
return (MDCButton *)button;
}
return nil;
}
- (nullable M3CButton *)M3CButtonForAction:(nonnull MDCAlertAction *)action {
UIButton *button = [self.actionManager buttonForAction:action];
if (!button && [self.actionManager hasAction:action] && self.isM3CButtonEnabled) {
button = [self.actionManager createButtonForAction:action
target:self
selector:@selector(actionButtonPressed:forEvent:)];
}
if ([button isKindOfClass:[M3CButton class]]) {
return (M3CButton *)button;
}
return nil;
}
- (void)addButtonToAlertViewForAction:(MDCAlertAction *)action {
if (self.alertView) {
UIButton *button;
if (!self.isM3CButtonEnabled) {
button = [self buttonForAction:action];
} else {
button = [self M3CButtonForAction:action];
}
[self.alertView addActionButton:button];
[button setAccessibilityIdentifier:action.accessibilityIdentifier];
self.preferredContentSize =
[self.alertView calculatePreferredContentSizeForBounds:CGRectInfinite.size];
[self.alertView setNeedsLayout];
}
}
- (void)setActionsHorizontalAlignment:(MDCContentHorizontalAlignment)actionsHorizontalAlignment {
if (_actionsHorizontalAlignment == actionsHorizontalAlignment) {
return;
}
_actionsHorizontalAlignment = actionsHorizontalAlignment;
self.alertView.actionsHorizontalAlignment = actionsHorizontalAlignment;
}
- (void)setActionsHorizontalAlignmentInVerticalLayout:(MDCContentHorizontalAlignment)alignment {
if (_actionsHorizontalAlignmentInVerticalLayout == alignment) {
return;
}
_actionsHorizontalAlignmentInVerticalLayout = alignment;
self.alertView.actionsHorizontalAlignmentInVerticalLayout = alignment;
}
- (void)setOrderVerticalActionsByEmphasis:(BOOL)orderVerticalActionsByEmphasis {
if (_orderVerticalActionsByEmphasis == orderVerticalActionsByEmphasis) {
return;
}
_orderVerticalActionsByEmphasis = orderVerticalActionsByEmphasis;
self.alertView.orderVerticalActionsByEmphasis = orderVerticalActionsByEmphasis;
}
- (void)setTitleFont:(nullable UIFont *)titleFont {
_titleFont = titleFont;
if (self.alertView) {
self.alertView.titleFont = titleFont;
}
}
- (void)setMessageFont:(nullable UIFont *)messageFont {
_messageFont = messageFont;
if (self.alertView) {
self.alertView.messageFont = messageFont;
}
}
- (void)setTitleColor:(nullable UIColor *)titleColor {
_titleColor = titleColor;
if (self.alertView) {
self.alertView.titleColor = titleColor;
}
}
- (void)setMessageColor:(nullable UIColor *)messageColor {
_messageColor = messageColor;
if (self.alertView) {
self.alertView.messageColor = messageColor;
}
}
// b/117717380: Will be deprecated
- (void)setButtonTitleColor:(nullable UIColor *)buttonColor {
_buttonTitleColor = buttonColor;
if (self.alertView) {
self.alertView.buttonColor = buttonColor;
}
}
- (void)setTitleAlignment:(NSTextAlignment)titleAlignment {
_titleAlignment = titleAlignment;
if (_alignIconWithTitle) {
_titleIconAlignment = titleAlignment;
}
if (self.alertView) {
self.alertView.titleAlignment = titleAlignment;
if (_alignIconWithTitle) {
self.alertView.titleIconAlignment = titleAlignment;
}
}
}
- (void)setMessageAlignment:(NSTextAlignment)messageAlignment {
_messageAlignment = messageAlignment;
if (self.alertView) {
self.alertView.messageAlignment = messageAlignment;
}
}
- (void)setTitleIcon:(nullable UIImage *)titleIcon {
_titleIcon = titleIcon;
if (self.alertView) {
self.alertView.titleIcon = titleIcon;
self.preferredContentSize =
[self.alertView calculatePreferredContentSizeForBounds:CGRectInfinite.size];
}
}
- (void)setTitleIconView:(nullable UIView *)titleIconView {
_titleIconView = titleIconView;
if (self.alertView) {
self.alertView.titleIconView = titleIconView;
self.preferredContentSize =
[self.alertView calculatePreferredContentSizeForBounds:CGRectInfinite.size];
}
}
- (void)setTitleIconTintColor:(nullable UIColor *)titleIconTintColor {
_titleIconTintColor = titleIconTintColor;
if (self.alertView) {
self.alertView.titleIconTintColor = titleIconTintColor;
}
}
- (nullable UIImageView *)titleIconImageView {
return self.alertView.titleIconImageView;
}
- (void)setTitleIconAlignment:(NSTextAlignment)titleIconAlignment {
_alignIconWithTitle = NO;
_titleIconAlignment = titleIconAlignment;
if (self.alertView) {
self.alertView.titleIconAlignment = titleIconAlignment;
}
}
- (void)setScrimColor:(nullable UIColor *)scrimColor {
_scrimColor = scrimColor;
self.mdc_dialogPresentationController.scrimColor = scrimColor;
}
- (void)setBackgroundColor:(nullable UIColor *)backgroundColor {
_backgroundColor = backgroundColor;
if (self.alertView) {
self.alertView.backgroundColor = backgroundColor;
}
}
- (void)setCornerRadius:(CGFloat)cornerRadius {
_cornerRadius = cornerRadius;
if (self.alertView) {
self.alertView.cornerRadius = cornerRadius;
}
self.mdc_dialogPresentationController.dialogCornerRadius = cornerRadius;
}
- (void)setElevation:(MDCShadowElevation)elevation {
BOOL shouldNotifyChanges = !MDCCGFloatEqual(elevation, _elevation);
_elevation = elevation;
self.mdc_dialogPresentationController.dialogElevation = elevation;
if (shouldNotifyChanges) {
[self.view mdc_elevationDidChange];
}
}
- (CGFloat)mdc_currentElevation {
return self.elevation;
}
- (void)setShadowColor:(UIColor *)shadowColor {
UIColor *shadowColorCopy = [shadowColor copy];
_shadowColor = shadowColorCopy;
self.mdc_dialogPresentationController.dialogShadowColor = shadowColorCopy;
}
- (void)setAdjustsFontForContentSizeCategory:(BOOL)adjustsFontForContentSizeCategory {
_adjustsFontForContentSizeCategory = adjustsFontForContentSizeCategory;
if (self.viewLoaded) {
self.alertView.adjustsFontForContentSizeCategory = adjustsFontForContentSizeCategory;
}
}
// 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 forEvent:(UIEvent *)event {
MDCAlertAction *action = [self.actionManager actionForButton:button];
if ([self.delegate respondsToSelector:@selector(alertController:didTapAction:withEvent:)]) {
[self.delegate alertController:self didTapAction:action withEvent:event];
}
if (action.dismissOnAction) {
// We call our action.tapHandler 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.tapHandler) {
action.tapHandler(action);
}
}];
} else {
if (action.tapHandler) {
action.tapHandler(action);
}
}
}
#pragma mark - Text View Delegate
- (BOOL)textView:(UITextView *)textView
shouldInteractWithURL:(NSURL *)URL
inRange:(NSRange)characterRange
interaction:(UITextItemInteraction)interaction {
if (self.attributedMessageAction != nil) {
return self.attributedMessageAction(URL, characterRange, interaction);
}
return YES;
}
#pragma mark - UIViewController
- (void)loadView {
self.view = [[MDCAlertControllerView alloc] initWithFrame:CGRectZero];
self.alertView = (MDCAlertControllerView *)self.view;
[self.alertView setM3CButtonEnabled:self.isM3CButtonEnabled];
[self.alertView
setShouldPlaceAccessoryViewAboveMessage:self.shouldPlaceAccessoryViewAboveMessage];
// sharing MDCActionManager with with the alert view
self.alertView.actionManager = self.actionManager;
}
- (void)viewDidLoad {
[super viewDidLoad];
[self setupAlertView];
_previousLayoutSize = CGSizeZero;
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)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
if ([self.delegate respondsToSelector:@selector(alertController:willAppear:)]) {
[self.delegate alertController:self willAppear:animated];
}
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
if ([self.delegate respondsToSelector:@selector(alertController:didAppear:)]) {
[self.delegate alertController:self didAppear:animated];
}
[self.alertView.contentScrollView flashScrollIndicators];
[self.alertView.actionsScrollView flashScrollIndicators];
}
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
if ([self.delegate respondsToSelector:@selector(alertController:willDisappear:)]) {
[self.delegate alertController:self willDisappear:animated];
}
}
- (void)viewDidDisappear:(BOOL)animated {
[super viewDidDisappear:animated];
if ([self.delegate respondsToSelector:@selector(alertController:didDisappear:)]) {
[self.delegate alertController:self didDisappear:animated];
}
}
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
// Increments the counter to account for an additional layout pass.
self.layoutPassCounter += 1;
// Abort if the layout pass counter is too high.
if (self.layoutPassCounter > MAX_LAYOUT_PASSES) {
// Resets counter.
self.layoutPassCounter = 0;
return;
}
// Recalculate preferredContentSize and potentially the view frame.
BOOL boundsSizeChanged =
!CGSizeEqualToSize(CGRectStandardize(self.view.bounds).size, _previousLayoutSize);
// UIContentSizeCategoryAdjusting behavior only updates fonts after -viewWillLayoutSubviews and
// before -viewDidLayoutSubviews. Because `preferredContentSize` may have changed as a result,
// it is necessary to check if it changed here and possibly require a second layout pass.
CGSize currentPreferredContentSize = self.preferredContentSize;
CGSize calculatedPreferredContentSize = [self.alertView
calculatePreferredContentSizeForBounds:CGRectStandardize(self.alertView.bounds).size];
BOOL preferredContentSizeChanged =
!CGSizeEqualToSize(currentPreferredContentSize, calculatedPreferredContentSize);
if (preferredContentSizeChanged) {
// NOTE: Setting the preferredContentSize can lead to a change to self.view.bounds.
self.preferredContentSize = calculatedPreferredContentSize;
}
if (preferredContentSizeChanged || boundsSizeChanged) {
_previousLayoutSize = CGRectStandardize(self.alertView.bounds).size;
[self.view setNeedsLayout];
[self.view layoutIfNeeded];
}
}
- (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) {
[self.alertView setNeedsLayout];
// Reset preferredContentSize on viewWillTransition to take advantage of additional width.
self.preferredContentSize =
[self.alertView calculatePreferredContentSizeForBounds:CGRectInfinite.size];
}
completion:nil];
}
- (BOOL)shouldAutorotate {
return _shouldAutorotateOverride;
}
- (UIInterfaceOrientationMask)supportedInterfaceOrientations {
return _supportedInterfaceOrientationsOverride;
}
- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation {
return _preferredInterfaceOrientationForPresentationOverride;
}
- (UIModalTransitionStyle)modalTransitionStyle {
return _modalTransitionStyleOverride;
}
#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];
}
#pragma mark - Setup Alert View
- (void)setupAlertView {
self.alertView.titleLabel.text = self.title;
self.alertView.adjustsFontForContentSizeCategory = self.adjustsFontForContentSizeCategory;
if (self.attributedMessage.length > 0) {
self.alertView.messageTextView.attributedText = self.attributedMessage;
} else {
self.alertView.messageTextView.text = self.message;
}
self.alertView.titleLabel.accessibilityLabel = self.titleAccessibilityLabel ?: self.title;
self.alertView.messageTextView.accessibilityLabel =
self.messageAccessibilityLabel ?: self.message;
// Set messageTextView's accessibilityValue to the empty string to resolve b/158732017.
// For a plain string, the MessageTextView acts as a label and should not have an
// accessibilityValue. Setting the accessibilityValue to nil causes VoiceOver to use the default
// value, which is the text of the message, so the value must be set to the empty string instead.
// This is not the case for attributed strings as disabling the accessibilityValue will not call
// out attribute traits, e.g (embedded links).
if ([self shouldUseAttributedStringForMessageA11Y]) {
self.alertView.messageTextView.accessibilityLabel = self.attributedMessage.accessibilityLabel;
} else {
self.alertView.messageTextView.accessibilityValue = @"";
}
self.alertView.messageTextView.delegate = self;
self.alertView.titleIconImageView.accessibilityLabel = self.imageAccessibilityLabel;
self.alertView.titleIconView.accessibilityLabel = self.imageAccessibilityLabel;
// TODO(https://github.com/material-components/material-components-ios/issues/8671): Update
// adjustsFontForContentSizeCategory for messageTextView
self.alertView.accessoryView = self.accessoryView;
self.alertView.titleFont = self.titleFont;
self.alertView.messageFont = self.messageFont;
self.alertView.titleColor = self.titleColor ?: UIColor.blackColor;
self.alertView.messageTextView.tintColor = self.attributedLinkColor;
if (self.attributedMessage.length > 0) {
// Avoid overriding `messageColor` during initialization, to allow the attributed messages's
// foregroundColor to take precedence in case `messageColor` was not set.
if (self.messageColor != nil) {
self.alertView.messageColor = self.messageColor;
}
} else {
self.alertView.messageColor = self.messageColor ?: UIColor.blackColor;
}
if (self.backgroundColor) {
// Avoid reset background color to transparent when self.backgroundColor is nil.
self.alertView.backgroundColor = self.backgroundColor;
}
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
}
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.messageAlignment = self.messageAlignment;
self.alertView.titleIcon = self.titleIcon;
self.alertView.titleIconTintColor = self.titleIconTintColor;
self.alertView.titleIconAlignment = self.titleIconAlignment;
self.alertView.titleIconView = self.titleIconView;
self.alertView.cornerRadius = self.cornerRadius;
self.alertView.enableRippleBehavior = self.enableRippleBehavior;
self.alertView.orderVerticalActionsByEmphasis = self.orderVerticalActionsByEmphasis;
self.alertView.actionsHorizontalAlignment = self.actionsHorizontalAlignment;
self.alertView.actionsHorizontalAlignmentInVerticalLayout =
self.actionsHorizontalAlignmentInVerticalLayout;
self.alertView.titlePinsToTop = self.titlePinsToTop;
// Create buttons for the actions (if not already created) and apply default styling
for (MDCAlertAction *action in self.actions) {
[self addButtonToAlertViewForAction:action];
}
}
#pragma mark - UIAccessibilityAction
- (BOOL)accessibilityPerformEscape {
MDCDialogPresentationController *dialogPresentationController =
self.mdc_dialogPresentationController;
if (dialogPresentationController.dismissOnBackgroundTap) {
BOOL shouldDismiss = YES;
if ([dialogPresentationController.dialogPresentationControllerDelegate
respondsToSelector:@selector(dialogPresentationControllerShouldDismiss:)]) {
shouldDismiss = [dialogPresentationController.dialogPresentationControllerDelegate
dialogPresentationControllerShouldDismiss:dialogPresentationController];
}
if (!shouldDismiss) {
return NO;
}
if ([dialogPresentationController.dialogPresentationControllerDelegate
respondsToSelector:@selector(dialogPresentationControllerWillDismiss:)]) {
[dialogPresentationController.dialogPresentationControllerDelegate
dialogPresentationControllerWillDismiss:dialogPresentationController];
}
void (^dismissalCompletion)(void) = ^{
if ([dialogPresentationController.dialogPresentationControllerDelegate
respondsToSelector:@selector(dialogPresentationControllerDidDismiss:)]) {
[dialogPresentationController.dialogPresentationControllerDelegate
dialogPresentationControllerDidDismiss:dialogPresentationController];
}
};
[self.presentingViewController dismissViewControllerAnimated:YES
completion:dismissalCompletion];
return YES;
}
return NO;
}
#pragma mark - Private
/// Returns YES if the attributed message should be used as an accessibility label of the alert
/// message.
/// This happens when the attributed message was explicitly provided without a custom accessibility
/// label.
- (BOOL)shouldUseAttributedStringForMessageA11Y {
return !(self.messageAccessibilityLabel || self.message) && self.attributedMessage;
}
@end
NS_ASSUME_NONNULL_END