You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

520 lines
20 KiB

  1. //
  2. // TSMessageView.m
  3. // Felix Krause
  4. //
  5. // Created by Felix Krause on 24.08.12.
  6. // Copyright (c) 2012 Felix Krause. All rights reserved.
  7. //
  8. #import "TSMessageView.h"
  9. #import "HexColor.h"
  10. #import "TSBlurView.h"
  11. #import "TSMessage.h"
  12. #define TSMessageViewMinimumPadding 15.0
  13. #define TSDesignFileName @"TSMessagesDefaultDesign"
  14. static NSMutableDictionary *_notificationDesign;
  15. @interface TSMessage (TSMessageView)
  16. - (void)fadeOutNotification:(TSMessageView *)currentView; // private method of TSMessage, but called by TSMessageView in -[fadeMeOut]
  17. @end
  18. @interface TSMessageView () <UIGestureRecognizerDelegate>
  19. /** The displayed title of this message */
  20. @property (nonatomic, strong) NSString *title;
  21. /** The displayed subtitle of this message view */
  22. @property (nonatomic, strong) NSString *subtitle;
  23. /** The title of the added button */
  24. @property (nonatomic, strong) NSString *buttonTitle;
  25. /** The view controller this message is displayed in */
  26. @property (nonatomic, strong) UIViewController *viewController;
  27. /** Internal properties needed to resize the view on device rotation properly */
  28. @property (nonatomic, strong) UILabel *titleLabel;
  29. @property (nonatomic, strong) UILabel *contentLabel;
  30. @property (nonatomic, strong) UIImageView *iconImageView;
  31. @property (nonatomic, strong) UIButton *button;
  32. @property (nonatomic, strong) UIView *borderView;
  33. @property (nonatomic, strong) UIImageView *backgroundImageView;
  34. @property (nonatomic, strong) TSBlurView *backgroundBlurView; // Only used in iOS 7
  35. @property (nonatomic, assign) CGFloat textSpaceLeft;
  36. @property (nonatomic, assign) CGFloat textSpaceRight;
  37. @property (copy) void (^callback)();
  38. @property (copy) void (^buttonCallback)();
  39. - (CGFloat)updateHeightOfMessageView;
  40. - (void)layoutSubviews;
  41. @end
  42. @implementation TSMessageView
  43. + (NSMutableDictionary *)notificationDesign
  44. {
  45. if (!_notificationDesign)
  46. {
  47. NSString *path = [[NSBundle mainBundle] pathForResource:TSDesignFileName ofType:@"json"];
  48. NSData *data = [NSData dataWithContentsOfFile:path];
  49. NSAssert(data != nil, @"Could not read TSMessages config file from main bundle with name %@.json", TSDesignFileName);
  50. _notificationDesign = [NSMutableDictionary dictionaryWithDictionary:[NSJSONSerialization JSONObjectWithData:data
  51. options:kNilOptions
  52. error:nil]];
  53. }
  54. return _notificationDesign;
  55. }
  56. + (void)addNotificationDesignFromFile:(NSString *)filename
  57. {
  58. NSString *path = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:filename];
  59. if ([[NSFileManager defaultManager] fileExistsAtPath:path])
  60. {
  61. NSDictionary *design = [NSJSONSerialization JSONObjectWithData:[NSData dataWithContentsOfFile:path]
  62. options:kNilOptions
  63. error:nil];
  64. [[TSMessageView notificationDesign] addEntriesFromDictionary:design];
  65. }
  66. else
  67. {
  68. NSAssert(NO, @"Error loading design file with name %@", filename);
  69. }
  70. }
  71. - (CGFloat)padding
  72. {
  73. // Adds 10 padding to to cover navigation bar
  74. return self.messagePosition == TSMessageNotificationPositionNavBarOverlay ? TSMessageViewMinimumPadding + 10.0f : TSMessageViewMinimumPadding;
  75. }
  76. - (id)initWithTitle:(NSString *)title
  77. subtitle:(NSString *)subtitle
  78. image:(UIImage *)image
  79. type:(TSMessageNotificationType)notificationType
  80. duration:(CGFloat)duration
  81. inViewController:(UIViewController *)viewController
  82. callback:(void (^)())callback
  83. buttonTitle:(NSString *)buttonTitle
  84. buttonCallback:(void (^)())buttonCallback
  85. atPosition:(TSMessageNotificationPosition)position
  86. canBeDismissedByUser:(BOOL)dismissingEnabled
  87. {
  88. NSDictionary *notificationDesign = [TSMessageView notificationDesign];
  89. if ((self = [self init]))
  90. {
  91. _title = title;
  92. _subtitle = subtitle;
  93. _buttonTitle = buttonTitle;
  94. _duration = duration;
  95. _viewController = viewController;
  96. _messagePosition = position;
  97. self.callback = callback;
  98. self.buttonCallback = buttonCallback;
  99. CGFloat screenWidth = self.viewController.view.bounds.size.width;
  100. CGFloat padding = [self padding];
  101. NSDictionary *current;
  102. NSString *currentString;
  103. switch (notificationType)
  104. {
  105. case TSMessageNotificationTypeMessage:
  106. {
  107. currentString = @"message";
  108. break;
  109. }
  110. case TSMessageNotificationTypeError:
  111. {
  112. currentString = @"error";
  113. break;
  114. }
  115. case TSMessageNotificationTypeSuccess:
  116. {
  117. currentString = @"success";
  118. break;
  119. }
  120. case TSMessageNotificationTypeWarning:
  121. {
  122. currentString = @"warning";
  123. break;
  124. }
  125. default:
  126. break;
  127. }
  128. current = [notificationDesign valueForKey:currentString];
  129. if (!image && [[current valueForKey:@"imageName"] length])
  130. {
  131. image = [UIImage imageNamed:[current valueForKey:@"imageName"]];
  132. }
  133. if (![TSMessage iOS7StyleEnabled])
  134. {
  135. self.alpha = 0.0;
  136. // add background image here
  137. UIImage *backgroundImage = [UIImage imageNamed:[current valueForKey:@"backgroundImageName"]];
  138. backgroundImage = [backgroundImage stretchableImageWithLeftCapWidth:0.0 topCapHeight:0.0];
  139. _backgroundImageView = [[UIImageView alloc] initWithImage:backgroundImage];
  140. self.backgroundImageView.autoresizingMask = (UIViewAutoresizingFlexibleWidth);
  141. [self addSubview:self.backgroundImageView];
  142. }
  143. else
  144. {
  145. // On iOS 7 and above use a blur layer instead (not yet finished)
  146. _backgroundBlurView = [[TSBlurView alloc] init];
  147. self.backgroundBlurView.autoresizingMask = (UIViewAutoresizingFlexibleWidth);
  148. self.backgroundBlurView.blurTintColor = [UIColor colorWithHexString:current[@"backgroundColor"]];
  149. [self addSubview:self.backgroundBlurView];
  150. }
  151. UIColor *fontColor = [UIColor colorWithHexString:[current valueForKey:@"textColor"]
  152. alpha:1.0];
  153. self.textSpaceLeft = 2 * padding;
  154. if (image) self.textSpaceLeft += image.size.width + 2 * padding;
  155. // Set up title label
  156. _titleLabel = [[UILabel alloc] init];
  157. [self.titleLabel setText:title];
  158. [self.titleLabel setTextColor:fontColor];
  159. [self.titleLabel setBackgroundColor:[UIColor clearColor]];
  160. CGFloat fontSize = [[current valueForKey:@"titleFontSize"] floatValue];
  161. NSString *fontName = [current valueForKey:@"titleFontName"];
  162. if (fontName != nil) {
  163. [self.titleLabel setFont:[UIFont fontWithName:fontName size:fontSize]];
  164. } else {
  165. [self.titleLabel setFont:[UIFont boldSystemFontOfSize:fontSize]];
  166. }
  167. [self.titleLabel setShadowColor:[UIColor colorWithHexString:[current valueForKey:@"shadowColor"] alpha:1.0]];
  168. [self.titleLabel setShadowOffset:CGSizeMake([[current valueForKey:@"shadowOffsetX"] floatValue],
  169. [[current valueForKey:@"shadowOffsetY"] floatValue])];
  170. self.titleLabel.numberOfLines = 0;
  171. self.titleLabel.lineBreakMode = NSLineBreakByWordWrapping;
  172. [self addSubview:self.titleLabel];
  173. // Set up content label (if set)
  174. if ([subtitle length])
  175. {
  176. _contentLabel = [[UILabel alloc] init];
  177. [self.contentLabel setText:subtitle];
  178. UIColor *contentTextColor = [UIColor colorWithHexString:[current valueForKey:@"contentTextColor"] alpha:1.0];
  179. if (!contentTextColor)
  180. {
  181. contentTextColor = fontColor;
  182. }
  183. [self.contentLabel setTextColor:contentTextColor];
  184. [self.contentLabel setBackgroundColor:[UIColor clearColor]];
  185. CGFloat fontSize = [[current valueForKey:@"contentFontSize"] floatValue];
  186. NSString *fontName = [current valueForKey:@"contentFontName"];
  187. if (fontName != nil) {
  188. [self.contentLabel setFont:[UIFont fontWithName:fontName size:fontSize]];
  189. } else {
  190. [self.contentLabel setFont:[UIFont systemFontOfSize:fontSize]];
  191. }
  192. [self.contentLabel setShadowColor:self.titleLabel.shadowColor];
  193. [self.contentLabel setShadowOffset:self.titleLabel.shadowOffset];
  194. self.contentLabel.lineBreakMode = self.titleLabel.lineBreakMode;
  195. self.contentLabel.numberOfLines = 0;
  196. [self addSubview:self.contentLabel];
  197. }
  198. if (image)
  199. {
  200. _iconImageView = [[UIImageView alloc] initWithImage:image];
  201. self.iconImageView.frame = CGRectMake(padding * 2,
  202. padding,
  203. image.size.width,
  204. image.size.height);
  205. [self addSubview:self.iconImageView];
  206. }
  207. // Set up button (if set)
  208. if ([buttonTitle length])
  209. {
  210. _button = [UIButton buttonWithType:UIButtonTypeCustom];
  211. UIImage *buttonBackgroundImage = [UIImage imageNamed:[current valueForKey:@"buttonBackgroundImageName"]];
  212. buttonBackgroundImage = [buttonBackgroundImage resizableImageWithCapInsets:UIEdgeInsetsMake(15.0, 12.0, 15.0, 11.0)];
  213. if (!buttonBackgroundImage)
  214. {
  215. buttonBackgroundImage = [UIImage imageNamed:[current valueForKey:@"NotificationButtonBackground"]];
  216. buttonBackgroundImage = [buttonBackgroundImage resizableImageWithCapInsets:UIEdgeInsetsMake(15.0, 12.0, 15.0, 11.0)];
  217. }
  218. [self.button setBackgroundImage:buttonBackgroundImage forState:UIControlStateNormal];
  219. [self.button setTitle:self.buttonTitle forState:UIControlStateNormal];
  220. UIColor *buttonTitleShadowColor = [UIColor colorWithHexString:[current valueForKey:@"buttonTitleShadowColor"] alpha:1.0];
  221. if (!buttonTitleShadowColor)
  222. {
  223. buttonTitleShadowColor = self.titleLabel.shadowColor;
  224. }
  225. [self.button setTitleShadowColor:buttonTitleShadowColor forState:UIControlStateNormal];
  226. UIColor *buttonTitleTextColor = [UIColor colorWithHexString:[current valueForKey:@"buttonTitleTextColor"] alpha:1.0];
  227. if (!buttonTitleTextColor)
  228. {
  229. buttonTitleTextColor = fontColor;
  230. }
  231. [self.button setTitleColor:buttonTitleTextColor forState:UIControlStateNormal];
  232. self.button.titleLabel.font = [UIFont boldSystemFontOfSize:14.0];
  233. self.button.titleLabel.shadowOffset = CGSizeMake([[current valueForKey:@"buttonTitleShadowOffsetX"] floatValue],
  234. [[current valueForKey:@"buttonTitleShadowOffsetY"] floatValue]);
  235. [self.button addTarget:self
  236. action:@selector(buttonTapped:)
  237. forControlEvents:UIControlEventTouchUpInside];
  238. self.button.contentEdgeInsets = UIEdgeInsetsMake(0.0, 5.0, 0.0, 5.0);
  239. [self.button sizeToFit];
  240. self.button.frame = CGRectMake(screenWidth - padding - self.button.frame.size.width,
  241. 0.0,
  242. self.button.frame.size.width,
  243. 31.0);
  244. [self addSubview:self.button];
  245. self.textSpaceRight = self.button.frame.size.width + padding;
  246. }
  247. // Add a border on the bottom (or on the top, depending on the view's postion)
  248. if (![TSMessage iOS7StyleEnabled])
  249. {
  250. _borderView = [[UIView alloc] initWithFrame:CGRectMake(0.0,
  251. 0.0, // will be set later
  252. screenWidth,
  253. [[current valueForKey:@"borderHeight"] floatValue])];
  254. self.borderView.backgroundColor = [UIColor colorWithHexString:[current valueForKey:@"borderColor"]
  255. alpha:1.0];
  256. self.borderView.autoresizingMask = (UIViewAutoresizingFlexibleWidth);
  257. [self addSubview:self.borderView];
  258. }
  259. CGFloat actualHeight = [self updateHeightOfMessageView]; // this call also takes care of positioning the labels
  260. CGFloat topPosition = -actualHeight;
  261. if (self.messagePosition == TSMessageNotificationPositionBottom)
  262. {
  263. topPosition = self.viewController.view.bounds.size.height;
  264. }
  265. self.frame = CGRectMake(0.0, topPosition, screenWidth, actualHeight);
  266. if (self.messagePosition == TSMessageNotificationPositionTop)
  267. {
  268. self.autoresizingMask = UIViewAutoresizingFlexibleWidth;
  269. }
  270. else
  271. {
  272. self.autoresizingMask = (UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleBottomMargin);
  273. }
  274. if (dismissingEnabled)
  275. {
  276. UISwipeGestureRecognizer *gestureRec = [[UISwipeGestureRecognizer alloc] initWithTarget:self
  277. action:@selector(fadeMeOut)];
  278. [gestureRec setDirection:(self.messagePosition == TSMessageNotificationPositionTop ?
  279. UISwipeGestureRecognizerDirectionUp :
  280. UISwipeGestureRecognizerDirectionDown)];
  281. [self addGestureRecognizer:gestureRec];
  282. UITapGestureRecognizer *tapRec = [[UITapGestureRecognizer alloc] initWithTarget:self
  283. action:@selector(fadeMeOut)];
  284. [self addGestureRecognizer:tapRec];
  285. }
  286. if (self.callback) {
  287. UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTap:)];
  288. tapGesture.delegate = self;
  289. [self addGestureRecognizer:tapGesture];
  290. }
  291. }
  292. return self;
  293. }
  294. - (CGFloat)updateHeightOfMessageView
  295. {
  296. CGFloat currentHeight;
  297. CGFloat screenWidth = self.viewController.view.bounds.size.width;
  298. CGFloat padding = [self padding];
  299. self.titleLabel.frame = CGRectMake(self.textSpaceLeft,
  300. padding,
  301. screenWidth - padding - self.textSpaceLeft - self.textSpaceRight,
  302. 0.0);
  303. [self.titleLabel sizeToFit];
  304. if ([self.subtitle length])
  305. {
  306. self.contentLabel.frame = CGRectMake(self.textSpaceLeft,
  307. self.titleLabel.frame.origin.y + self.titleLabel.frame.size.height + 5.0,
  308. screenWidth - padding - self.textSpaceLeft - self.textSpaceRight,
  309. 0.0);
  310. [self.contentLabel sizeToFit];
  311. currentHeight = self.contentLabel.frame.origin.y + self.contentLabel.frame.size.height;
  312. }
  313. else
  314. {
  315. // only the title was set
  316. currentHeight = self.titleLabel.frame.origin.y + self.titleLabel.frame.size.height;
  317. }
  318. currentHeight += padding;
  319. if (self.iconImageView)
  320. {
  321. // Check if that makes the popup larger (height)
  322. if (self.iconImageView.frame.origin.y + self.iconImageView.frame.size.height + padding > currentHeight)
  323. {
  324. currentHeight = self.iconImageView.frame.origin.y + self.iconImageView.frame.size.height + padding;
  325. }
  326. else
  327. {
  328. // z-align
  329. self.iconImageView.center = CGPointMake([self.iconImageView center].x,
  330. round(currentHeight / 2.0));
  331. }
  332. }
  333. // z-align button
  334. self.button.center = CGPointMake([self.button center].x,
  335. round(currentHeight / 2.0));
  336. if (self.messagePosition == TSMessageNotificationPositionTop)
  337. {
  338. // Correct the border position
  339. CGRect borderFrame = self.borderView.frame;
  340. borderFrame.origin.y = currentHeight;
  341. self.borderView.frame = borderFrame;
  342. }
  343. currentHeight += self.borderView.frame.size.height;
  344. self.frame = CGRectMake(0.0, self.frame.origin.y, self.frame.size.width, currentHeight);
  345. if (self.button)
  346. {
  347. self.button.frame = CGRectMake(self.frame.size.width - self.textSpaceRight,
  348. round((self.frame.size.height / 2.0) - self.button.frame.size.height / 2.0),
  349. self.button.frame.size.width,
  350. self.button.frame.size.height);
  351. }
  352. CGRect backgroundFrame = CGRectMake(self.backgroundImageView.frame.origin.x,
  353. self.backgroundImageView.frame.origin.y,
  354. screenWidth,
  355. currentHeight);
  356. // increase frame of background view because of the spring animation
  357. if ([TSMessage iOS7StyleEnabled])
  358. {
  359. if (self.messagePosition == TSMessageNotificationPositionTop)
  360. {
  361. float topOffset = 0.f;
  362. UINavigationController *navigationController = self.viewController.navigationController;
  363. if (!navigationController && [self.viewController isKindOfClass:[UINavigationController class]]) {
  364. navigationController = (UINavigationController *)self.viewController;
  365. }
  366. BOOL isNavBarIsHidden = !navigationController || [TSMessage isNavigationBarInNavigationControllerHidden:navigationController];
  367. BOOL isNavBarIsOpaque = !navigationController.navigationBar.isTranslucent && navigationController.navigationBar.alpha == 1;
  368. if (isNavBarIsHidden || isNavBarIsOpaque) {
  369. topOffset = -30.f;
  370. }
  371. backgroundFrame = UIEdgeInsetsInsetRect(backgroundFrame, UIEdgeInsetsMake(topOffset, 0.f, 0.f, 0.f));
  372. }
  373. else if (self.messagePosition == TSMessageNotificationPositionBottom)
  374. {
  375. backgroundFrame = UIEdgeInsetsInsetRect(backgroundFrame, UIEdgeInsetsMake(0.f, 0.f, -30.f, 0.f));
  376. }
  377. }
  378. self.backgroundImageView.frame = backgroundFrame;
  379. self.backgroundBlurView.frame = backgroundFrame;
  380. return currentHeight;
  381. }
  382. - (void)layoutSubviews
  383. {
  384. [super layoutSubviews];
  385. [self updateHeightOfMessageView];
  386. }
  387. - (void)fadeMeOut
  388. {
  389. [[TSMessage sharedMessage] performSelectorOnMainThread:@selector(fadeOutNotification:) withObject:self waitUntilDone:NO];
  390. }
  391. - (void)didMoveToWindow
  392. {
  393. [super didMoveToWindow];
  394. if (self.duration == TSMessageNotificationDurationEndless && self.superview && !self.window )
  395. {
  396. // view controller was dismissed, let's fade out
  397. [self fadeMeOut];
  398. }
  399. }
  400. #pragma mark - Target/Action
  401. - (void)buttonTapped:(id) sender
  402. {
  403. if (self.buttonCallback)
  404. {
  405. self.buttonCallback();
  406. }
  407. [self fadeMeOut];
  408. }
  409. - (void)handleTap:(UITapGestureRecognizer *)tapGesture
  410. {
  411. if (tapGesture.state == UIGestureRecognizerStateRecognized)
  412. {
  413. if (self.callback)
  414. {
  415. self.callback();
  416. }
  417. }
  418. }
  419. #pragma mark - UIGestureRecognizerDelegate
  420. - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
  421. {
  422. return ! ([touch.view isKindOfClass:[UIControl class]]);
  423. }
  424. @end