UIView+Toast.m 17 KB

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