mop-flutter-sdk/ios/Classes/Utils/UIView+MOPFATToast.m

562 lines
22 KiB
Dart
Raw Normal View History

2023-01-03 22:13:59 +08:00
#import "UIView+MOPFATToast.h"
#import <QuartzCore/QuartzCore.h>
#import <objc/runtime.h>
// Positions
NSString * CSToastPositionTop = @"CSToastPositionTop";
NSString * CSToastPositionCenter = @"CSToastPositionCenter";
NSString * CSToastPositionBottom = @"CSToastPositionBottom";
// Keys for values associated with toast views
static const NSString * CSToastTimerKey = @"CSToastTimerKey";
static const NSString * CSToastDurationKey = @"CSToastDurationKey";
static const NSString * CSToastPositionKey = @"CSToastPositionKey";
static const NSString * CSToastCompletionKey = @"CSToastCompletionKey";
// Keys for values associated with self
static const NSString * CSToastActiveKey = @"CSToastActiveKey";
static const NSString * CSToastActivityViewKey = @"CSToastActivityViewKey";
static const NSString * CSToastQueueKey = @"CSToastQueueKey";
@interface UIView (ToastPrivate)
/**
These private methods are being prefixed with "cs_" to reduce the likelihood of non-obvious
naming conflicts with other UIView methods.
@discussion Should the public API also use the cs_ prefix? Technically it should, but it
results in code that is less legible. The current public method names seem unlikely to cause
conflicts so I think we should favor the cleaner API for now.
*/
- (void)cs_showToast:(UIView *)toast duration:(NSTimeInterval)duration position:(id)position;
- (void)cs_hideToast:(UIView *)toast;
- (void)cs_hideToast:(UIView *)toast fromTap:(BOOL)fromTap;
- (void)cs_toastTimerDidFinish:(NSTimer *)timer;
- (void)cs_handleToastTapped:(UITapGestureRecognizer *)recognizer;
- (CGPoint)cs_centerPointForPosition:(id)position withToast:(UIView *)toast;
- (NSMutableArray *)cs_toastQueue;
@end
@implementation UIView (MOPFATToast)
#pragma mark - Make Toast Methods
- (void)fatMakeToast:(NSString *)message {
[self fatMakeToast:message duration:[CSToastManager defaultDuration] position:[CSToastManager defaultPosition] style:nil];
}
- (void)fatMakeToast:(NSString *)message duration:(NSTimeInterval)duration position:(id)position {
[self fatMakeToast:message duration:duration position:position style:nil];
}
- (void)fatMakeToast:(NSString *)message duration:(NSTimeInterval)duration position:(id)position style:(CSToastStyle *)style {
UIView *toast = [self fatToastViewForMessage:message title:nil image:nil style:style];
[self fatShowToast:toast duration:duration position:position completion:nil];
}
- (void)fatMakeToast:(NSString *)message duration:(NSTimeInterval)duration position:(id)position title:(NSString *)title image:(UIImage *)image style:(CSToastStyle *)style completion:(void(^)(BOOL didTap))completion {
UIView *toast = [self fatToastViewForMessage:message title:title image:image style:style];
[self fatShowToast:toast duration:duration position:position completion:completion];
}
#pragma mark - Show Toast Methods
- (void)fatShowToast:(UIView *)toast {
[self fatShowToast:toast duration:[CSToastManager defaultDuration] position:[CSToastManager defaultPosition] completion:nil];
}
- (void)fatShowToast:(UIView *)toast duration:(NSTimeInterval)duration position:(id)position completion:(void(^)(BOOL didTap))completion {
// sanity
if (toast == nil) return;
// store the completion block on the toast view
objc_setAssociatedObject(toast, &CSToastCompletionKey, completion, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
if ([CSToastManager isQueueEnabled] && [self.cs_activeToasts count] > 0) {
// we're about to queue this toast view so we need to store the duration and position as well
objc_setAssociatedObject(toast, &CSToastDurationKey, @(duration), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
objc_setAssociatedObject(toast, &CSToastPositionKey, position, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
// enqueue
[self.cs_toastQueue addObject:toast];
} else {
// present
[self cs_showToast:toast duration:duration position:position];
}
}
#pragma mark - Hide Toast Methods
- (void)fatHideToast {
[self fatHideToast:[[self cs_activeToasts] firstObject]];
}
- (void)fatHideToast:(UIView *)toast {
// sanity
if (!toast || ![[self cs_activeToasts] containsObject:toast]) return;
[self cs_hideToast:toast];
}
- (void)fatHideAllToasts {
[self fatHideAllToasts:NO clearQueue:YES];
}
- (void)fatHideAllToasts:(BOOL)includeActivity clearQueue:(BOOL)clearQueue {
if (clearQueue) {
[self fatClearToastQueue];
}
for (UIView *toast in [self cs_activeToasts]) {
[self fatHideToast:toast];
}
if (includeActivity) {
[self fatHideToastActivity];
}
}
- (void)fatClearToastQueue {
[[self cs_toastQueue] removeAllObjects];
}
#pragma mark - Private Show/Hide Methods
- (void)cs_showToast:(UIView *)toast duration:(NSTimeInterval)duration position:(id)position {
toast.center = [self cs_centerPointForPosition:position withToast:toast];
toast.alpha = 0.0;
if ([CSToastManager isTapToDismissEnabled]) {
UITapGestureRecognizer *recognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(cs_handleToastTapped:)];
[toast addGestureRecognizer:recognizer];
toast.userInteractionEnabled = YES;
toast.exclusiveTouch = YES;
}
[[self cs_activeToasts] addObject:toast];
[self addSubview:toast];
[UIView animateWithDuration:[[CSToastManager sharedStyle] fadeDuration]
delay:0.0
options:(UIViewAnimationOptionCurveEaseOut | UIViewAnimationOptionAllowUserInteraction)
animations:^{
toast.alpha = 1.0;
} completion:^(BOOL finished) {
NSTimer *timer = [NSTimer timerWithTimeInterval:duration target:self selector:@selector(cs_toastTimerDidFinish:) userInfo:toast repeats:NO];
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
objc_setAssociatedObject(toast, &CSToastTimerKey, timer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}];
}
- (void)cs_hideToast:(UIView *)toast {
[self cs_hideToast:toast fromTap:NO];
}
- (void)cs_hideToast:(UIView *)toast fromTap:(BOOL)fromTap {
NSTimer *timer = (NSTimer *)objc_getAssociatedObject(toast, &CSToastTimerKey);
[timer invalidate];
[UIView animateWithDuration:[[CSToastManager sharedStyle] fadeDuration]
delay:0.0
options:(UIViewAnimationOptionCurveEaseIn | UIViewAnimationOptionBeginFromCurrentState)
animations:^{
toast.alpha = 0.0;
} completion:^(BOOL finished) {
[toast removeFromSuperview];
// remove
[[self cs_activeToasts] removeObject:toast];
// execute the completion block, if necessary
void (^completion)(BOOL didTap) = objc_getAssociatedObject(toast, &CSToastCompletionKey);
if (completion) {
completion(fromTap);
}
if ([self.cs_toastQueue count] > 0) {
// dequeue
UIView *nextToast = [[self cs_toastQueue] firstObject];
[[self cs_toastQueue] removeObjectAtIndex:0];
// present the next toast
NSTimeInterval duration = [objc_getAssociatedObject(nextToast, &CSToastDurationKey) doubleValue];
id position = objc_getAssociatedObject(nextToast, &CSToastPositionKey);
[self cs_showToast:nextToast duration:duration position:position];
}
}];
}
#pragma mark - View Construction
- (UIView *)fatToastViewForMessage:(NSString *)message title:(NSString *)title image:(UIImage *)image style:(CSToastStyle *)style {
// sanity
if (message == nil && title == nil && image == nil) return nil;
// default to the shared style
if (style == nil) {
style = [CSToastManager sharedStyle];
}
// dynamically build a toast view with any combination of message, title, & image
UILabel *messageLabel = nil;
UILabel *titleLabel = nil;
UIImageView *imageView = nil;
UIView *wrapperView = [[UIView alloc] init];
wrapperView.autoresizingMask = (UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleBottomMargin);
wrapperView.layer.cornerRadius = style.cornerRadius;
if (style.displayShadow) {
wrapperView.layer.shadowColor = style.shadowColor.CGColor;
wrapperView.layer.shadowOpacity = style.shadowOpacity;
wrapperView.layer.shadowRadius = style.shadowRadius;
wrapperView.layer.shadowOffset = style.shadowOffset;
}
wrapperView.backgroundColor = style.backgroundColor;
if(image != nil) {
imageView = [[UIImageView alloc] initWithImage:image];
imageView.contentMode = UIViewContentModeScaleAspectFit;
imageView.frame = CGRectMake(style.horizontalPadding, style.verticalPadding, style.imageSize.width, style.imageSize.height);
}
CGRect imageRect = CGRectZero;
if(imageView != nil) {
imageRect.origin.x = style.horizontalPadding;
imageRect.origin.y = style.verticalPadding;
imageRect.size.width = imageView.bounds.size.width;
imageRect.size.height = imageView.bounds.size.height;
}
if (title != nil) {
titleLabel = [[UILabel alloc] init];
titleLabel.numberOfLines = style.titleNumberOfLines;
titleLabel.font = style.titleFont;
titleLabel.textAlignment = style.titleAlignment;
titleLabel.lineBreakMode = NSLineBreakByTruncatingTail;
titleLabel.textColor = style.titleColor;
titleLabel.backgroundColor = [UIColor clearColor];
titleLabel.alpha = 1.0;
titleLabel.text = title;
// size the title label according to the length of the text
CGSize maxSizeTitle = CGSizeMake((self.bounds.size.width * style.maxWidthPercentage) - imageRect.size.width, self.bounds.size.height * style.maxHeightPercentage);
CGSize expectedSizeTitle = [titleLabel sizeThatFits:maxSizeTitle];
// UILabel can return a size larger than the max size when the number of lines is 1
expectedSizeTitle = CGSizeMake(MIN(maxSizeTitle.width, expectedSizeTitle.width), MIN(maxSizeTitle.height, expectedSizeTitle.height));
titleLabel.frame = CGRectMake(0.0, 0.0, expectedSizeTitle.width, expectedSizeTitle.height);
}
if (message != nil) {
messageLabel = [[UILabel alloc] init];
messageLabel.numberOfLines = style.messageNumberOfLines;
messageLabel.font = style.messageFont;
messageLabel.textAlignment = style.messageAlignment;
messageLabel.lineBreakMode = NSLineBreakByTruncatingTail;
messageLabel.textColor = style.messageColor;
messageLabel.backgroundColor = [UIColor clearColor];
messageLabel.alpha = 1.0;
messageLabel.text = message;
CGSize maxSizeMessage = CGSizeMake((self.bounds.size.width * style.maxWidthPercentage) - imageRect.size.width, self.bounds.size.height * style.maxHeightPercentage);
CGSize expectedSizeMessage = [messageLabel sizeThatFits:maxSizeMessage];
// UILabel can return a size larger than the max size when the number of lines is 1
expectedSizeMessage = CGSizeMake(MIN(maxSizeMessage.width, expectedSizeMessage.width), MIN(maxSizeMessage.height, expectedSizeMessage.height));
messageLabel.frame = CGRectMake(0.0, 0.0, expectedSizeMessage.width, expectedSizeMessage.height);
}
CGRect titleRect = CGRectZero;
if(titleLabel != nil) {
titleRect.origin.x = imageRect.origin.x + imageRect.size.width + style.horizontalPadding;
titleRect.origin.y = style.verticalPadding;
titleRect.size.width = titleLabel.bounds.size.width;
titleRect.size.height = titleLabel.bounds.size.height;
}
CGRect messageRect = CGRectZero;
if(messageLabel != nil) {
messageRect.origin.x = imageRect.origin.x + imageRect.size.width + style.horizontalPadding;
messageRect.origin.y = titleRect.origin.y + titleRect.size.height + style.verticalPadding;
messageRect.size.width = messageLabel.bounds.size.width;
messageRect.size.height = messageLabel.bounds.size.height;
}
CGFloat longerWidth = MAX(titleRect.size.width, messageRect.size.width);
CGFloat longerX = MAX(titleRect.origin.x, messageRect.origin.x);
// Wrapper width uses the longerWidth or the image width, whatever is larger. Same logic applies to the wrapper height.
CGFloat wrapperWidth = MAX((imageRect.size.width + (style.horizontalPadding * 2.0)), (longerX + longerWidth + style.horizontalPadding));
CGFloat wrapperHeight = MAX((messageRect.origin.y + messageRect.size.height + style.verticalPadding), (imageRect.size.height + (style.verticalPadding * 2.0)));
wrapperView.frame = CGRectMake(0.0, 0.0, wrapperWidth, wrapperHeight);
if(titleLabel != nil) {
titleLabel.frame = titleRect;
[wrapperView addSubview:titleLabel];
}
if(messageLabel != nil) {
messageLabel.frame = messageRect;
[wrapperView addSubview:messageLabel];
}
if(imageView != nil) {
[wrapperView addSubview:imageView];
}
return wrapperView;
}
#pragma mark - Storage
- (NSMutableArray *)cs_activeToasts {
NSMutableArray *cs_activeToasts = objc_getAssociatedObject(self, &CSToastActiveKey);
if (cs_activeToasts == nil) {
cs_activeToasts = [[NSMutableArray alloc] init];
objc_setAssociatedObject(self, &CSToastActiveKey, cs_activeToasts, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
return cs_activeToasts;
}
- (NSMutableArray *)cs_toastQueue {
NSMutableArray *cs_toastQueue = objc_getAssociatedObject(self, &CSToastQueueKey);
if (cs_toastQueue == nil) {
cs_toastQueue = [[NSMutableArray alloc] init];
objc_setAssociatedObject(self, &CSToastQueueKey, cs_toastQueue, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
return cs_toastQueue;
}
#pragma mark - Events
- (void)cs_toastTimerDidFinish:(NSTimer *)timer {
[self cs_hideToast:(UIView *)timer.userInfo];
}
- (void)cs_handleToastTapped:(UITapGestureRecognizer *)recognizer {
UIView *toast = recognizer.view;
NSTimer *timer = (NSTimer *)objc_getAssociatedObject(toast, &CSToastTimerKey);
[timer invalidate];
[self cs_hideToast:toast fromTap:YES];
}
#pragma mark - Activity Methods
- (void)fatMakeToastActivity:(id)position {
// sanity
UIView *existingActivityView = (UIView *)objc_getAssociatedObject(self, &CSToastActivityViewKey);
if (existingActivityView != nil) return;
CSToastStyle *style = [CSToastManager sharedStyle];
UIView *activityView = [[UIView alloc] initWithFrame:CGRectMake(0.0, 0.0, style.activitySize.width, style.activitySize.height)];
activityView.center = [self cs_centerPointForPosition:position withToast:activityView];
activityView.backgroundColor = style.backgroundColor;
activityView.alpha = 0.0;
activityView.autoresizingMask = (UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleBottomMargin);
activityView.layer.cornerRadius = style.cornerRadius;
if (style.displayShadow) {
activityView.layer.shadowColor = style.shadowColor.CGColor;
activityView.layer.shadowOpacity = style.shadowOpacity;
activityView.layer.shadowRadius = style.shadowRadius;
activityView.layer.shadowOffset = style.shadowOffset;
}
UIActivityIndicatorView *activityIndicatorView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
activityIndicatorView.center = CGPointMake(activityView.bounds.size.width / 2, activityView.bounds.size.height / 2);
[activityView addSubview:activityIndicatorView];
[activityIndicatorView startAnimating];
// associate the activity view with self
objc_setAssociatedObject (self, &CSToastActivityViewKey, activityView, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
[self addSubview:activityView];
[UIView animateWithDuration:style.fadeDuration
delay:0.0
options:UIViewAnimationOptionCurveEaseOut
animations:^{
activityView.alpha = 1.0;
} completion:nil];
}
- (void)fatHideToastActivity {
UIView *existingActivityView = (UIView *)objc_getAssociatedObject(self, &CSToastActivityViewKey);
if (existingActivityView != nil) {
[UIView animateWithDuration:[[CSToastManager sharedStyle] fadeDuration]
delay:0.0
options:(UIViewAnimationOptionCurveEaseIn | UIViewAnimationOptionBeginFromCurrentState)
animations:^{
existingActivityView.alpha = 0.0;
} completion:^(BOOL finished) {
[existingActivityView removeFromSuperview];
objc_setAssociatedObject (self, &CSToastActivityViewKey, nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}];
}
}
#pragma mark - Helpers
- (CGPoint)cs_centerPointForPosition:(id)point withToast:(UIView *)toast {
CSToastStyle *style = [CSToastManager sharedStyle];
UIEdgeInsets safeInsets = UIEdgeInsetsZero;
if (@available(iOS 11.0, *)) {
safeInsets = self.safeAreaInsets;
}
CGFloat topPadding = style.verticalPadding + safeInsets.top;
CGFloat bottomPadding = style.verticalPadding + safeInsets.bottom;
if([point isKindOfClass:[NSString class]]) {
if([point caseInsensitiveCompare:CSToastPositionTop] == NSOrderedSame) {
return CGPointMake(self.bounds.size.width / 2.0, (toast.frame.size.height / 2.0) + topPadding);
} else if([point caseInsensitiveCompare:CSToastPositionCenter] == NSOrderedSame) {
return CGPointMake(self.bounds.size.width / 2.0, self.bounds.size.height / 2.0);
}
} else if ([point isKindOfClass:[NSValue class]]) {
return [point CGPointValue];
}
// default to bottom
return CGPointMake(self.bounds.size.width / 2.0, (self.bounds.size.height - (toast.frame.size.height / 2.0)) - bottomPadding);
}
@end
@implementation CSToastStyle
#pragma mark - Constructors
- (instancetype)initWithDefaultStyle {
self = [super init];
if (self) {
self.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.8];
self.titleColor = [UIColor whiteColor];
self.messageColor = [UIColor whiteColor];
self.maxWidthPercentage = 0.8;
self.maxHeightPercentage = 0.8;
self.horizontalPadding = 10.0;
self.verticalPadding = 10.0;
self.cornerRadius = 10.0;
self.titleFont = [UIFont boldSystemFontOfSize:16.0];
self.messageFont = [UIFont systemFontOfSize:16.0];
self.titleAlignment = NSTextAlignmentLeft;
self.messageAlignment = NSTextAlignmentLeft;
self.titleNumberOfLines = 0;
self.messageNumberOfLines = 0;
self.displayShadow = NO;
self.shadowOpacity = 0.8;
self.shadowRadius = 6.0;
self.shadowOffset = CGSizeMake(4.0, 4.0);
self.imageSize = CGSizeMake(80.0, 80.0);
self.activitySize = CGSizeMake(100.0, 100.0);
self.fadeDuration = 0.2;
}
return self;
}
- (void)setMaxWidthPercentage:(CGFloat)maxWidthPercentage {
_maxWidthPercentage = MAX(MIN(maxWidthPercentage, 1.0), 0.0);
}
- (void)setMaxHeightPercentage:(CGFloat)maxHeightPercentage {
_maxHeightPercentage = MAX(MIN(maxHeightPercentage, 1.0), 0.0);
}
- (instancetype)init NS_UNAVAILABLE {
return nil;
}
@end
@interface CSToastManager ()
@property (strong, nonatomic) CSToastStyle *sharedStyle;
@property (assign, nonatomic, getter=isTapToDismissEnabled) BOOL tapToDismissEnabled;
@property (assign, nonatomic, getter=isQueueEnabled) BOOL queueEnabled;
@property (assign, nonatomic) NSTimeInterval defaultDuration;
@property (strong, nonatomic) id defaultPosition;
@end
@implementation CSToastManager
#pragma mark - Constructors
+ (instancetype)sharedManager {
static CSToastManager *_sharedManager = nil;
static dispatch_once_t oncePredicate;
dispatch_once(&oncePredicate, ^{
_sharedManager = [[self alloc] init];
});
return _sharedManager;
}
- (instancetype)init {
self = [super init];
if (self) {
self.sharedStyle = [[CSToastStyle alloc] initWithDefaultStyle];
self.tapToDismissEnabled = YES;
self.queueEnabled = NO;
self.defaultDuration = 3.0;
self.defaultPosition = CSToastPositionBottom;
}
return self;
}
#pragma mark - Singleton Methods
+ (void)setSharedStyle:(CSToastStyle *)sharedStyle {
[[self sharedManager] setSharedStyle:sharedStyle];
}
+ (CSToastStyle *)sharedStyle {
return [[self sharedManager] sharedStyle];
}
+ (void)setTapToDismissEnabled:(BOOL)tapToDismissEnabled {
[[self sharedManager] setTapToDismissEnabled:tapToDismissEnabled];
}
+ (BOOL)isTapToDismissEnabled {
return [[self sharedManager] isTapToDismissEnabled];
}
+ (void)setQueueEnabled:(BOOL)queueEnabled {
[[self sharedManager] setQueueEnabled:queueEnabled];
}
+ (BOOL)isQueueEnabled {
return [[self sharedManager] isQueueEnabled];
}
+ (void)setDefaultDuration:(NSTimeInterval)duration {
[[self sharedManager] setDefaultDuration:duration];
}
+ (NSTimeInterval)defaultDuration {
return [[self sharedManager] defaultDuration];
}
+ (void)setDefaultPosition:(id)position {
if ([position isKindOfClass:[NSString class]] || [position isKindOfClass:[NSValue class]]) {
[[self sharedManager] setDefaultPosition:position];
}
}
+ (id)defaultPosition {
return [[self sharedManager] defaultPosition];
}
@end