#import "Toast+UIView.h" #import <QuartzCore/QuartzCore.h> #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 = 16.0; static const CGFloat CSToastVerticalPadding = 12.0; static const CGFloat CSToastTopBottomOffset = 20.0; static const CGFloat CSToastCornerRadius = 20.0; static const CGFloat CSToastOpacity = 0.8; static const CGFloat CSToastFontSize = 13.0; static const CGFloat CSToastMaxTitleLines = 0; static const CGFloat CSToastMaxMessageLines = 0; static const NSTimeInterval CSToastFadeDuration = 0.3; // shadow appearance static const CGFloat CSToastShadowOpacity = 0.8; static const CGFloat CSToastShadowRadius = 6.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 UIView *prevToast = NULL; // doesn't matter these are static static id commandDelegate; static id callbackId; static id msg; static id data; static id styling; @interface UIView (ToastPrivate) - (void)hideToast:(UIView *)toast; - (void)toastTimerDidFinish:(NSTimer *)timer; - (void)handleToastTapped:(UITapGestureRecognizer *)recognizer; - (CGPoint)centerPointForPosition:(id)position withToast:(UIView *)toast withAddedPixelsY:(int) addPixelsY; - (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 addPixelsY:(int)addPixelsY data:(NSDictionary*)_data styling:(NSDictionary*)_styling commandDelegate:(id <CDVCommandDelegate>)_commandDelegate callbackId:(NSString *)_callbackId { commandDelegate = _commandDelegate; callbackId = _callbackId; msg = message; data = _data; styling = _styling; UIView *toast = [self viewForMessage:message title:nil image:nil]; [self showToast:toast duration:duration position:position addedPixelsY:addPixelsY]; } - (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:CSToastDefaultDuration position:CSToastDefaultPosition addedPixelsY:0]; } - (void)showToast:(UIView *)toast duration:(NSTimeInterval)duration position:(id)point addedPixelsY:(int) addPixelsY { [self hideToast]; prevToast = toast; toast.center = [self centerPointForPosition:point withToast:toast withAddedPixelsY:addPixelsY]; toast.alpha = 0.0; // note that we changed this to be always true if (CSToastHidesOnTap) { UITapGestureRecognizer *recognizer = [[UITapGestureRecognizer alloc] initWithTarget:toast action:@selector(handleToastTapped:)]; [toast addGestureRecognizer:recognizer]; toast.userInteractionEnabled = YES; toast.exclusiveTouch = YES; } // make sure that if InAppBrowser is active, we're still showing Toasts on top of it UIViewController *vc = [self getTopMostViewController]; UIView *v = [vc view]; [v addSubview:toast]; NSNumber * opacity = styling[@"opacity"]; CGFloat theOpacity = opacity == nil ? CSToastOpacity : [opacity floatValue]; [UIView animateWithDuration:CSToastFadeDuration delay:0.0 options:(UIViewAnimationOptionCurveEaseOut | UIViewAnimationOptionAllowUserInteraction) animations:^{ toast.alpha = theOpacity; } 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); }]; } - (UIViewController*) getTopMostViewController { UIViewController *presentingViewController = [[[UIApplication sharedApplication] delegate] window].rootViewController; while (presentingViewController.presentedViewController != nil) { presentingViewController = presentingViewController.presentedViewController; } return presentingViewController; } - (void)hideToast { if (prevToast){ [self hideToast:prevToast]; } } - (void)hideToast:(UIView *)toast { [UIView animateWithDuration:CSToastFadeDuration delay:0.0 options:(UIViewAnimationOptionCurveEaseIn | UIViewAnimationOptionBeginFromCurrentState) animations:^{ toast.alpha = 0.0; } completion:^(BOOL finished) { [toast removeFromSuperview]; }]; } #pragma mark - Events - (void)toastTimerDidFinish:(NSTimer *)timer { [self hideToast:(UIView *)timer.userInfo]; // also send an event back to JS NSMutableDictionary *dict = [[NSMutableDictionary alloc] initWithObjectsAndKeys:msg, @"message", @"hide", @"event", nil]; if (data != nil) { [dict setObject:data forKey:@"data"]; } CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:dict]; [commandDelegate sendPluginResult:pluginResult callbackId:callbackId]; } - (void)handleToastTapped:(UITapGestureRecognizer *)recognizer { NSTimer *timer = (NSTimer *)objc_getAssociatedObject(self, &CSToastTimerKey); [timer invalidate]; [self hideToast:recognizer.view]; // also send an event back to JS NSMutableDictionary *dict = [[NSMutableDictionary alloc] initWithObjectsAndKeys:msg, @"message", @"touch", @"event", nil]; if (data != nil) { [dict setObject:data forKey:@"data"]; } CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:dict]; [commandDelegate sendPluginResult:pluginResult callbackId:callbackId]; } #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 withAddedPixelsY:0]; 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 withAddedPixelsY:(int) addPixelsY { 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) + addPixelsY + CSToastVerticalPadding + CSToastTopBottomOffset); } else if([point caseInsensitiveCompare:@"bottom"] == NSOrderedSame) { return CGPointMake(self.bounds.size.width/2, (self.bounds.size.height - (toast.frame.size.height / 2)) - CSToastVerticalPadding - CSToastTopBottomOffset + addPixelsY); } else if([point caseInsensitiveCompare:@"center"] == NSOrderedSame) { return CGPointMake(self.bounds.size.width / 2, (self.bounds.size.height / 2) + addPixelsY); } } else if ([point isKindOfClass:[NSValue class]]) { return [point CGPointValue]; } NSLog(@"Warning: Invalid position for toast."); return [self centerPointForPosition:CSToastDefaultPosition withToast:toast withAddedPixelsY:addPixelsY]; } - (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); NSNumber * cornerRadius = styling[@"cornerRadius"]; wrapperView.layer.cornerRadius = cornerRadius == nil ? CSToastCornerRadius : [cornerRadius floatValue]; if (CSToastDisplayShadow) { wrapperView.layer.shadowColor = [UIColor blackColor].CGColor; wrapperView.layer.shadowOpacity = CSToastShadowOpacity; wrapperView.layer.shadowRadius = CSToastShadowRadius; wrapperView.layer.shadowOffset = CSToastShadowOffset; } NSString * backgroundColor = styling[@"backgroundColor"]; UIColor *theColor = backgroundColor == nil ? [UIColor blackColor] : [self colorFromHexString:backgroundColor]; NSNumber * horizontalPadding = styling[@"horizontalPadding"]; CGFloat theHorizontalPadding = horizontalPadding == nil ? CSToastHorizontalPadding : [horizontalPadding floatValue]; NSNumber * verticalPadding = styling[@"verticalPadding"]; CGFloat theVerticalPadding = verticalPadding == nil ? CSToastVerticalPadding : [verticalPadding floatValue]; NSNumber * textSize = styling[@"textSize"]; CGFloat theTextSize = textSize == nil ? CSToastFontSize : [textSize floatValue]; wrapperView.backgroundColor = theColor; if(image != nil) { imageView = [[UIImageView alloc] initWithImage:image]; imageView.contentMode = UIViewContentModeScaleAspectFit; imageView.frame = CGRectMake(theHorizontalPadding, theVerticalPadding, 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 = theHorizontalPadding; } else { imageWidth = imageHeight = imageLeft = 0.0; } if (title != nil) { NSString * titleLabelTextColor = styling[@"textColor"]; UIColor *theTitleLabelTextColor = titleLabelTextColor == nil ? [UIColor whiteColor] : [self colorFromHexString:titleLabelTextColor]; titleLabel = [[UILabel alloc] init]; titleLabel.numberOfLines = CSToastMaxTitleLines; titleLabel.font = [UIFont boldSystemFontOfSize:theTextSize]; titleLabel.textAlignment = NSTextAlignmentCenter; titleLabel.lineBreakMode = NSLineBreakByWordWrapping; titleLabel.textColor = theTitleLabelTextColor; 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) { NSString * messageLabelTextColor = styling[@"textColor"]; UIColor *theMessageLabelTextColor = messageLabelTextColor == nil ? [UIColor whiteColor] : [self colorFromHexString:messageLabelTextColor]; messageLabel = [[UILabel alloc] init]; messageLabel.numberOfLines = CSToastMaxMessageLines; messageLabel.font = [UIFont systemFontOfSize:theTextSize]; messageLabel.lineBreakMode = NSLineBreakByWordWrapping; messageLabel.textAlignment = NSTextAlignmentCenter; messageLabel.textColor = theMessageLabelTextColor; 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 = theVerticalPadding; titleLeft = imageLeft + imageWidth + theHorizontalPadding; } 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 + theHorizontalPadding; messageTop = titleTop + titleHeight + theVerticalPadding; } 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 + (theHorizontalPadding * 2)), (longerLeft + longerWidth + theHorizontalPadding)); CGFloat wrapperHeight = MAX((messageTop + messageHeight + theVerticalPadding), (imageHeight + (theVerticalPadding * 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; } // Assumes input like "#00FF00" (#RRGGBB) - (UIColor*) colorFromHexString:(NSString*) hexString { unsigned rgbValue = 0; NSScanner *scanner = [NSScanner scannerWithString:hexString]; [scanner setScanLocation:1]; // bypass '#' character [scanner scanHexInt:&rgbValue]; return [UIColor colorWithRed:((rgbValue & 0xFF0000) >> 16) / 255.0 green:((rgbValue & 0xFF00) >> 8) / 255.0 blue:(rgbValue & 0xFF) / 255.0 alpha:1.0]; } @end