| // 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 <QuartzCore/QuartzCore.h> |
| |
| #import "MDCSnackbarManager.h" |
| #import "MDCSnackbarMessage.h" |
| #import "MDCSnackbarMessageView.h" |
| |
| #import "MaterialAnimationTiming.h" |
| #import "MaterialMath.h" |
| #import "MaterialShadowLayer.h" |
| #import "MaterialTypography.h" |
| #import "private/MDCSnackbarMessageViewInternal.h" |
| #import "private/MDCSnackbarOverlayView.h" |
| #import "private/MaterialSnackbarStrings.h" |
| #import "private/MaterialSnackbarStrings_table.h" |
| |
| NSString *const MDCSnackbarMessageTitleAutomationIdentifier = |
| @"MDCSnackbarMessageTitleAutomationIdentifier"; |
| |
| // The Bundle for string resources. |
| static NSString *const kMaterialSnackbarBundle = @"MaterialSnackbar.bundle"; |
| |
| static inline UIColor *MDCRGBAColor(uint8_t r, uint8_t g, uint8_t b, float a) { |
| return [UIColor colorWithRed:(r) / (CGFloat)255 |
| green:(g) / (CGFloat)255 |
| blue:(b) / (CGFloat)255 |
| alpha:(a)]; |
| } |
| |
| /** Test whether any of the accessibility elements of a view is focused */ |
| static BOOL UIViewHasFocusedAccessibilityElement(UIView *view) { |
| for (NSInteger i = 0; i < [view accessibilityElementCount]; i++) { |
| id accessibilityElement = [view accessibilityElementAtIndex:i]; |
| if ([accessibilityElement accessibilityElementIsFocused]) { |
| return YES; |
| } |
| } |
| return NO; |
| }; |
| |
| /** |
| The thickness of the Snackbar border. |
| */ |
| static const CGFloat kBorderWidth = 0; |
| |
| /** |
| The radius of the corners. |
| */ |
| static const CGFloat kCornerRadius = 4; |
| static const CGFloat kLegacyCornerRadius = 0; |
| |
| /** |
| Padding between the edges of the Snackbar and any content. |
| */ |
| static UIEdgeInsets kContentMargin = (UIEdgeInsets){16.0, 16.0, 16.0, 8.0}; |
| static UIEdgeInsets kLegacyContentMargin = (UIEdgeInsets){18.0, 24.0, 18.0, 24.0}; |
| |
| /** |
| Padding between the image and the main title. |
| */ |
| static const CGFloat kTitleImagePadding = 8; |
| |
| /** |
| Padding between the main title and the first button. |
| */ |
| static const CGFloat kTitleButtonPadding = 8; |
| |
| /** |
| Padding on the edges of the buttons. |
| */ |
| static const CGFloat kLegacyButtonPadding = 5; |
| static const CGFloat kButtonPadding = 8; |
| |
| /** |
| Minimum padding for the vertical padding of the buttons to the Snackbar |
| */ |
| static const CGFloat kMinVerticalButtonPadding = 6; |
| |
| /** |
| The width of the Snackbar. |
| */ |
| static const CGFloat kMinimumViewWidth_iPad = 288; |
| static const CGFloat kMaximumViewWidth_iPad = 568; |
| static const CGFloat kMinimumViewWidth_iPhone = 320; |
| static const CGFloat kMaximumViewWidth_iPhone = 320; |
| |
| /** |
| The minimum height of the Snackbar. |
| */ |
| static const CGFloat kMinimumHeight = 48; |
| |
| /** |
| Each button will have a tag indexed starting from this value. |
| */ |
| static const NSInteger kButtonTagStart = 20000; |
| |
| static const MDCFontTextStyle kMessageTextStyle = MDCFontTextStyleBody1; |
| static const MDCFontTextStyle kButtonTextStyle = MDCFontTextStyleButton; |
| |
| #if defined(__IPHONE_10_0) && (__IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_10_0) |
| @interface MDCSnackbarMessageView () <CAAnimationDelegate> |
| @end |
| #endif |
| |
| @interface MDCSnackbarMessageView () |
| |
| /** |
| Holds the icon for the image. |
| */ |
| @property(nonatomic, strong) UIImageView *imageView; |
| |
| /** |
| Holds the button views. |
| */ |
| @property(nonatomic, strong) NSArray *buttons; |
| |
| /** |
| Holds the text label for the main message. |
| */ |
| @property(nonatomic, strong) UILabel *label; |
| |
| /** |
| The constraints managing this view. |
| */ |
| @property(nonatomic, strong) NSArray *viewConstraints; |
| |
| /** |
| The view containing all of the visual content. Inset by @c kBorderWidth from the view. |
| */ |
| @property(nonatomic, strong) UIControl *containerView; |
| |
| /** |
| The view containing all of the buttons. |
| */ |
| @property(nonatomic, strong) UIView *buttonView; |
| |
| /** |
| The view containing the title and image views. |
| */ |
| @property(nonatomic, strong) UIView *contentView; |
| |
| /** |
| An invisible hit target to ensure that tapping the horizontal area between the button and the edge |
| of the screen triggers a button tap instead of dismissing the snackbar. |
| */ |
| @property(nonatomic, strong) UIControl *buttonGutterTapTarget; |
| |
| /** |
| Holds onto the dismissal handler, called when the Snackbar should dismiss due to user interaction. |
| */ |
| @property(nonatomic, copy) MDCSnackbarMessageDismissHandler dismissalHandler; |
| |
| @end |
| |
| @interface MDCSnackbarMessageViewButton : MDCFlatButton |
| @end |
| |
| @implementation MDCSnackbarMessageViewButton |
| |
| - (instancetype)initWithFrame:(CGRect)frame { |
| self = [super initWithFrame:frame]; |
| if (self) { |
| self.inkColor = [UIColor colorWithWhite:1 alpha:(CGFloat)0.06]; |
| |
| CGFloat buttonContentPadding = |
| MDCSnackbarMessage.usesLegacySnackbar ? kLegacyButtonPadding : kButtonPadding; |
| [self setTranslatesAutoresizingMaskIntoConstraints:NO]; |
| self.tag = kButtonTagStart; |
| |
| // Style the text in the button. |
| self.titleLabel.numberOfLines = 1; |
| self.contentHorizontalAlignment = UIControlContentHorizontalAlignmentRight; |
| self.contentVerticalAlignment = UIControlContentVerticalAlignmentCenter; |
| self.contentEdgeInsets = UIEdgeInsetsMake(buttonContentPadding, buttonContentPadding, |
| buttonContentPadding, buttonContentPadding); |
| |
| // Make sure the button doesn't get too compressed. |
| [self setContentCompressionResistancePriority:UILayoutPriorityRequired |
| forAxis:UILayoutConstraintAxisHorizontal]; |
| [self setContentHuggingPriority:UILayoutPriorityDefaultHigh |
| forAxis:UILayoutConstraintAxisHorizontal]; |
| } |
| return self; |
| } |
| |
| @end |
| |
| @implementation MDCSnackbarMessageView { |
| UIFont *_messageFont; |
| UIFont *_buttonFont; |
| |
| // Holds the instances of MDCButton |
| NSMutableArray<MDCButton *> *_actionButtons; |
| |
| NSMutableDictionary<NSNumber *, UIColor *> *_buttonTitleColors; |
| |
| BOOL _mdc_adjustsFontForContentSizeCategory; |
| } |
| |
| @synthesize mdc_overrideBaseElevation = _mdc_overrideBaseElevation; |
| @synthesize mdc_elevationDidChangeBlock = _mdc_elevationDidChangeBlock; |
| |
| - (instancetype)initWithFrame:(CGRect)frame { |
| return [self initWithMessage:nil |
| dismissHandler:nil |
| snackbarManager:MDCSnackbarManager.defaultManager]; |
| } |
| |
| - (instancetype)initWithMessage:(MDCSnackbarMessage *)message |
| dismissHandler:(MDCSnackbarMessageDismissHandler)handler |
| snackbarManager:(MDCSnackbarManager *)manager { |
| self = [super initWithFrame:CGRectZero]; |
| |
| if (self) { |
| _snackbarMessageViewShadowColor = manager.snackbarMessageViewShadowColor ?: UIColor.blackColor; |
| _snackbarMessageViewBackgroundColor = |
| manager.snackbarMessageViewBackgroundColor ?: MDCRGBAColor(0x32, 0x32, 0x32, 1); |
| _messageTextColor = manager.messageTextColor ?: UIColor.whiteColor; |
| _buttonTitleColors = [NSMutableDictionary dictionary]; |
| _buttonTitleColors[@(UIControlStateNormal)] = |
| [manager buttonTitleColorForState:UIControlStateNormal] |
| ?: MDCRGBAColor(0xFF, 0xFF, 0xFF, (float)0.6); |
| _buttonTitleColors[@(UIControlStateHighlighted)] = |
| [manager buttonTitleColorForState:UIControlStateHighlighted] ?: UIColor.whiteColor; |
| _mdc_adjustsFontForContentSizeCategory = manager.mdc_adjustsFontForContentSizeCategory; |
| #pragma clang diagnostic push |
| #pragma clang diagnostic ignored "-Wdeprecated-declarations" |
| _adjustsFontForContentSizeCategoryWhenScaledFontIsUnavailable = |
| manager.adjustsFontForContentSizeCategoryWhenScaledFontIsUnavailable; |
| #pragma clang diagnostic pop |
| _messageFont = manager.messageFont; |
| _buttonFont = manager.buttonFont; |
| _message = message; |
| _dismissalHandler = [handler copy]; |
| _mdc_overrideBaseElevation = manager.mdc_overrideBaseElevation; |
| _traitCollectionDidChangeBlock = manager.traitCollectionDidChangeBlockForMessageView; |
| _mdc_elevationDidChangeBlock = manager.mdc_elevationDidChangeBlockForMessageView; |
| self.backgroundColor = _snackbarMessageViewBackgroundColor; |
| if (MDCSnackbarMessage.usesLegacySnackbar) { |
| self.layer.cornerRadius = kLegacyCornerRadius; |
| } else { |
| self.layer.cornerRadius = kCornerRadius; |
| } |
| _elevation = manager.messageElevation; |
| [(MDCShadowLayer *)self.layer setElevation:_elevation]; |
| |
| _anchoredToScreenBottom = YES; |
| |
| // Borders are drawn inside of the bounds of a layer. Because our border is translucent, we need |
| // to have a view with transparent background and border only (@c self). Inside will be a |
| // content view that has the dark grey color. |
| _containerView = [[UIControl alloc] init]; |
| [self addSubview:_containerView]; |
| |
| [_containerView setTranslatesAutoresizingMaskIntoConstraints:NO]; |
| _containerView.backgroundColor = [UIColor clearColor]; |
| _containerView.layer.cornerRadius = |
| MDCSnackbarMessage.usesLegacySnackbar ? kLegacyCornerRadius : kCornerRadius; |
| _containerView.layer.masksToBounds = YES; |
| |
| // Listen for taps on the background of the view. |
| [_containerView addTarget:self |
| action:@selector(handleBackgroundTapped:) |
| forControlEvents:UIControlEventTouchUpInside]; |
| |
| _buttonGutterTapTarget = [[UIControl alloc] init]; |
| _buttonGutterTapTarget.translatesAutoresizingMaskIntoConstraints = NO; |
| [_buttonGutterTapTarget addTarget:self |
| action:@selector(handleButtonGutterTapped:) |
| forControlEvents:UIControlEventTouchUpInside]; |
| [self addSubview:_buttonGutterTapTarget]; |
| |
| if (MDCSnackbarMessage.usesLegacySnackbar) { |
| UISwipeGestureRecognizer *swipeRightGesture = |
| [[UISwipeGestureRecognizer alloc] initWithTarget:self |
| action:@selector(handleBackgroundSwipedRight:)]; |
| [swipeRightGesture setDirection:UISwipeGestureRecognizerDirectionRight]; |
| [_containerView addGestureRecognizer:swipeRightGesture]; |
| |
| UISwipeGestureRecognizer *swipeLeftGesture = |
| [[UISwipeGestureRecognizer alloc] initWithTarget:self |
| action:@selector(handleBackgroundSwipedLeft:)]; |
| [swipeRightGesture setDirection:UISwipeGestureRecognizerDirectionLeft]; |
| [_containerView addGestureRecognizer:swipeLeftGesture]; |
| } |
| |
| _contentView = [[UIView alloc] init]; |
| [_contentView setTranslatesAutoresizingMaskIntoConstraints:NO]; |
| [_containerView addSubview:_contentView]; |
| _contentView.userInteractionEnabled = NO; |
| |
| _buttonView = [[UIView alloc] init]; |
| [_buttonView setTranslatesAutoresizingMaskIntoConstraints:NO]; |
| [_containerView addSubview:_buttonView]; |
| |
| _actionButtons = [[NSMutableArray alloc] init]; |
| |
| // Set up the title label. |
| _label = [[UILabel alloc] initWithFrame:CGRectZero]; |
| [_contentView addSubview:_label]; |
| // TODO(#2709): Migrate to a single source of truth for fonts |
| // If we are using the default (system) font loader, retrieve the |
| // font from the UIFont standardFont API. |
| [self updateMessageFont]; |
| |
| NSMutableAttributedString *messageString = [message.attributedText mutableCopy]; |
| |
| if (!_messageFont && !_mdc_adjustsFontForContentSizeCategory) { |
| // Find any of the bold attributes in the string, and set the proper font for those ranges. |
| // Use NSAttributedStringEnumerationLongestEffectiveRangeNotRequired as opposed to 0, |
| // otherwise it will only work if bold text is in the end. |
| [messageString |
| enumerateAttribute:MDCSnackbarMessageBoldAttributeName |
| inRange:NSMakeRange(0, messageString.length) |
| options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired |
| usingBlock:^(id value, NSRange range, __unused BOOL *stop) { |
| UIFont *font = [MDCTypography body1Font]; |
| if ([value boolValue]) { |
| font = [MDCTypography body2Font]; |
| } |
| [messageString setAttributes:@{NSFontAttributeName : font} range:range]; |
| }]; |
| } |
| |
| // Apply 'global' attributes along the whole string. |
| _label.backgroundColor = [UIColor clearColor]; |
| _label.textAlignment = NSTextAlignmentNatural; |
| _label.adjustsFontSizeToFitWidth = YES; |
| _label.attributedText = messageString; |
| _label.numberOfLines = 0; |
| [_label setTranslatesAutoresizingMaskIntoConstraints:NO]; |
| [_label setContentCompressionResistancePriority:UILayoutPriorityDefaultHigh |
| forAxis:UILayoutConstraintAxisHorizontal]; |
| [_label setContentHuggingPriority:UILayoutPriorityDefaultLow |
| forAxis:UILayoutConstraintAxisHorizontal]; |
| |
| NSString *accessibilityHintKey = |
| kMaterialSnackbarStringTable[kStr_MaterialSnackbarMessageViewTitleA11yHint]; |
| NSString *accessibilityHint = NSLocalizedStringFromTableInBundle( |
| accessibilityHintKey, kMaterialSnackbarStringsTableName, [[self class] bundle], |
| @"Dismissal accessibility hint for Snackbar"); |
| |
| // For UIAccessibility purposes, the label is the primary 'button' for dismissing the Snackbar, |
| // so we'll make sure the label is treated like a button. |
| _label.accessibilityTraits = UIAccessibilityTraitButton; |
| _label.accessibilityIdentifier = MDCSnackbarMessageTitleAutomationIdentifier; |
| _label.accessibilityHint = accessibilityHint; |
| |
| // If an accessibility label or hint was set on the message model object, use that instead of |
| // the text in the label or the default hint. |
| if ([message.accessibilityLabel length]) { |
| _label.accessibilityLabel = message.accessibilityLabel; |
| } |
| if (message.accessibilityHint.length) { |
| _label.accessibilityHint = message.accessibilityHint; |
| } |
| |
| _label.textColor = _messageTextColor; |
| |
| [self initializeMDCSnackbarMessageViewButtons:message withManager:manager]; |
| } |
| |
| return self; |
| } |
| |
| - (void)initializeMDCSnackbarMessageViewButtons:(MDCSnackbarMessage *)message |
| withManager:(MDCSnackbarManager *)manager { |
| // Add buttons to the view. We'll use this opportunity to determine how much space a button will |
| // need, to inform the layout direction. |
| NSMutableArray *actions = [NSMutableArray array]; |
| if (message.action) { |
| UIView *buttonView = [[UIView alloc] init]; |
| [buttonView setTranslatesAutoresizingMaskIntoConstraints:NO]; |
| [_buttonView addSubview:buttonView]; |
| |
| MDCButton *button = [[MDCSnackbarMessageViewButton alloc] init]; |
| [button setTitleColor:_buttonTitleColors[@(UIControlStateNormal)] |
| forState:UIControlStateNormal]; |
| [button setTitleColor:_buttonTitleColors[@(UIControlStateHighlighted)] |
| forState:UIControlStateHighlighted]; |
| |
| // TODO: Eventually remove this if statement, buttonTextColor is deprecated. |
| #pragma clang diagnostic push |
| #pragma clang diagnostic ignored "-Wdeprecated-declarations" |
| if (message.buttonTextColor) { |
| [button setTitleColor:message.buttonTextColor forState:UIControlStateNormal]; |
| } |
| #pragma clang diagnostic pop |
| |
| button.enableRippleBehavior = message.enableRippleBehavior; |
| [buttonView addSubview:button]; |
| [_actionButtons addObject:button]; |
| |
| // Set up the button's accessibility values. |
| button.accessibilityIdentifier = message.action.accessibilityIdentifier; |
| button.accessibilityHint = message.action.accessibilityHint; |
| |
| [button setTitle:message.action.title forState:UIControlStateNormal]; |
| [button setTitle:message.action.title forState:UIControlStateHighlighted]; |
| |
| [button addTarget:self |
| action:@selector(handleButtonTapped:) |
| forControlEvents:UIControlEventTouchUpInside]; |
| |
| button.uppercaseTitle = manager.uppercaseButtonTitle; |
| button.disabledAlpha = manager.disabledButtonAlpha; |
| if (manager.buttonInkColor) { |
| button.inkColor = manager.buttonInkColor; |
| } |
| |
| [actions addObject:buttonView]; |
| } |
| |
| self.buttons = actions; |
| [self updateButtonFont]; |
| } |
| |
| - (void)dismissWithAction:(MDCSnackbarMessageAction *)action userInitiated:(BOOL)userInitiated { |
| if (self.dismissalHandler) { |
| self.dismissalHandler(userInitiated, action); |
| |
| // Change focus only if the focus is on this view. |
| if (self.message.elementToFocusOnDismiss) { |
| BOOL hasVoiceOverFocus = |
| UIAccessibilityIsVoiceOverRunning() && UIViewHasFocusedAccessibilityElement(self); |
| if (hasVoiceOverFocus) { |
| UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, |
| self.message.elementToFocusOnDismiss); |
| } |
| } |
| |
| // In case our dismissal handler has a reference to us, release the block. |
| self.dismissalHandler = nil; |
| } |
| } |
| |
| + (Class)layerClass { |
| return [MDCShadowLayer class]; |
| } |
| |
| - (void)setElevation:(MDCShadowElevation)elevation { |
| BOOL elevationChanged = !MDCCGFloatEqual(_elevation, elevation); |
| _elevation = elevation; |
| [(MDCShadowLayer *)self.layer setElevation:_elevation]; |
| if (elevationChanged) { |
| [self mdc_elevationDidChange]; |
| } |
| } |
| |
| - (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection { |
| [super traitCollectionDidChange:previousTraitCollection]; |
| |
| if (self.traitCollectionDidChangeBlock) { |
| self.traitCollectionDidChangeBlock(self, previousTraitCollection); |
| } |
| } |
| |
| #pragma mark - Subclass overrides |
| |
| + (BOOL)requiresConstraintBasedLayout { |
| return YES; |
| } |
| |
| - (CGFloat)minimumWidth { |
| return UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad ? kMinimumViewWidth_iPad |
| : kMinimumViewWidth_iPhone; |
| } |
| |
| - (CGFloat)maximumWidth { |
| return UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad ? kMaximumViewWidth_iPad |
| : kMaximumViewWidth_iPhone; |
| } |
| |
| #pragma mark - Styling the view |
| |
| - (void)setSnackbarMessageViewBackgroundColor:(UIColor *)snackbarMessageViewBackgroundColor { |
| _snackbarMessageViewBackgroundColor = snackbarMessageViewBackgroundColor; |
| self.backgroundColor = snackbarMessageViewBackgroundColor; |
| } |
| |
| - (void)setSnackbarShadowColor:(UIColor *)snackbarMessageViewShadowColor { |
| _snackbarMessageViewShadowColor = snackbarMessageViewShadowColor; |
| self.layer.shadowColor = snackbarMessageViewShadowColor.CGColor; |
| } |
| |
| - (void)setMessageTextColor:(UIColor *)messageTextColor { |
| _messageTextColor = messageTextColor; |
| if (_messageTextColor) { |
| self.label.textColor = _messageTextColor; |
| } else { |
| self.label.textColor = UIColor.whiteColor; |
| } |
| } |
| |
| - (nullable UIColor *)buttonTitleColorForState:(UIControlState)state { |
| return _buttonTitleColors[@(state)]; |
| } |
| |
| - (void)setButtonTitleColor:(nullable UIColor *)buttonTitleColor forState:(UIControlState)state { |
| _buttonTitleColors[@(state)] = buttonTitleColor; |
| for (MDCButton *button in _actionButtons) { |
| if (_buttonTitleColors[@(state)]) { |
| [button setTitleColor:buttonTitleColor forState:state]; |
| } else { |
| // Set to default |
| UIColor *defaultButtonTitleColor; |
| switch (state) { |
| case UIControlStateHighlighted: |
| defaultButtonTitleColor = UIColor.whiteColor; |
| break; |
| case UIControlStateNormal: |
| default: |
| defaultButtonTitleColor = MDCRGBAColor(0xFF, 0xFF, 0xFF, (float)0.6); |
| break; |
| } |
| [button setTitleColor:defaultButtonTitleColor forState:state]; |
| } |
| } |
| } |
| |
| - (void)addColorToMessageLabel:(UIColor *)color { |
| NSMutableAttributedString *messageString = [_label.attributedText mutableCopy]; |
| [messageString addAttributes:@{ |
| NSForegroundColorAttributeName : color, |
| } |
| range:NSMakeRange(0, messageString.length)]; |
| _label.attributedText = messageString; |
| } |
| |
| - (UIFont *)messageFont { |
| return _messageFont; |
| } |
| |
| - (void)setMessageFont:(UIFont *)font { |
| _messageFont = font; |
| |
| [self updateMessageFont]; |
| } |
| |
| - (void)updateMessageFont { |
| // If we have a custom font apply it to the label. |
| // If not, fall back to the Material specified font. |
| if (_messageFont) { |
| // If we are automatically adjusting for Dynamic Type resize the font based on the text style |
| if (_mdc_adjustsFontForContentSizeCategory) { |
| if (_messageFont.mdc_scalingCurve) { |
| _label.font = [_messageFont mdc_scaledFontForTraitEnvironment:self]; |
| } else if (_adjustsFontForContentSizeCategoryWhenScaledFontIsUnavailable) { |
| _label.font = |
| [_messageFont mdc_fontSizedForMaterialTextStyle:kMessageTextStyle |
| scaledForDynamicType:_mdc_adjustsFontForContentSizeCategory]; |
| } |
| } else { |
| _label.font = _messageFont; |
| } |
| } else { |
| // TODO(#2709): Migrate to a single source of truth for fonts |
| // There is no custom font, so use the default font. |
| if (_mdc_adjustsFontForContentSizeCategory) { |
| // If we are using the default (system) font loader, retrieve the |
| // font from the UIFont preferredFont API. |
| if ([MDCTypography.fontLoader isKindOfClass:[MDCSystemFontLoader class]]) { |
| _label.font = [UIFont mdc_preferredFontForMaterialTextStyle:kMessageTextStyle]; |
| } else { |
| // There is a custom font loader, retrieve the font and scale it. |
| UIFont *customTypographyFont = [MDCTypography body1Font]; |
| _label.font = [customTypographyFont |
| mdc_fontSizedForMaterialTextStyle:kMessageTextStyle |
| scaledForDynamicType:_mdc_adjustsFontForContentSizeCategory]; |
| } |
| } else { |
| // If we are using the default (system) font loader, retrieve the |
| // font from the UIFont standardFont API. |
| if ([MDCTypography.fontLoader isKindOfClass:[MDCSystemFontLoader class]]) { |
| _label.font = [UIFont mdc_standardFontForMaterialTextStyle:kMessageTextStyle]; |
| } else { |
| // There is a custom font loader, retrieve the font from it. |
| _label.font = [MDCTypography body1Font]; |
| } |
| } |
| } |
| [self setNeedsLayout]; |
| } |
| |
| - (UIFont *)buttonFont { |
| return _buttonFont; |
| } |
| |
| - (void)setButtonFont:(UIFont *)font { |
| _buttonFont = font; |
| |
| [self updateButtonFont]; |
| } |
| |
| - (void)updateButtonFont { |
| UIFont *finalButtonFont; |
| |
| // If we have a custom font apply it to the label. |
| // If not, fall back to the Material specified font. |
| if (_buttonFont) { |
| finalButtonFont = _buttonFont; |
| // If we are automatically adjusting for Dynamic Type resize the font based on the text style |
| if (_mdc_adjustsFontForContentSizeCategory) { |
| if (_buttonFont.mdc_scalingCurve) { |
| finalButtonFont = [_buttonFont mdc_scaledFontForTraitEnvironment:self]; |
| } else if (_adjustsFontForContentSizeCategoryWhenScaledFontIsUnavailable) { |
| finalButtonFont = |
| [_buttonFont mdc_fontSizedForMaterialTextStyle:kButtonTextStyle |
| scaledForDynamicType:_mdc_adjustsFontForContentSizeCategory]; |
| } |
| } |
| } else { |
| // TODO(#2709): Migrate to a single source of truth for fonts |
| // There is no custom font, so use the default font. |
| if (_mdc_adjustsFontForContentSizeCategory) { |
| // If we are using the default (system) font loader, retrieve the |
| // font from the UIFont preferredFont API. |
| if ([MDCTypography.fontLoader isKindOfClass:[MDCSystemFontLoader class]]) { |
| finalButtonFont = [UIFont mdc_preferredFontForMaterialTextStyle:kButtonTextStyle]; |
| } else { |
| // There is a custom font loader, retrieve the font and scale it. |
| UIFont *customTypographyFont = [MDCTypography buttonFont]; |
| finalButtonFont = [customTypographyFont |
| mdc_fontSizedForMaterialTextStyle:kButtonTextStyle |
| scaledForDynamicType:_mdc_adjustsFontForContentSizeCategory]; |
| } |
| } else { |
| // If we are using the default (system) font loader, retrieve the |
| // font from the UIFont standardFont API. |
| if ([MDCTypography.fontLoader isKindOfClass:[MDCSystemFontLoader class]]) { |
| finalButtonFont = [UIFont mdc_standardFontForMaterialTextStyle:kButtonTextStyle]; |
| } else { |
| // There is a custom font loader, retrieve the font from it. |
| finalButtonFont = [MDCTypography buttonFont]; |
| } |
| } |
| } |
| |
| for (MDCButton *button in _actionButtons) { |
| [button setTitleFont:finalButtonFont forState:UIControlStateNormal]; |
| [button setTitleFont:finalButtonFont forState:UIControlStateHighlighted]; |
| } |
| |
| [self setNeedsLayout]; |
| } |
| |
| - (BOOL)shouldWaitForDismissalDuringVoiceover { |
| return self.message.action != nil; |
| } |
| |
| #pragma mark - Constraints and layout |
| |
| - (void)setAnchoredToScreenBottom:(BOOL)anchoredToScreenBottom { |
| _anchoredToScreenBottom = anchoredToScreenBottom; |
| [self invalidateIntrinsicContentSize]; |
| |
| if (self.viewConstraints) { |
| [self removeConstraints:self.viewConstraints]; |
| self.viewConstraints = nil; |
| [self updateConstraints]; |
| } |
| } |
| |
| - (void)updateConstraints { |
| [super updateConstraints]; |
| |
| if (self.viewConstraints) { |
| return; |
| } |
| |
| NSMutableArray *constraints = [NSMutableArray array]; |
| |
| [constraints addObjectsFromArray:[self containerViewConstraints]]; |
| [constraints addObjectsFromArray:[self contentViewConstraints]]; |
| [constraints addObjectsFromArray:[self horizontalButtonLayoutConstraints]]; |
| |
| [self addConstraints:constraints]; |
| self.viewConstraints = constraints; |
| } |
| |
| /** |
| Provides constraints to pin the container view to the size of the Snackbar, inset by |
| @c kBorderWidth. Also positions the content view and button view inside of the container view. |
| */ |
| - (NSArray *)containerViewConstraints { |
| NSDictionary *metrics = @{ |
| @"kBorderMargin" : @(kBorderWidth), |
| @"kBottomMargin" : @(self.safeContentMargin.bottom), |
| @"kLeftMargin" : @(self.safeContentMargin.left), |
| @"kRightMargin" : @(self.safeContentMargin.right), |
| @"kTitleImagePadding" : @(kTitleImagePadding), |
| @"kTopMargin" : @(self.safeContentMargin.top), |
| @"kTitleButtonPadding" : @(kTitleButtonPadding), |
| @"kContentSafeBottomInset" : @(kBorderWidth + self.contentSafeBottomInset), |
| @"kMinVerticalButtonPadding" : @(kMinVerticalButtonPadding), |
| }; |
| NSDictionary *views = @{ |
| @"container" : self.containerView, |
| @"content" : self.contentView, |
| @"buttons" : self.buttonView, |
| @"buttonGutter" : self.buttonGutterTapTarget, |
| }; |
| |
| BOOL hasButtons = (self.buttons.count > 0); |
| |
| NSString *formatString = nil; // Scratch variable. |
| NSMutableArray *constraints = [NSMutableArray array]; |
| |
| // Pin the left and right edges of the container view to the Snackbar. |
| formatString = @"H:|-(==kBorderMargin)-[container]-(==kBorderMargin)-|"; |
| [constraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:formatString |
| options:0 |
| metrics:metrics |
| views:views]]; |
| |
| // Pin the top and bottom edges of the container view to the Snackbar. |
| formatString = @"V:|-(==kBorderMargin)-[container]-(==kContentSafeBottomInset)-|"; |
| [constraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:formatString |
| options:0 |
| metrics:metrics |
| views:views]]; |
| |
| // Pin the content to the top of the container view. |
| formatString = @"V:|-(==kTopMargin)-[content]"; |
| [constraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:formatString |
| options:0 |
| metrics:metrics |
| views:views]]; |
| |
| // Pin the content view to the left side of the container. |
| formatString = @"H:|-(==kLeftMargin)-[content]"; |
| [constraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:formatString |
| options:0 |
| metrics:metrics |
| views:views]]; |
| |
| // If there are no buttons, or the buttons are below the main content, then the content view |
| // should be pinned to the right side of the container. If there are horizontal buttons, the |
| // leftmost button will take care of positioning the trailing edge of the label view. |
| |
| // If there are no buttons, then go ahead and pin the content view to the bottom of the |
| // container view. |
| if (!hasButtons) { |
| formatString = @"V:[content]-(==kBottomMargin)-|"; |
| [constraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:formatString |
| options:0 |
| metrics:metrics |
| views:views]]; |
| } |
| |
| if (!hasButtons) { |
| // There is nothing to the right of the content, so go ahead and pin it to the trailing edge of |
| // the container view. |
| formatString = @"H:[content]-(==kRightMargin)-|"; |
| [constraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:formatString |
| options:0 |
| metrics:metrics |
| views:views]]; |
| } else { // This is a horizontal layout, and there are buttons present. |
| if (MDCSnackbarMessage.usesLegacySnackbar) { |
| // Align the content and buttons horizontally. |
| formatString = |
| @"H:[content]-(==kTitleButtonPadding)-[buttons][buttonGutter(==kRightMargin)]|"; |
| [constraints addObjectsFromArray:[NSLayoutConstraint |
| constraintsWithVisualFormat:formatString |
| options:NSLayoutFormatAlignAllCenterY |
| metrics:metrics |
| views:views]]; |
| |
| // The buttons should take up the entire height of the container view. |
| formatString = @"V:|[buttons]|"; |
| [constraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:formatString |
| options:0 |
| metrics:metrics |
| views:views]]; |
| |
| // The button gutter tap target should take up the entire height of the container view. |
| formatString = @"V:|[buttonGutter]|"; |
| [constraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:formatString |
| options:0 |
| metrics:metrics |
| views:views]]; |
| } else { |
| // Align the content and buttons horizontally. |
| formatString = @"H:[content]-(==kTitleButtonPadding)-[buttons]-(==kRightMargin)-|"; |
| [constraints addObjectsFromArray:[NSLayoutConstraint |
| constraintsWithVisualFormat:formatString |
| options:NSLayoutFormatAlignAllCenterY |
| metrics:metrics |
| views:views]]; |
| |
| formatString = @"V:|-(>=kMinVerticalButtonPadding)-[buttons]-(>=kMinVerticalButtonPadding)-|"; |
| [constraints addObjectsFromArray:[NSLayoutConstraint |
| constraintsWithVisualFormat:formatString |
| options:NSLayoutFormatAlignAllCenterY |
| metrics:metrics |
| views:views]]; |
| } |
| |
| // Pin the content to the bottom of the container view, since there's nothing below. |
| formatString = @"V:[content]-(==kBottomMargin)-|"; |
| [constraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:formatString |
| options:0 |
| metrics:metrics |
| views:views]]; |
| } |
| |
| return constraints; |
| } |
| |
| /** |
| Provides constraints for the image view and label within the content view. |
| */ |
| - (NSArray *)contentViewConstraints { |
| NSDictionary *metrics = @{ |
| @"kBottomMargin" : @(self.safeContentMargin.bottom), |
| @"kLeftMargin" : @(self.safeContentMargin.left), |
| @"kRightMargin" : @(self.safeContentMargin.right), |
| @"kTitleImagePadding" : @(kTitleImagePadding), |
| @"kTopMargin" : @(self.safeContentMargin.top), |
| }; |
| |
| NSMutableDictionary *views = [NSMutableDictionary dictionary]; |
| views[@"label"] = self.label; |
| if (self.imageView) { |
| views[@"imageView"] = self.imageView; |
| } |
| |
| NSString *formatString = nil; // Scratch variable. |
| NSMutableArray *constraints = [NSMutableArray array]; |
| |
| // The label should take up the entire height of the content view. |
| formatString = @"V:|[label]|"; |
| [constraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:formatString |
| options:0 |
| metrics:metrics |
| views:views]]; |
| |
| // Pin the label to the trailing edge of the content view. |
| formatString = @"H:[label]|"; |
| [constraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:formatString |
| options:0 |
| metrics:metrics |
| views:views]]; |
| |
| if (self.imageView) { |
| // Constrain the image view to be no taller than @c kMaximumImageSize. |
| formatString = @"V:[imageView(<=kMaximumImageSize)]"; |
| [constraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:formatString |
| options:0 |
| metrics:metrics |
| views:views]]; |
| |
| // Vertically center the image view within the content view. |
| [constraints addObject:[NSLayoutConstraint constraintWithItem:self.imageView |
| attribute:NSLayoutAttributeCenterY |
| relatedBy:NSLayoutRelationEqual |
| toItem:self.contentView |
| attribute:NSLayoutAttributeCenterY |
| multiplier:1.0 |
| constant:0]]; |
| |
| // Constrain the image view to be no wider than @c kMaximumImageSize, and pin it to the leading |
| // edge of the content view. Pin the label to the trailing edge of the image. |
| formatString = @"H:|[imageView(<=kMaximumImageSize)]-(==kTitleImagePadding)-[label]"; |
| [constraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:formatString |
| options:0 |
| metrics:metrics |
| views:views]]; |
| } else { |
| formatString = @"H:|[label]"; |
| [constraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:formatString |
| options:0 |
| metrics:metrics |
| views:views]]; |
| } |
| |
| return constraints; |
| } |
| |
| /** |
| Provides constraints positioning the buttons horizontally within the button container. |
| */ |
| - (NSArray *)horizontalButtonLayoutConstraints { |
| NSMutableArray *constraints = [NSMutableArray array]; |
| |
| NSDictionary *metrics = @{ |
| @"kLeftMargin" : @(self.safeContentMargin.left), |
| @"kRightMargin" : @(self.safeContentMargin.right), |
| @"kTopMargin" : @(self.safeContentMargin.top), |
| @"kBottomMargin" : @(self.safeContentMargin.bottom), |
| @"kTitleImagePadding" : @(kTitleImagePadding), |
| @"kBorderMargin" : @(kBorderWidth), |
| @"kTitleButtonPadding" : @(kTitleButtonPadding), |
| @"kButtonPadding" : |
| @(MDCSnackbarMessage.usesLegacySnackbar ? kLegacyButtonPadding : kButtonPadding), |
| }; |
| |
| __block UIView *previousButton = nil; |
| [self.buttons enumerateObjectsUsingBlock:^(UIView *button, NSUInteger idx, __unused BOOL *stop) { |
| // Convenience dictionary of views. |
| NSMutableDictionary *views = [NSMutableDictionary dictionary]; |
| views[@"buttonContainer"] = button; |
| views[@"button"] = [button viewWithTag:kButtonTagStart + idx]; |
| if (previousButton) { |
| views[@"previousButton"] = previousButton; |
| } |
| |
| // In a horizontal layout, the button takes on the height of the Snackbar. |
| [constraints |
| addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[buttonContainer]|" |
| options:0 |
| metrics:metrics |
| views:views]]; |
| |
| // Pin the button to the height of its container. |
| [constraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[button]|" |
| options:0 |
| metrics:metrics |
| views:views]]; |
| |
| // Pin the button to the width of its container. |
| [constraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[button]|" |
| options:0 |
| metrics:metrics |
| views:views]]; |
| |
| if (idx == 0) { |
| // The first button should be pinned to the leading edge of the button view. |
| [constraints addObjectsFromArray:[NSLayoutConstraint |
| constraintsWithVisualFormat:@"H:|[buttonContainer]" |
| options:0 |
| metrics:metrics |
| views:views]]; |
| } |
| |
| if (idx == ([self.buttons count] - 1)) { |
| // The last button should be pinned to the trailing edge of the button view. |
| [constraints addObjectsFromArray:[NSLayoutConstraint |
| constraintsWithVisualFormat:@"H:[buttonContainer]|" |
| options:0 |
| metrics:metrics |
| views:views]]; |
| } |
| |
| if (previousButton) { |
| // If there was a button before this one, pin this one to it. |
| NSString *formatString = @"H:[previousButton]-(==kButtonPadding)-[buttonContainer]"; |
| [constraints addObjectsFromArray:[NSLayoutConstraint constraintsWithVisualFormat:formatString |
| options:0 |
| metrics:metrics |
| views:views]]; |
| } |
| }]; |
| |
| return constraints; |
| } |
| |
| - (void)layoutSubviews { |
| [super layoutSubviews]; |
| |
| // As our layout changes, make sure that the shadow path is kept up-to-date. |
| UIBezierPath *path = [UIBezierPath |
| bezierPathWithRoundedRect:self.bounds |
| cornerRadius:MDCSnackbarMessage.usesLegacySnackbar ? kLegacyCornerRadius |
| : kCornerRadius]; |
| self.layer.shadowPath = path.CGPath; |
| self.layer.shadowColor = self.snackbarMessageViewShadowColor.CGColor; |
| } |
| |
| #pragma mark - Sizing |
| |
| - (CGSize)intrinsicContentSize { |
| CGFloat height = 0; |
| |
| // Figure out which of the elements is tallest, and use that for calculating our preferred height. |
| // Images are forced to be no bigger than @c kMaximumImageSize. |
| height = MAX(height, self.label.intrinsicContentSize.height); |
| |
| // Make sure that content margins are included in our calculation. |
| height += self.safeContentMargin.top + self.safeContentMargin.bottom; |
| |
| // Make sure that the height of the image and text is larger than the minimum height; |
| height = MAX(kMinimumHeight, height) + self.contentSafeBottomInset; |
| |
| return CGSizeMake(UIViewNoIntrinsicMetric, height); |
| } |
| |
| - (CGFloat)contentSafeBottomInset { |
| // If a bottom offset has been set to raise the HUD/Snackbar, e.g. above a tab bar, we should |
| // ignore any safeAreaInsets, since it is no longer 'anchored' to the bottom of the screen. This |
| // is set by the MDCSnackbarOverlayView whenever the bottomOffset is non-zero. |
| if (!self.anchoredToScreenBottom || !MDCSnackbarMessage.usesLegacySnackbar) { |
| return 0; |
| } |
| if (@available(iOS 11.0, *)) { |
| return self.window.safeAreaInsets.bottom; |
| } |
| return 0; |
| } |
| |
| - (UIEdgeInsets)safeContentMargin { |
| UIEdgeInsets contentMargin = |
| MDCSnackbarMessage.usesLegacySnackbar ? kLegacyContentMargin : kContentMargin; |
| |
| UIEdgeInsets safeAreaInsets = UIEdgeInsetsZero; |
| if (@available(iOS 11.0, *)) { |
| safeAreaInsets = self.window.safeAreaInsets; |
| } |
| |
| // We only take the left and right safeAreaInsets in to account because the bottom is |
| // handled by contentSafeBottomInset and we will never overlap the top inset. |
| contentMargin.left = MAX(contentMargin.left, safeAreaInsets.left); |
| contentMargin.right = MAX(contentMargin.right, safeAreaInsets.right); |
| |
| return contentMargin; |
| } |
| |
| #pragma mark - Event Handlers |
| |
| - (void)handleBackgroundSwipedRight:(__unused UIButton *)sender { |
| CABasicAnimation *translationAnimation = |
| [CABasicAnimation animationWithKeyPath:@"transform.translation.x"]; |
| translationAnimation.toValue = [NSNumber numberWithDouble:-self.frame.size.width]; |
| translationAnimation.duration = MDCSnackbarLegacyTransitionDuration; |
| translationAnimation.timingFunction = |
| [CAMediaTimingFunction mdc_functionWithType:MDCAnimationTimingFunctionTranslateOffScreen]; |
| translationAnimation.delegate = self; |
| translationAnimation.fillMode = kCAFillModeForwards; |
| translationAnimation.removedOnCompletion = NO; |
| [self.layer addAnimation:translationAnimation forKey:@"transform.translation.x"]; |
| } |
| |
| - (void)handleBackgroundSwipedLeft:(__unused UIButton *)sender { |
| CABasicAnimation *translationAnimation = |
| [CABasicAnimation animationWithKeyPath:@"transform.translation.x"]; |
| translationAnimation.toValue = [NSNumber numberWithDouble:self.frame.size.width]; |
| translationAnimation.duration = MDCSnackbarLegacyTransitionDuration; |
| translationAnimation.timingFunction = |
| [CAMediaTimingFunction mdc_functionWithType:MDCAnimationTimingFunctionTranslateOffScreen]; |
| translationAnimation.delegate = self; |
| translationAnimation.fillMode = kCAFillModeForwards; |
| translationAnimation.removedOnCompletion = NO; |
| [self.layer addAnimation:translationAnimation forKey:@"transform.translation.x"]; |
| } |
| |
| - (void)handleBackgroundTapped:(__unused UIButton *)sender { |
| [self dismissWithAction:nil userInitiated:YES]; |
| } |
| |
| - (void)handleButtonGutterTapped:(__unused UIControl *)sender { |
| [self handleButtonTapped:nil]; |
| } |
| |
| - (void)handleButtonTapped:(__unused UIButton *)sender { |
| [self dismissWithAction:self.message.action userInitiated:YES]; |
| } |
| |
| - (void)animationDidStop:(__unused CAAnimation *)theAnimation finished:(BOOL)flag { |
| if (flag) { |
| [self dismissWithAction:nil userInitiated:YES]; |
| } |
| } |
| |
| #pragma mark - Accessibility |
| |
| // Override regular accessibility element ordering and ensure label |
| // is read first before any buttons, if they exist. |
| - (NSInteger)accessibilityElementCount { |
| return 1 + [self.buttons count]; |
| } |
| |
| - (id)accessibilityElementAtIndex:(NSInteger)index { |
| if (index == 0) { |
| return _label; |
| } |
| return [self.buttons objectAtIndex:(index - 1)]; |
| } |
| |
| - (NSInteger)indexOfAccessibilityElement:(id)element { |
| if (element == _label) { |
| return 0; |
| } |
| |
| NSInteger buttonIndex = [self.buttons indexOfObject:element]; |
| if (buttonIndex == NSNotFound) { |
| return NSNotFound; |
| } |
| return buttonIndex + 1; |
| } |
| |
| #pragma mark - Animation |
| |
| - (void)animateContentOpacityFrom:(CGFloat)fromOpacity |
| to:(CGFloat)toOpacity |
| duration:(NSTimeInterval)duration |
| timingFunction:(CAMediaTimingFunction *)timingFunction { |
| [CATransaction begin]; |
| CABasicAnimation *opacityAnimation = [CABasicAnimation animationWithKeyPath:@"opacity"]; |
| opacityAnimation.duration = duration; |
| opacityAnimation.fromValue = @(fromOpacity); |
| opacityAnimation.toValue = @(toOpacity); |
| opacityAnimation.timingFunction = timingFunction; |
| |
| // The text and the button do not share a common view that can be animated independently of the |
| // background color, so just animate them both independently here. If this becomes more |
| // complicated, refactor to add a containing view for both and animate that. |
| [self.contentView.layer addAnimation:opacityAnimation forKey:@"opacity"]; |
| [self.buttonView.layer addAnimation:opacityAnimation forKey:@"opacity"]; |
| [CATransaction commit]; |
| } |
| |
| - (CABasicAnimation *)animateSnackbarOpacityFrom:(CGFloat)fromOpacity to:(CGFloat)toOpacity { |
| CABasicAnimation *opacityAnimation = [CABasicAnimation animationWithKeyPath:@"opacity"]; |
| opacityAnimation.fromValue = @(fromOpacity); |
| opacityAnimation.toValue = @(toOpacity); |
| return opacityAnimation; |
| } |
| |
| - (CABasicAnimation *)animateSnackbarScaleFrom:(CGFloat)fromScale toScale:(CGFloat)toScale { |
| CABasicAnimation *scaleAnimation = [CABasicAnimation animationWithKeyPath:@"transform.scale"]; |
| scaleAnimation.fromValue = [NSNumber numberWithDouble:fromScale]; |
| scaleAnimation.toValue = [NSNumber numberWithDouble:toScale]; |
| return scaleAnimation; |
| } |
| |
| #pragma mark - Resource bundle |
| |
| + (NSBundle *)bundle { |
| static NSBundle *bundle = nil; |
| static dispatch_once_t onceToken; |
| dispatch_once(&onceToken, ^{ |
| bundle = [NSBundle bundleWithPath:[self bundlePathWithName:kMaterialSnackbarBundle]]; |
| }); |
| |
| 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:[MDCSnackbarMessageView class]]; |
| NSString *resourcePath = [(nil == bundle ? [NSBundle mainBundle] : bundle) resourcePath]; |
| return [resourcePath stringByAppendingPathComponent:bundleName]; |
| } |
| |
| #pragma mark - Dynamic Type Support |
| |
| - (BOOL)mdc_adjustsFontForContentSizeCategory { |
| return _mdc_adjustsFontForContentSizeCategory; |
| } |
| |
| - (void)mdc_setAdjustsFontForContentSizeCategory:(BOOL)adjusts { |
| _mdc_adjustsFontForContentSizeCategory = adjusts; |
| |
| if (_mdc_adjustsFontForContentSizeCategory) { |
| [[NSNotificationCenter defaultCenter] addObserver:self |
| selector:@selector(contentSizeCategoryDidChange:) |
| name:UIContentSizeCategoryDidChangeNotification |
| object:nil]; |
| } else { |
| [[NSNotificationCenter defaultCenter] removeObserver:self |
| name:UIContentSizeCategoryDidChangeNotification |
| object:nil]; |
| } |
| |
| [self updateMessageFont]; |
| [self updateButtonFont]; |
| } |
| |
| // Handles UIContentSizeCategoryDidChangeNotifications |
| - (void)contentSizeCategoryDidChange:(__unused NSNotification *)notification { |
| [self updateMessageFont]; |
| [self updateButtonFont]; |
| } |
| |
| #pragma mark - Elevation |
| |
| - (CGFloat)mdc_currentElevation { |
| return self.elevation; |
| } |
| |
| @end |