UIView+Toast.m 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393
  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. if (toast) {
  107. objc_setAssociatedObject (toast, &CSToastTimerKey, timer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
  108. objc_setAssociatedObject (toast, &CSToastTapCallbackKey, tapCallback, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
  109. }
  110. }];
  111. }
  112. - (void)hideToast:(UIView *)toast {
  113. [UIView animateWithDuration:CSToastFadeDuration
  114. delay:0.0
  115. options:(UIViewAnimationOptionCurveEaseIn | UIViewAnimationOptionBeginFromCurrentState)
  116. animations:^{
  117. toast.alpha = 0.0;
  118. } completion:^(BOOL finished) {
  119. [toast removeFromSuperview];
  120. CSToastIsShowing = NO;
  121. }];
  122. }
  123. #pragma mark - Events
  124. - (void)toastTimerDidFinish:(NSTimer *)timer {
  125. [self hideToast:(UIView *)timer.userInfo];
  126. }
  127. - (void)handleToastTapped:(UITapGestureRecognizer *)recognizer {
  128. NSTimer *timer = (NSTimer *)objc_getAssociatedObject(self, &CSToastTimerKey);
  129. [timer invalidate];
  130. void (^callback)(void) = objc_getAssociatedObject(self, &CSToastTapCallbackKey);
  131. if (callback) {
  132. callback();
  133. }
  134. [self hideToast:recognizer.view];
  135. }
  136. #pragma mark - Toast Activity Methods
  137. - (void)makeToastActivity {
  138. [self makeToastActivity:CSToastActivityDefaultPosition];
  139. }
  140. - (void)makeToastActivity:(id)position {
  141. // sanity
  142. UIView *existingActivityView = (UIView *)objc_getAssociatedObject(self, &CSToastActivityViewKey);
  143. if (existingActivityView != nil) return;
  144. UIView *activityView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, CSToastActivityWidth, CSToastActivityHeight)];
  145. activityView.center = [self centerPointForPosition:position withToast:activityView];
  146. activityView.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:CSToastOpacity];
  147. activityView.alpha = 0.0;
  148. activityView.autoresizingMask = (UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleBottomMargin);
  149. activityView.layer.cornerRadius = CSToastCornerRadius;
  150. if (CSToastDisplayShadow) {
  151. activityView.layer.shadowColor = [UIColor blackColor].CGColor;
  152. activityView.layer.shadowOpacity = CSToastShadowOpacity;
  153. activityView.layer.shadowRadius = CSToastShadowRadius;
  154. activityView.layer.shadowOffset = CSToastShadowOffset;
  155. }
  156. UIActivityIndicatorView *activityIndicatorView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
  157. activityIndicatorView.center = CGPointMake(activityView.bounds.size.width / 2, activityView.bounds.size.height / 2);
  158. [activityView addSubview:activityIndicatorView];
  159. [activityIndicatorView startAnimating];
  160. // associate the activity view with self
  161. objc_setAssociatedObject (self, &CSToastActivityViewKey, activityView, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
  162. [self addSubview:activityView];
  163. [UIView animateWithDuration:CSToastFadeDuration
  164. delay:0.0
  165. options:UIViewAnimationOptionCurveEaseOut
  166. animations:^{
  167. activityView.alpha = 1.0;
  168. } completion:nil];
  169. }
  170. - (void)hideToastActivity {
  171. UIView *existingActivityView = (UIView *)objc_getAssociatedObject(self, &CSToastActivityViewKey);
  172. if (existingActivityView != nil) {
  173. [UIView animateWithDuration:CSToastFadeDuration
  174. delay:0.0
  175. options:(UIViewAnimationOptionCurveEaseIn | UIViewAnimationOptionBeginFromCurrentState)
  176. animations:^{
  177. existingActivityView.alpha = 0.0;
  178. } completion:^(BOOL finished) {
  179. [existingActivityView removeFromSuperview];
  180. objc_setAssociatedObject (self, &CSToastActivityViewKey, nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
  181. }];
  182. }
  183. }
  184. #pragma mark - Helpers
  185. - (CGPoint)centerPointForPosition:(id)point withToast:(UIView *)toast {
  186. if([point isKindOfClass:[NSString class]]) {
  187. // convert string literals @"top", @"bottom", @"center", or any point wrapped in an NSValue object into a CGPoint
  188. if([point caseInsensitiveCompare:@"top"] == NSOrderedSame) {
  189. return CGPointMake(self.bounds.size.width/2, (toast.frame.size.height / 2) + CSToastVerticalPadding);
  190. } else if([point caseInsensitiveCompare:@"bottom"] == NSOrderedSame) {
  191. return CGPointMake(self.bounds.size.width/2, (self.bounds.size.height - (toast.frame.size.height / 2)) - CSToastVerticalPadding);
  192. } else if([point caseInsensitiveCompare:@"center"] == NSOrderedSame) {
  193. return CGPointMake(self.bounds.size.width / 2, self.bounds.size.height / 2);
  194. }
  195. } else if ([point isKindOfClass:[NSValue class]]) {
  196. return [point CGPointValue];
  197. }
  198. NSLog(@"Warning: Invalid position for toast.");
  199. return [self centerPointForPosition:CSToastDefaultPosition withToast:toast];
  200. }
  201. - (CGSize)sizeForString:(NSString *)string font:(UIFont *)font constrainedToSize:(CGSize)constrainedSize lineBreakMode:(NSLineBreakMode)lineBreakMode {
  202. if ([string respondsToSelector:@selector(boundingRectWithSize:options:attributes:context:)]) {
  203. NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init];
  204. paragraphStyle.lineBreakMode = lineBreakMode;
  205. NSDictionary *attributes = @{NSFontAttributeName:font, NSParagraphStyleAttributeName:paragraphStyle};
  206. CGRect boundingRect = [string boundingRectWithSize:constrainedSize options:NSStringDrawingUsesLineFragmentOrigin attributes:attributes context:nil];
  207. return CGSizeMake(ceilf(boundingRect.size.width), ceilf(boundingRect.size.height));
  208. }
  209. #pragma clang diagnostic push
  210. #pragma clang diagnostic ignored "-Wdeprecated-declarations"
  211. return [string sizeWithFont:font constrainedToSize:constrainedSize lineBreakMode:lineBreakMode];
  212. #pragma clang diagnostic pop
  213. }
  214. - (UIView *)viewForMessage:(NSString *)message title:(NSString *)title image:(UIImage *)image {
  215. // sanity
  216. if((message == nil) && (title == nil) && (image == nil)) return nil;
  217. // dynamically build a toast view with any combination of message, title, & image.
  218. UILabel *messageLabel = nil;
  219. UILabel *titleLabel = nil;
  220. UIImageView *imageView = nil;
  221. // create the parent view
  222. UIView *wrapperView = [[UIView alloc] init];
  223. wrapperView.autoresizingMask = (UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleBottomMargin);
  224. wrapperView.layer.cornerRadius = CSToastCornerRadius;
  225. if (CSToastDisplayShadow) {
  226. wrapperView.layer.shadowColor = [UIColor blackColor].CGColor;
  227. wrapperView.layer.shadowOpacity = CSToastShadowOpacity;
  228. wrapperView.layer.shadowRadius = CSToastShadowRadius;
  229. wrapperView.layer.shadowOffset = CSToastShadowOffset;
  230. }
  231. wrapperView.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:CSToastOpacity];
  232. if(image != nil) {
  233. imageView = [[UIImageView alloc] initWithImage:image];
  234. imageView.contentMode = UIViewContentModeScaleAspectFit;
  235. imageView.frame = CGRectMake(CSToastHorizontalPadding, CSToastVerticalPadding, CSToastImageViewWidth, CSToastImageViewHeight);
  236. }
  237. CGFloat imageWidth, imageHeight, imageLeft;
  238. // the imageView frame values will be used to size & position the other views
  239. if(imageView != nil) {
  240. imageWidth = imageView.bounds.size.width;
  241. imageHeight = imageView.bounds.size.height;
  242. imageLeft = CSToastHorizontalPadding;
  243. } else {
  244. imageWidth = imageHeight = imageLeft = 0.0;
  245. }
  246. if (title != nil) {
  247. titleLabel = [[UILabel alloc] init];
  248. titleLabel.numberOfLines = CSToastMaxTitleLines;
  249. titleLabel.font = [UIFont boldSystemFontOfSize:CSToastFontSize];
  250. titleLabel.textAlignment = NSTextAlignmentLeft;
  251. titleLabel.lineBreakMode = NSLineBreakByWordWrapping;
  252. titleLabel.textColor = [UIColor whiteColor];
  253. titleLabel.backgroundColor = [UIColor clearColor];
  254. titleLabel.alpha = 1.0;
  255. titleLabel.text = title;
  256. // size the title label according to the length of the text
  257. CGSize maxSizeTitle = CGSizeMake((self.bounds.size.width * CSToastMaxWidth) - imageWidth, self.bounds.size.height * CSToastMaxHeight);
  258. CGSize expectedSizeTitle = [self sizeForString:title font:titleLabel.font constrainedToSize:maxSizeTitle lineBreakMode:titleLabel.lineBreakMode];
  259. titleLabel.frame = CGRectMake(0.0, 0.0, expectedSizeTitle.width, expectedSizeTitle.height);
  260. }
  261. if (message != nil) {
  262. messageLabel = [[UILabel alloc] init];
  263. messageLabel.numberOfLines = CSToastMaxMessageLines;
  264. messageLabel.font = [UIFont systemFontOfSize:CSToastFontSize];
  265. messageLabel.lineBreakMode = NSLineBreakByWordWrapping;
  266. messageLabel.textColor = [UIColor whiteColor];
  267. messageLabel.backgroundColor = [UIColor clearColor];
  268. messageLabel.alpha = 1.0;
  269. messageLabel.text = message;
  270. // size the message label according to the length of the text
  271. CGSize maxSizeMessage = CGSizeMake((self.bounds.size.width * CSToastMaxWidth) - imageWidth, self.bounds.size.height * CSToastMaxHeight);
  272. CGSize expectedSizeMessage = [self sizeForString:message font:messageLabel.font constrainedToSize:maxSizeMessage lineBreakMode:messageLabel.lineBreakMode];
  273. messageLabel.frame = CGRectMake(0.0, 0.0, expectedSizeMessage.width, expectedSizeMessage.height);
  274. }
  275. // titleLabel frame values
  276. CGFloat titleWidth, titleHeight, titleTop, titleLeft;
  277. if(titleLabel != nil) {
  278. titleWidth = titleLabel.bounds.size.width;
  279. titleHeight = titleLabel.bounds.size.height;
  280. titleTop = CSToastVerticalPadding;
  281. titleLeft = imageLeft + imageWidth + CSToastHorizontalPadding;
  282. } else {
  283. titleWidth = titleHeight = titleTop = titleLeft = 0.0;
  284. }
  285. // messageLabel frame values
  286. CGFloat messageWidth, messageHeight, messageLeft, messageTop;
  287. if(messageLabel != nil) {
  288. messageWidth = messageLabel.bounds.size.width;
  289. messageHeight = messageLabel.bounds.size.height;
  290. messageLeft = imageLeft + imageWidth + CSToastHorizontalPadding;
  291. messageTop = titleTop + titleHeight + CSToastVerticalPadding;
  292. } else {
  293. messageWidth = messageHeight = messageLeft = messageTop = 0.0;
  294. }
  295. CGFloat longerWidth = MAX(titleWidth, messageWidth);
  296. CGFloat longerLeft = MAX(titleLeft, messageLeft);
  297. // wrapper width uses the longerWidth or the image width, whatever is larger. same logic applies to the wrapper height
  298. CGFloat wrapperWidth = MAX((imageWidth + (CSToastHorizontalPadding * 2)), (longerLeft + longerWidth + CSToastHorizontalPadding));
  299. CGFloat wrapperHeight = MAX((messageTop + messageHeight + CSToastVerticalPadding), (imageHeight + (CSToastVerticalPadding * 2)));
  300. wrapperView.frame = CGRectMake(0.0, 0.0, wrapperWidth, wrapperHeight);
  301. if(titleLabel != nil) {
  302. titleLabel.frame = CGRectMake(titleLeft, titleTop, titleWidth, titleHeight);
  303. [wrapperView addSubview:titleLabel];
  304. }
  305. if(messageLabel != nil) {
  306. messageLabel.frame = CGRectMake(messageLeft, messageTop, messageWidth, messageHeight);
  307. [wrapperView addSubview:messageLabel];
  308. }
  309. if(imageView != nil) {
  310. [wrapperView addSubview:imageView];
  311. }
  312. return wrapperView;
  313. }
  314. @end