| // 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 "MDCSnackbarMessage.h" |
| #import "private/MDCSnackbarMessageInternal.h" |
| #import "MDCAvailability.h" |
| #import "MDCSnackbarMessageView.h" |
| |
| static const NSTimeInterval kDefaultDuration = 4; |
| |
| const NSTimeInterval MDCSnackbarMessageDurationMax = 10; |
| NSString *const MDCSnackbarMessageBoldAttributeName = @"MDCSnackbarMessageBoldAttributeName"; |
| |
| @interface MDCSnackbarMessage () |
| |
| @property(nonatomic, readonly, strong) dispatch_queue_t targetQueue; |
| |
| @property(nonatomic, assign) BOOL hasSetTextAlignment; |
| |
| @end |
| |
| @implementation MDCSnackbarMessage |
| static BOOL _usesLegacySnackbar = NO; |
| @synthesize accessibilityIdentifier; |
| @dynamic text; |
| |
| + (instancetype)messageWithText:(NSString *)text { |
| MDCSnackbarMessage *message = [[[self class] alloc] init]; |
| message.text = text; |
| return message; |
| } |
| |
| + (instancetype)messageWithAttributedText:(NSAttributedString *)attributedText { |
| MDCSnackbarMessage *message = [[[self class] alloc] init]; |
| message.attributedText = attributedText; |
| return message; |
| } |
| |
| - (instancetype)init { |
| self = [super init]; |
| if (self) { |
| _duration = kDefaultDuration; |
| _automaticallyDismisses = YES; |
| } |
| return self; |
| } |
| |
| - (Class)viewClass { |
| return [MDCSnackbarMessageView class]; |
| } |
| |
| - (instancetype)copyWithZone:(__unused NSZone *)zone { |
| MDCSnackbarMessage *copy = [[[self class] alloc] init]; |
| copy.attributedText = self.attributedText; |
| copy.duration = self.duration; |
| copy.category = self.category; |
| copy.accessibilityLabel = self.accessibilityLabel; |
| copy.accessibilityHint = self.accessibilityHint; |
| #pragma clang diagnostic push |
| #pragma clang diagnostic ignored "-Wdeprecated-declarations" |
| copy.buttonTextColor = self.buttonTextColor; |
| #pragma clang diagnostic pop |
| copy.enableRippleBehavior = self.enableRippleBehavior; |
| copy.focusOnShow = self.focusOnShow; |
| copy.elementToFocusOnDismiss = self.elementToFocusOnDismiss; |
| copy.automaticallyDismisses = self.automaticallyDismisses; |
| copy.presentationHostViewOverride = self.presentationHostViewOverride; |
| copy.shouldDismissOnOverlayTap = self.shouldDismissOnOverlayTap; |
| copy.usesLegacyDismissalBehavior = self.usesLegacyDismissalBehavior; |
| |
| // Unfortunately there's not really a concept of 'copying' a block (in the same way you would copy |
| // a string, for example). A block's pointer is immutable once it is created and copied to the |
| // heap, so we're pretty safe just using the same block. |
| copy.completionHandler = self.completionHandler; |
| copy.completionHandlerWithError = self.completionHandlerWithError; |
| copy.action = self.action; |
| copy.snackbarMessageWillPresentBlock = self.snackbarMessageWillPresentBlock; |
| copy.error = self.error; |
| |
| return copy; |
| } |
| |
| - (dispatch_queue_t)targetQueue { |
| return dispatch_get_main_queue(); |
| } |
| |
| - (NSString *)description { |
| NSMutableString *description = [[NSMutableString alloc] init]; |
| [description appendFormat:@"<%@: %p> {\n", [self class], self]; |
| [description appendFormat:@" text: \"%@\",\n", self.text]; |
| if (self.action) { |
| [description appendFormat:@" action: \"%@\",\n", self.action.title]; |
| } |
| [description appendFormat:@" viewClass: \"%@\",\n", self.viewClass]; |
| [description appendString:@"}"]; |
| return [description copy]; |
| } |
| |
| #pragma mark - Text |
| |
| - (void)setText:(NSString *)text { |
| NSDictionary *attributes = @{}; |
| |
| // TODO(b/298435271): Remove the #if check below once all users are building with Xcode 15. |
| #if MDC_AVAILABLE_SDK_IOS(17_0) |
| if (@available(iOS 17.0, *)) { |
| // Default to low-priority announcements. |
| attributes = @{UIAccessibilitySpeechAttributeAnnouncementPriority : UIAccessibilityPriorityLow}; |
| } |
| #endif |
| self.attributedText = [[NSAttributedString alloc] initWithString:[text copy] |
| attributes:attributes]; |
| } |
| |
| - (NSString *)text { |
| return [self.attributedText string]; |
| } |
| |
| #pragma mark - Action |
| |
| - (void)setAction:(MDCSnackbarMessageAction *)action { |
| if (action) { |
| NSAssert(action.title.length > 0, @"Snackbar actions must have a non-empty title."); |
| } |
| if (action.title.length == 0) { |
| _action = nil; |
| } else { |
| _action = action; |
| } |
| } |
| |
| #pragma mark - Duration |
| |
| - (void)setDuration:(NSTimeInterval)duration { |
| NSAssert(duration <= MDCSnackbarMessageDurationMax, |
| @"Duration %g is longer than the maximum allowed: %g.", duration, |
| MDCSnackbarMessageDurationMax); |
| _duration = MIN(MDCSnackbarMessageDurationMax, duration); |
| } |
| |
| #pragma mark - A11y |
| |
| - (void)setAccessibilityLabel:(NSString *)accessibilityLabel { |
| if (accessibilityLabel == nil) { |
| self.attributedAccessibilityLabel = nil; |
| } else { |
| NSDictionary *attributes = @{}; |
| |
| // TODO(b/298435271): Remove the #if check below once all users are building with Xcode 15. |
| #if MDC_AVAILABLE_SDK_IOS(17_0) |
| if (@available(iOS 17.0, *)) { |
| // Default to low-priority announcements. |
| attributes = |
| @{UIAccessibilitySpeechAttributeAnnouncementPriority : UIAccessibilityPriorityLow}; |
| } |
| #endif |
| self.attributedAccessibilityLabel = |
| [[NSAttributedString alloc] initWithString:[accessibilityLabel copy] attributes:attributes]; |
| } |
| } |
| |
| - (NSString *)accessibilityLabel { |
| return [self.attributedAccessibilityLabel string]; |
| } |
| |
| - (NSAttributedString *)voiceNotificationText { |
| if (self.attributedAccessibilityLabel) { |
| return self.attributedAccessibilityLabel; |
| } else { |
| return self.attributedText; |
| } |
| } |
| |
| #pragma mark - Internal |
| |
| - (void)executeCompletionHandlerWithUserInteraction:(BOOL)userInteraction |
| completion:(void (^)(void))completion { |
| if (self.completionHandlerWithError || self.completionHandler) { |
| dispatch_async(self.targetQueue, ^{ |
| NSError *error = self.error; |
| self.error = nil; // Break retain cycle. |
| if (self.completionHandlerWithError) { |
| self.completionHandlerWithError(userInteraction, error); |
| } |
| if (self.completionHandler) { |
| self.completionHandler(userInteraction); |
| } |
| if (completion) { |
| completion(); |
| } |
| }); |
| } else { |
| self.error = nil; // Break retain cycle. |
| if (completion) { |
| completion(); |
| } |
| } |
| } |
| |
| - (void)executeActionHandler:(MDCSnackbarMessageAction *)action |
| completion:(void (^)(void))completion { |
| if (!action || !action.handler) { |
| if (completion) { |
| completion(); |
| } |
| return; |
| } |
| |
| // Fire off the action handler. |
| dispatch_async(self.targetQueue, ^{ |
| action.handler(); |
| if (completion) { |
| completion(); |
| } |
| }); |
| } |
| |
| + (void)setUsesLegacySnackbar:(BOOL)usesLegacySnackbar { |
| _usesLegacySnackbar = usesLegacySnackbar; |
| } |
| |
| + (BOOL)usesLegacySnackbar { |
| return _usesLegacySnackbar; |
| } |
| |
| @end |
| |
| @implementation MDCSnackbarMessageAction |
| |
| @synthesize accessibilityIdentifier; |
| |
| - (instancetype)copyWithZone:(__unused NSZone *)zone { |
| MDCSnackbarMessageAction *copy = [[[self class] alloc] init]; |
| copy.title = self.title; |
| copy.handler = self.handler; // See the comment on @c completionHandler above. |
| copy.accessibilityIdentifier = self.accessibilityIdentifier; |
| copy.accessibilityHint = self.accessibilityHint; |
| |
| return copy; |
| } |
| |
| - (void)setTitle:(NSString *)title { |
| if (title.length > 0) { |
| _title = title; |
| } |
| } |
| |
| @end |