| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391 |
- //
- // UIView+Toast.m
- // Toast
- //
- // Copyright 2014 Charles Scalesse.
- //
- #import "UIView+Toast.h"
- @import QuartzCore;
- #import <objc/runtime.h>
- /*
- * CONFIGURE THESE VALUES TO ADJUST LOOK & FEEL,
- * DISPLAY DURATION, ETC.
- */
- // general appearance
- static const CGFloat CSToastMaxWidth = 0.8; // 80% of parent view width
- static const CGFloat CSToastMaxHeight = 0.8; // 80% of parent view height
- static const CGFloat CSToastHorizontalPadding = 10.0;
- static const CGFloat CSToastVerticalPadding = 10.0;
- static const CGFloat CSToastCornerRadius = 10.0;
- static const CGFloat CSToastOpacity = 0.8;
- static const CGFloat CSToastFontSize = 16.0;
- static const CGFloat CSToastMaxTitleLines = 0;
- static const CGFloat CSToastMaxMessageLines = 0;
- static const NSTimeInterval CSToastFadeDuration = 0.2;
- // shadow appearance
- static const CGFloat CSToastShadowOpacity = 0.1;
- static const CGFloat CSToastShadowRadius = 5.0;
- static const CGSize CSToastShadowOffset = { 4.0, 4.0 };
- static const BOOL CSToastDisplayShadow = YES;
- // display duration and position
- static const NSString * CSToastDefaultPosition = @"bottom";
- static const NSTimeInterval CSToastDefaultDuration = 3.0;
- // image view size
- static const CGFloat CSToastImageViewWidth = 80.0;
- static const CGFloat CSToastImageViewHeight = 80.0;
- // activity
- static const CGFloat CSToastActivityWidth = 100.0;
- static const CGFloat CSToastActivityHeight = 100.0;
- static const NSString * CSToastActivityDefaultPosition = @"center";
- // interaction
- static const BOOL CSToastHidesOnTap = YES; // excludes activity views
- // associative reference keys
- static const NSString * CSToastTimerKey = @"CSToastTimerKey";
- static const NSString * CSToastActivityViewKey = @"CSToastActivityViewKey";
- static const NSString * CSToastTapCallbackKey = @"CSToastTapCallbackKey";
- static BOOL CSToastIsShowing = NO; // excludes activity views
- @interface UIView (ToastPrivate)
- - (void)hideToast:(UIView *)toast;
- - (void)toastTimerDidFinish:(NSTimer *)timer;
- - (void)handleToastTapped:(UITapGestureRecognizer *)recognizer;
- - (CGPoint)centerPointForPosition:(id)position withToast:(UIView *)toast;
- - (UIView *)viewForMessage:(NSString *)message title:(NSString *)title image:(UIImage *)image;
- - (CGSize)sizeForString:(NSString *)string font:(UIFont *)font constrainedToSize:(CGSize)constrainedSize lineBreakMode:(NSLineBreakMode)lineBreakMode;
- @end
- @implementation UIView (Toast)
- #pragma mark - Toast Methods
- - (void)makeToast:(NSString *)message {
- [self makeToast:message duration:CSToastDefaultDuration position:CSToastDefaultPosition];
- }
- - (void)makeToast:(NSString *)message duration:(NSTimeInterval)duration position:(id)position {
- UIView *toast = [self viewForMessage:message title:nil image:nil];
- [self showToast:toast duration:duration position:position];
- }
- - (void)makeToast:(NSString *)message duration:(NSTimeInterval)duration position:(id)position title:(NSString *)title {
- UIView *toast = [self viewForMessage:message title:title image:nil];
- [self showToast:toast duration:duration position:position];
- }
- - (void)makeToast:(NSString *)message duration:(NSTimeInterval)duration position:(id)position image:(UIImage *)image {
- UIView *toast = [self viewForMessage:message title:nil image:image];
- [self showToast:toast duration:duration position:position];
- }
- - (void)makeToast:(NSString *)message duration:(NSTimeInterval)duration position:(id)position title:(NSString *)title image:(UIImage *)image {
- UIView *toast = [self viewForMessage:message title:title image:image];
- [self showToast:toast duration:duration position:position];
- }
- - (void)showToast:(UIView *)toast {
- [self showToast:toast duration:CSToastDefaultDuration position:CSToastDefaultPosition];
- }
- - (void)showToast:(UIView *)toast duration:(NSTimeInterval)duration position:(id)point {
- [self showToast:toast duration:duration position:point tapCallback:nil];
-
- }
- - (void)showToast:(UIView *)toast duration:(NSTimeInterval)duration position:(id)point
- tapCallback:(void(^)(void))tapCallback
- {
- if (CSToastIsShowing) {
- return;
- }
- CSToastIsShowing = YES;
- toast.center = [self centerPointForPosition:point withToast:toast];
- toast.alpha = 0.0;
-
- if (CSToastHidesOnTap) {
- UITapGestureRecognizer *recognizer = [[UITapGestureRecognizer alloc] initWithTarget:toast action:@selector(handleToastTapped:)];
- [toast addGestureRecognizer:recognizer];
- toast.userInteractionEnabled = YES;
- toast.exclusiveTouch = YES;
- }
-
- [self addSubview:toast];
-
- [UIView animateWithDuration:CSToastFadeDuration
- delay:0.0
- options:(UIViewAnimationOptionCurveEaseOut | UIViewAnimationOptionAllowUserInteraction)
- animations:^{
- toast.alpha = 1.0;
- } completion:^(BOOL finished) {
- NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:duration target:self selector:@selector(toastTimerDidFinish:) userInfo:toast repeats:NO];
- // associate the timer with the toast view
- objc_setAssociatedObject (toast, &CSToastTimerKey, timer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
- objc_setAssociatedObject (toast, &CSToastTapCallbackKey, tapCallback, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
- }];
- }
- - (void)hideToast:(UIView *)toast {
- [UIView animateWithDuration:CSToastFadeDuration
- delay:0.0
- options:(UIViewAnimationOptionCurveEaseIn | UIViewAnimationOptionBeginFromCurrentState)
- animations:^{
- toast.alpha = 0.0;
- } completion:^(BOOL finished) {
- [toast removeFromSuperview];
- CSToastIsShowing = NO;
- }];
- }
- #pragma mark - Events
- - (void)toastTimerDidFinish:(NSTimer *)timer {
- [self hideToast:(UIView *)timer.userInfo];
- }
- - (void)handleToastTapped:(UITapGestureRecognizer *)recognizer {
- NSTimer *timer = (NSTimer *)objc_getAssociatedObject(self, &CSToastTimerKey);
- [timer invalidate];
-
- void (^callback)(void) = objc_getAssociatedObject(self, &CSToastTapCallbackKey);
- if (callback) {
- callback();
- }
- [self hideToast:recognizer.view];
- }
- #pragma mark - Toast Activity Methods
- - (void)makeToastActivity {
- [self makeToastActivity:CSToastActivityDefaultPosition];
- }
- - (void)makeToastActivity:(id)position {
- // sanity
- UIView *existingActivityView = (UIView *)objc_getAssociatedObject(self, &CSToastActivityViewKey);
- if (existingActivityView != nil) return;
-
- UIView *activityView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, CSToastActivityWidth, CSToastActivityHeight)];
- activityView.center = [self centerPointForPosition:position withToast:activityView];
- activityView.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:CSToastOpacity];
- activityView.alpha = 0.0;
- activityView.autoresizingMask = (UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleBottomMargin);
- activityView.layer.cornerRadius = CSToastCornerRadius;
-
- if (CSToastDisplayShadow) {
- activityView.layer.shadowColor = [UIColor blackColor].CGColor;
- activityView.layer.shadowOpacity = CSToastShadowOpacity;
- activityView.layer.shadowRadius = CSToastShadowRadius;
- activityView.layer.shadowOffset = CSToastShadowOffset;
- }
-
- 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:CSToastFadeDuration
- delay:0.0
- options:UIViewAnimationOptionCurveEaseOut
- animations:^{
- activityView.alpha = 1.0;
- } completion:nil];
- }
- - (void)hideToastActivity {
- UIView *existingActivityView = (UIView *)objc_getAssociatedObject(self, &CSToastActivityViewKey);
- if (existingActivityView != nil) {
- [UIView animateWithDuration:CSToastFadeDuration
- 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)centerPointForPosition:(id)point withToast:(UIView *)toast {
- if([point isKindOfClass:[NSString class]]) {
- // convert string literals @"top", @"bottom", @"center", or any point wrapped in an NSValue object into a CGPoint
- if([point caseInsensitiveCompare:@"top"] == NSOrderedSame) {
- return CGPointMake(self.bounds.size.width/2, (toast.frame.size.height / 2) + CSToastVerticalPadding);
- } else if([point caseInsensitiveCompare:@"bottom"] == NSOrderedSame) {
- return CGPointMake(self.bounds.size.width/2, (self.bounds.size.height - (toast.frame.size.height / 2)) - CSToastVerticalPadding);
- } else if([point caseInsensitiveCompare:@"center"] == NSOrderedSame) {
- return CGPointMake(self.bounds.size.width / 2, self.bounds.size.height / 2);
- }
- } else if ([point isKindOfClass:[NSValue class]]) {
- return [point CGPointValue];
- }
-
- NSLog(@"Warning: Invalid position for toast.");
- return [self centerPointForPosition:CSToastDefaultPosition withToast:toast];
- }
- - (CGSize)sizeForString:(NSString *)string font:(UIFont *)font constrainedToSize:(CGSize)constrainedSize lineBreakMode:(NSLineBreakMode)lineBreakMode {
- if ([string respondsToSelector:@selector(boundingRectWithSize:options:attributes:context:)]) {
- NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init];
- paragraphStyle.lineBreakMode = lineBreakMode;
- NSDictionary *attributes = @{NSFontAttributeName:font, NSParagraphStyleAttributeName:paragraphStyle};
- CGRect boundingRect = [string boundingRectWithSize:constrainedSize options:NSStringDrawingUsesLineFragmentOrigin attributes:attributes context:nil];
- return CGSizeMake(ceilf(boundingRect.size.width), ceilf(boundingRect.size.height));
- }
- #pragma clang diagnostic push
- #pragma clang diagnostic ignored "-Wdeprecated-declarations"
- return [string sizeWithFont:font constrainedToSize:constrainedSize lineBreakMode:lineBreakMode];
- #pragma clang diagnostic pop
- }
- - (UIView *)viewForMessage:(NSString *)message title:(NSString *)title image:(UIImage *)image {
- // sanity
- if((message == nil) && (title == nil) && (image == nil)) return nil;
- // dynamically build a toast view with any combination of message, title, & image.
- UILabel *messageLabel = nil;
- UILabel *titleLabel = nil;
- UIImageView *imageView = nil;
-
- // create the parent view
- UIView *wrapperView = [[UIView alloc] init];
- wrapperView.autoresizingMask = (UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleBottomMargin);
- wrapperView.layer.cornerRadius = CSToastCornerRadius;
-
- if (CSToastDisplayShadow) {
- wrapperView.layer.shadowColor = [UIColor blackColor].CGColor;
- wrapperView.layer.shadowOpacity = CSToastShadowOpacity;
- wrapperView.layer.shadowRadius = CSToastShadowRadius;
- wrapperView.layer.shadowOffset = CSToastShadowOffset;
- }
- wrapperView.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:CSToastOpacity];
-
- if(image != nil) {
- imageView = [[UIImageView alloc] initWithImage:image];
- imageView.contentMode = UIViewContentModeScaleAspectFit;
- imageView.frame = CGRectMake(CSToastHorizontalPadding, CSToastVerticalPadding, CSToastImageViewWidth, CSToastImageViewHeight);
- }
-
- CGFloat imageWidth, imageHeight, imageLeft;
-
- // the imageView frame values will be used to size & position the other views
- if(imageView != nil) {
- imageWidth = imageView.bounds.size.width;
- imageHeight = imageView.bounds.size.height;
- imageLeft = CSToastHorizontalPadding;
- } else {
- imageWidth = imageHeight = imageLeft = 0.0;
- }
-
- if (title != nil) {
- titleLabel = [[UILabel alloc] init];
- titleLabel.numberOfLines = CSToastMaxTitleLines;
- titleLabel.font = [UIFont boldSystemFontOfSize:CSToastFontSize];
- titleLabel.textAlignment = NSTextAlignmentLeft;
- titleLabel.lineBreakMode = NSLineBreakByWordWrapping;
- titleLabel.textColor = [UIColor whiteColor];
- 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 * CSToastMaxWidth) - imageWidth, self.bounds.size.height * CSToastMaxHeight);
- CGSize expectedSizeTitle = [self sizeForString:title font:titleLabel.font constrainedToSize:maxSizeTitle lineBreakMode:titleLabel.lineBreakMode];
- titleLabel.frame = CGRectMake(0.0, 0.0, expectedSizeTitle.width, expectedSizeTitle.height);
- }
-
- if (message != nil) {
- messageLabel = [[UILabel alloc] init];
- messageLabel.numberOfLines = CSToastMaxMessageLines;
- messageLabel.font = [UIFont systemFontOfSize:CSToastFontSize];
- messageLabel.lineBreakMode = NSLineBreakByWordWrapping;
- messageLabel.textColor = [UIColor whiteColor];
- messageLabel.backgroundColor = [UIColor clearColor];
- messageLabel.alpha = 1.0;
- messageLabel.text = message;
-
- // size the message label according to the length of the text
- CGSize maxSizeMessage = CGSizeMake((self.bounds.size.width * CSToastMaxWidth) - imageWidth, self.bounds.size.height * CSToastMaxHeight);
- CGSize expectedSizeMessage = [self sizeForString:message font:messageLabel.font constrainedToSize:maxSizeMessage lineBreakMode:messageLabel.lineBreakMode];
- messageLabel.frame = CGRectMake(0.0, 0.0, expectedSizeMessage.width, expectedSizeMessage.height);
- }
-
- // titleLabel frame values
- CGFloat titleWidth, titleHeight, titleTop, titleLeft;
-
- if(titleLabel != nil) {
- titleWidth = titleLabel.bounds.size.width;
- titleHeight = titleLabel.bounds.size.height;
- titleTop = CSToastVerticalPadding;
- titleLeft = imageLeft + imageWidth + CSToastHorizontalPadding;
- } else {
- titleWidth = titleHeight = titleTop = titleLeft = 0.0;
- }
-
- // messageLabel frame values
- CGFloat messageWidth, messageHeight, messageLeft, messageTop;
- if(messageLabel != nil) {
- messageWidth = messageLabel.bounds.size.width;
- messageHeight = messageLabel.bounds.size.height;
- messageLeft = imageLeft + imageWidth + CSToastHorizontalPadding;
- messageTop = titleTop + titleHeight + CSToastVerticalPadding;
- } else {
- messageWidth = messageHeight = messageLeft = messageTop = 0.0;
- }
- CGFloat longerWidth = MAX(titleWidth, messageWidth);
- CGFloat longerLeft = MAX(titleLeft, messageLeft);
-
- // wrapper width uses the longerWidth or the image width, whatever is larger. same logic applies to the wrapper height
- CGFloat wrapperWidth = MAX((imageWidth + (CSToastHorizontalPadding * 2)), (longerLeft + longerWidth + CSToastHorizontalPadding));
- CGFloat wrapperHeight = MAX((messageTop + messageHeight + CSToastVerticalPadding), (imageHeight + (CSToastVerticalPadding * 2)));
-
- wrapperView.frame = CGRectMake(0.0, 0.0, wrapperWidth, wrapperHeight);
-
- if(titleLabel != nil) {
- titleLabel.frame = CGRectMake(titleLeft, titleTop, titleWidth, titleHeight);
- [wrapperView addSubview:titleLabel];
- }
-
- if(messageLabel != nil) {
- messageLabel.frame = CGRectMake(messageLeft, messageTop, messageWidth, messageHeight);
- [wrapperView addSubview:messageLabel];
- }
-
- if(imageView != nil) {
- [wrapperView addSubview:imageView];
- }
-
- return wrapperView;
- }
- @end
|