Toast+UIView.m 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389
  1. #import "Toast+UIView.h"
  2. #import <QuartzCore/QuartzCore.h>
  3. #import <objc/runtime.h>
  4. /*
  5. * CONFIGURE THESE VALUES TO ADJUST LOOK & FEEL,
  6. * DISPLAY DURATION, ETC.
  7. */
  8. // general appearance
  9. static const CGFloat CSToastMaxWidth = 0.8; // 80% of parent view width
  10. static const CGFloat CSToastMaxHeight = 0.8; // 80% of parent view height
  11. static const CGFloat CSToastHorizontalPadding = 10.0;
  12. static const CGFloat CSToastVerticalPadding = 10.0;
  13. static const CGFloat CSToastTopBottomOffset = 20.0;
  14. static const CGFloat CSToastCornerRadius = 5.0;
  15. static const CGFloat CSToastOpacity = 0.8;
  16. static const CGFloat CSToastFontSize = 16.0;
  17. static const CGFloat CSToastMaxTitleLines = 0;
  18. static const CGFloat CSToastMaxMessageLines = 0;
  19. static const NSTimeInterval CSToastFadeDuration = 0.2;
  20. // shadow appearance
  21. static const CGFloat CSToastShadowOpacity = 0.8;
  22. static const CGFloat CSToastShadowRadius = 6.0;
  23. static const CGSize CSToastShadowOffset = { 4.0, 4.0 };
  24. static const BOOL CSToastDisplayShadow = YES;
  25. // display duration and position
  26. static const NSString * CSToastDefaultPosition = @"bottom";
  27. static const NSTimeInterval CSToastDefaultDuration = 1.5;
  28. // image view size
  29. static const CGFloat CSToastImageViewWidth = 80.0;
  30. static const CGFloat CSToastImageViewHeight = 80.0;
  31. // activity
  32. static const CGFloat CSToastActivityWidth = 100.0;
  33. static const CGFloat CSToastActivityHeight = 100.0;
  34. static const NSString * CSToastActivityDefaultPosition = @"center";
  35. // interaction
  36. static const BOOL CSToastHidesOnTap = YES; // excludes activity views
  37. // associative reference keys
  38. static const NSString * CSToastTimerKey = @"CSToastTimerKey";
  39. static const NSString * CSToastActivityViewKey = @"CSToastActivityViewKey";
  40. static UIView *prevToast = NULL;
  41. @interface UIView (ToastPrivate)
  42. - (void)hideToast:(UIView *)toast;
  43. - (void)toastTimerDidFinish:(NSTimer *)timer;
  44. - (void)handleToastTapped:(UITapGestureRecognizer *)recognizer;
  45. - (CGPoint)centerPointForPosition:(id)position withToast:(UIView *)toast withAddedPixelsY:(int) addPixelsY;
  46. - (UIView *)viewForMessage:(NSString *)message title:(NSString *)title image:(UIImage *)image;
  47. - (CGSize)sizeForString:(NSString *)string font:(UIFont *)font constrainedToSize:(CGSize)constrainedSize lineBreakMode:(NSLineBreakMode)lineBreakMode;
  48. @end
  49. @implementation UIView (Toast)
  50. #pragma mark - Toast Methods
  51. - (void)makeToast:(NSString *)message {
  52. [self makeToast:message duration:CSToastDefaultDuration position:CSToastDefaultPosition];
  53. }
  54. - (void)makeToast:(NSString *)message duration:(NSTimeInterval)duration position:(id)position {
  55. UIView *toast = [self viewForMessage:message title:nil image:nil];
  56. [self showToast:toast duration:duration position:position];
  57. }
  58. - (void)makeToast:(NSString *)message duration:(NSTimeInterval)duration position:(id)position addPixelsY:(int)addPixelsY {
  59. UIView *toast = [self viewForMessage:message title:nil image:nil];
  60. [self showToast:toast duration:duration position:position addedPixelsY:addPixelsY];
  61. }
  62. - (void)makeToast:(NSString *)message duration:(NSTimeInterval)duration position:(id)position title:(NSString *)title {
  63. UIView *toast = [self viewForMessage:message title:title image:nil];
  64. [self showToast:toast duration:duration position:position];
  65. }
  66. - (void)makeToast:(NSString *)message duration:(NSTimeInterval)duration position:(id)position image:(UIImage *)image {
  67. UIView *toast = [self viewForMessage:message title:nil image:image];
  68. [self showToast:toast duration:duration position:position];
  69. }
  70. - (void)makeToast:(NSString *)message duration:(NSTimeInterval)duration position:(id)position title:(NSString *)title image:(UIImage *)image {
  71. UIView *toast = [self viewForMessage:message title:title image:image];
  72. [self showToast:toast duration:duration position:position];
  73. }
  74. - (void)showToast:(UIView *)toast {
  75. [self showToast:toast duration:CSToastDefaultDuration position:CSToastDefaultPosition];
  76. }
  77. - (void)showToast:(UIView *)toast duration:(NSTimeInterval)duration position:(id)point {
  78. [self showToast:toast duration:CSToastDefaultDuration position:CSToastDefaultPosition addedPixelsY:0];
  79. }
  80. - (void)showToast:(UIView *)toast duration:(NSTimeInterval)duration position:(id)point addedPixelsY:(int) addPixelsY {
  81. [self hideToast];
  82. prevToast = toast;
  83. toast.center = [self centerPointForPosition:point withToast:toast withAddedPixelsY:addPixelsY];
  84. toast.alpha = 0.0;
  85. if (CSToastHidesOnTap) {
  86. UITapGestureRecognizer *recognizer = [[UITapGestureRecognizer alloc] initWithTarget:toast action:@selector(handleToastTapped:)];
  87. [toast addGestureRecognizer:recognizer];
  88. toast.userInteractionEnabled = YES;
  89. toast.exclusiveTouch = YES;
  90. }
  91. // make sure that if InAppBrowser is active, we're still showing Toasts on top of it
  92. UIViewController *vc = [self getTopMostViewController];
  93. UIView *v = [vc view];
  94. [v addSubview:toast];
  95. [UIView animateWithDuration:CSToastFadeDuration
  96. delay:0.0
  97. options:(UIViewAnimationOptionCurveEaseOut | UIViewAnimationOptionAllowUserInteraction)
  98. animations:^{
  99. toast.alpha = CSToastOpacity;
  100. } completion:^(BOOL finished) {
  101. NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:duration target:self selector:@selector(toastTimerDidFinish:) userInfo:toast repeats:NO];
  102. // associate the timer with the toast view
  103. objc_setAssociatedObject (toast, &CSToastTimerKey, timer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
  104. }];
  105. }
  106. - (UIViewController*) getTopMostViewController {
  107. UIViewController *presentingViewController = [[[UIApplication sharedApplication] delegate] window].rootViewController;
  108. while (presentingViewController.presentedViewController != nil) {
  109. presentingViewController = presentingViewController.presentedViewController;
  110. }
  111. return presentingViewController;
  112. }
  113. - (void)hideToast {
  114. if (prevToast){
  115. [self hideToast:prevToast];
  116. }
  117. }
  118. - (void)hideToast:(UIView *)toast {
  119. [UIView animateWithDuration:CSToastFadeDuration
  120. delay:0.0
  121. options:(UIViewAnimationOptionCurveEaseIn | UIViewAnimationOptionBeginFromCurrentState)
  122. animations:^{
  123. toast.alpha = 0.0;
  124. } completion:^(BOOL finished) {
  125. [toast removeFromSuperview];
  126. }];
  127. }
  128. #pragma mark - Events
  129. - (void)toastTimerDidFinish:(NSTimer *)timer {
  130. [self hideToast:(UIView *)timer.userInfo];
  131. }
  132. - (void)handleToastTapped:(UITapGestureRecognizer *)recognizer {
  133. NSTimer *timer = (NSTimer *)objc_getAssociatedObject(self, &CSToastTimerKey);
  134. [timer invalidate];
  135. [self hideToast:recognizer.view];
  136. }
  137. #pragma mark - Toast Activity Methods
  138. - (void)makeToastActivity {
  139. [self makeToastActivity:CSToastActivityDefaultPosition];
  140. }
  141. - (void)makeToastActivity:(id)position {
  142. // sanity
  143. UIView *existingActivityView = (UIView *)objc_getAssociatedObject(self, &CSToastActivityViewKey);
  144. if (existingActivityView != nil) return;
  145. UIView *activityView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, CSToastActivityWidth, CSToastActivityHeight)];
  146. activityView.center = [self centerPointForPosition:position withToast:activityView withAddedPixelsY:0];
  147. activityView.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:CSToastOpacity];
  148. activityView.alpha = 0.0;
  149. activityView.autoresizingMask = (UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleBottomMargin);
  150. activityView.layer.cornerRadius = CSToastCornerRadius;
  151. if (CSToastDisplayShadow) {
  152. activityView.layer.shadowColor = [UIColor blackColor].CGColor;
  153. activityView.layer.shadowOpacity = CSToastShadowOpacity;
  154. activityView.layer.shadowRadius = CSToastShadowRadius;
  155. activityView.layer.shadowOffset = CSToastShadowOffset;
  156. }
  157. UIActivityIndicatorView *activityIndicatorView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
  158. activityIndicatorView.center = CGPointMake(activityView.bounds.size.width / 2, activityView.bounds.size.height / 2);
  159. [activityView addSubview:activityIndicatorView];
  160. [activityIndicatorView startAnimating];
  161. // associate the activity view with self
  162. objc_setAssociatedObject (self, &CSToastActivityViewKey, activityView, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
  163. [self addSubview:activityView];
  164. [UIView animateWithDuration:CSToastFadeDuration
  165. delay:0.0
  166. options:UIViewAnimationOptionCurveEaseOut
  167. animations:^{
  168. activityView.alpha = 1.0;
  169. } completion:nil];
  170. }
  171. - (void)hideToastActivity {
  172. UIView *existingActivityView = (UIView *)objc_getAssociatedObject(self, &CSToastActivityViewKey);
  173. if (existingActivityView != nil) {
  174. [UIView animateWithDuration:CSToastFadeDuration
  175. delay:0.0
  176. options:(UIViewAnimationOptionCurveEaseIn | UIViewAnimationOptionBeginFromCurrentState)
  177. animations:^{
  178. existingActivityView.alpha = 0.0;
  179. } completion:^(BOOL finished) {
  180. [existingActivityView removeFromSuperview];
  181. objc_setAssociatedObject (self, &CSToastActivityViewKey, nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
  182. }];
  183. }
  184. }
  185. #pragma mark - Helpers
  186. - (CGPoint)centerPointForPosition:(id)point withToast:(UIView *)toast withAddedPixelsY:(int) addPixelsY {
  187. if([point isKindOfClass:[NSString class]]) {
  188. // convert string literals @"top", @"bottom", @"center", or any point wrapped in an NSValue object into a CGPoint
  189. if([point caseInsensitiveCompare:@"top"] == NSOrderedSame) {
  190. return CGPointMake(self.bounds.size.width/2, (toast.frame.size.height / 2) + addPixelsY + CSToastVerticalPadding + CSToastTopBottomOffset);
  191. } else if([point caseInsensitiveCompare:@"bottom"] == NSOrderedSame) {
  192. return CGPointMake(self.bounds.size.width/2, (self.bounds.size.height - (toast.frame.size.height / 2)) - CSToastVerticalPadding - CSToastTopBottomOffset + addPixelsY);
  193. } else if([point caseInsensitiveCompare:@"center"] == NSOrderedSame) {
  194. return CGPointMake(self.bounds.size.width / 2, (self.bounds.size.height / 2) + addPixelsY);
  195. }
  196. } else if ([point isKindOfClass:[NSValue class]]) {
  197. return [point CGPointValue];
  198. }
  199. NSLog(@"Warning: Invalid position for toast.");
  200. return [self centerPointForPosition:CSToastDefaultPosition withToast:toast withAddedPixelsY:addPixelsY];
  201. }
  202. - (CGSize)sizeForString:(NSString *)string font:(UIFont *)font constrainedToSize:(CGSize)constrainedSize lineBreakMode:(NSLineBreakMode)lineBreakMode {
  203. if ([string respondsToSelector:@selector(boundingRectWithSize:options:attributes:context:)]) {
  204. NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init];
  205. paragraphStyle.lineBreakMode = lineBreakMode;
  206. NSDictionary *attributes = @{NSFontAttributeName:font, NSParagraphStyleAttributeName:paragraphStyle};
  207. CGRect boundingRect = [string boundingRectWithSize:constrainedSize options:NSStringDrawingUsesLineFragmentOrigin attributes:attributes context:nil];
  208. return CGSizeMake(ceilf(boundingRect.size.width), ceilf(boundingRect.size.height));
  209. }
  210. #pragma clang diagnostic push
  211. #pragma clang diagnostic ignored "-Wdeprecated-declarations"
  212. return [string sizeWithFont:font constrainedToSize:constrainedSize lineBreakMode:lineBreakMode];
  213. #pragma clang diagnostic pop
  214. }
  215. - (UIView *)viewForMessage:(NSString *)message title:(NSString *)title image:(UIImage *)image {
  216. // sanity
  217. if((message == nil) && (title == nil) && (image == nil)) return nil;
  218. // dynamically build a toast view with any combination of message, title, & image.
  219. UILabel *messageLabel = nil;
  220. UILabel *titleLabel = nil;
  221. UIImageView *imageView = nil;
  222. // create the parent view
  223. UIView *wrapperView = [[UIView alloc] init];
  224. wrapperView.autoresizingMask = (UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleBottomMargin);
  225. wrapperView.layer.cornerRadius = CSToastCornerRadius;
  226. if (CSToastDisplayShadow) {
  227. wrapperView.layer.shadowColor = [UIColor blackColor].CGColor;
  228. wrapperView.layer.shadowOpacity = CSToastShadowOpacity;
  229. wrapperView.layer.shadowRadius = CSToastShadowRadius;
  230. wrapperView.layer.shadowOffset = CSToastShadowOffset;
  231. }
  232. wrapperView.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:CSToastOpacity];
  233. if(image != nil) {
  234. imageView = [[UIImageView alloc] initWithImage:image];
  235. imageView.contentMode = UIViewContentModeScaleAspectFit;
  236. imageView.frame = CGRectMake(CSToastHorizontalPadding, CSToastVerticalPadding, CSToastImageViewWidth, CSToastImageViewHeight);
  237. }
  238. CGFloat imageWidth, imageHeight, imageLeft;
  239. // the imageView frame values will be used to size & position the other views
  240. if(imageView != nil) {
  241. imageWidth = imageView.bounds.size.width;
  242. imageHeight = imageView.bounds.size.height;
  243. imageLeft = CSToastHorizontalPadding;
  244. } else {
  245. imageWidth = imageHeight = imageLeft = 0.0;
  246. }
  247. if (title != nil) {
  248. titleLabel = [[UILabel alloc] init];
  249. titleLabel.numberOfLines = CSToastMaxTitleLines;
  250. titleLabel.font = [UIFont boldSystemFontOfSize:CSToastFontSize];
  251. titleLabel.textAlignment = NSTextAlignmentLeft;
  252. titleLabel.lineBreakMode = NSLineBreakByWordWrapping;
  253. titleLabel.textColor = [UIColor whiteColor];
  254. titleLabel.backgroundColor = [UIColor clearColor];
  255. titleLabel.alpha = 1.0;
  256. titleLabel.text = title;
  257. // size the title label according to the length of the text
  258. CGSize maxSizeTitle = CGSizeMake((self.bounds.size.width * CSToastMaxWidth) - imageWidth, self.bounds.size.height * CSToastMaxHeight);
  259. CGSize expectedSizeTitle = [self sizeForString:title font:titleLabel.font constrainedToSize:maxSizeTitle lineBreakMode:titleLabel.lineBreakMode];
  260. titleLabel.frame = CGRectMake(0.0, 0.0, expectedSizeTitle.width, expectedSizeTitle.height);
  261. }
  262. if (message != nil) {
  263. messageLabel = [[UILabel alloc] init];
  264. messageLabel.numberOfLines = CSToastMaxMessageLines;
  265. messageLabel.font = [UIFont systemFontOfSize:CSToastFontSize];
  266. messageLabel.lineBreakMode = NSLineBreakByWordWrapping;
  267. messageLabel.textColor = [UIColor whiteColor];
  268. messageLabel.backgroundColor = [UIColor clearColor];
  269. messageLabel.alpha = 1.0;
  270. messageLabel.text = message;
  271. // size the message label according to the length of the text
  272. CGSize maxSizeMessage = CGSizeMake((self.bounds.size.width * CSToastMaxWidth) - imageWidth, self.bounds.size.height * CSToastMaxHeight);
  273. CGSize expectedSizeMessage = [self sizeForString:message font:messageLabel.font constrainedToSize:maxSizeMessage lineBreakMode:messageLabel.lineBreakMode];
  274. messageLabel.frame = CGRectMake(0.0, 0.0, expectedSizeMessage.width, expectedSizeMessage.height);
  275. }
  276. // titleLabel frame values
  277. CGFloat titleWidth, titleHeight, titleTop, titleLeft;
  278. if(titleLabel != nil) {
  279. titleWidth = titleLabel.bounds.size.width;
  280. titleHeight = titleLabel.bounds.size.height;
  281. titleTop = CSToastVerticalPadding;
  282. titleLeft = imageLeft + imageWidth + CSToastHorizontalPadding;
  283. } else {
  284. titleWidth = titleHeight = titleTop = titleLeft = 0.0;
  285. }
  286. // messageLabel frame values
  287. CGFloat messageWidth, messageHeight, messageLeft, messageTop;
  288. if(messageLabel != nil) {
  289. messageWidth = messageLabel.bounds.size.width;
  290. messageHeight = messageLabel.bounds.size.height;
  291. messageLeft = imageLeft + imageWidth + CSToastHorizontalPadding;
  292. messageTop = titleTop + titleHeight + CSToastVerticalPadding;
  293. } else {
  294. messageWidth = messageHeight = messageLeft = messageTop = 0.0;
  295. }
  296. CGFloat longerWidth = MAX(titleWidth, messageWidth);
  297. CGFloat longerLeft = MAX(titleLeft, messageLeft);
  298. // wrapper width uses the longerWidth or the image width, whatever is larger. same logic applies to the wrapper height
  299. CGFloat wrapperWidth = MAX((imageWidth + (CSToastHorizontalPadding * 2)), (longerLeft + longerWidth + CSToastHorizontalPadding));
  300. CGFloat wrapperHeight = MAX((messageTop + messageHeight + CSToastVerticalPadding), (imageHeight + (CSToastVerticalPadding * 2)));
  301. wrapperView.frame = CGRectMake(0.0, 0.0, wrapperWidth, wrapperHeight);
  302. if(titleLabel != nil) {
  303. titleLabel.frame = CGRectMake(titleLeft, titleTop, titleWidth, titleHeight);
  304. [wrapperView addSubview:titleLabel];
  305. }
  306. if(messageLabel != nil) {
  307. messageLabel.frame = CGRectMake(messageLeft, messageTop, messageWidth, messageHeight);
  308. [wrapperView addSubview:messageLabel];
  309. }
  310. if(imageView != nil) {
  311. [wrapperView addSubview:imageView];
  312. }
  313. return wrapperView;
  314. }
  315. @end