FIRIAMModalViewController.m 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491
  1. /*
  2. * Copyright 2018 Google
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. #import <TargetConditionals.h>
  17. #if TARGET_OS_IOS
  18. #import <UIKit/UIKit.h>
  19. #import "FirebaseInAppMessaging/Sources/DefaultUI/FIRCore+InAppMessagingDisplay.h"
  20. #import "FirebaseInAppMessaging/Sources/DefaultUI/Modal/FIRIAMModalViewController.h"
  21. @interface FIRIAMModalViewController ()
  22. @property(nonatomic, readwrite) FIRInAppMessagingModalDisplay *modalDisplayMessage;
  23. @property(weak, nonatomic) IBOutlet UIImageView *imageView;
  24. @property(weak, nonatomic) IBOutlet UILabel *titleLabel;
  25. @property(weak, nonatomic) IBOutlet UIButton *actionButton;
  26. @property(weak, nonatomic) IBOutlet UIView *messageCardView;
  27. @property(weak, nonatomic) IBOutlet UITextView *bodyTextView;
  28. @property(weak, nonatomic) IBOutlet UIButton *closeButton;
  29. // this is only needed for removing the layout errors in interface builder. At runtime
  30. // we determine the height via its content size. So disable this at runtime.
  31. @property(weak, nonatomic) IBOutlet NSLayoutConstraint *fixedMessageCardHeightConstraint;
  32. @property(weak, nonatomic) IBOutlet NSLayoutConstraint *messageCardHeightMaxInTabletCase;
  33. @property(weak, nonatomic) IBOutlet NSLayoutConstraint *maxActionButtonHeight;
  34. @property(weak, nonatomic) IBOutlet NSLayoutConstraint *bodyTextViewHeightConstraint;
  35. @property(weak, nonatomic) IBOutlet NSLayoutConstraint *buttonTopToBodyBottomConstraint;
  36. @property(weak, nonatomic) IBOutlet NSLayoutConstraint *imageActualHeightConstraint;
  37. // constraints manipulated further in portrait mode
  38. @property(weak, nonatomic) IBOutlet NSLayoutConstraint *titleLabelHeightConstraint;
  39. @property(weak, nonatomic) IBOutlet NSLayoutConstraint *buttonBottomToContainerBottomInPortraitMode;
  40. @property(weak, nonatomic) IBOutlet NSLayoutConstraint *imageTopToTitleBottomInPortraitMode;
  41. // constraints manipulated further in landscape mode
  42. @property(weak, nonatomic) IBOutlet NSLayoutConstraint *imageWidthInLandscapeMode;
  43. @property(weak, nonatomic) IBOutlet NSLayoutConstraint *titleTopToCardViewTop;
  44. @property(weak, nonatomic) IBOutlet NSLayoutConstraint *cardLeadingMarginInLandscapeMode;
  45. @property(weak, nonatomic) IBOutlet NSLayoutConstraint *maxCardHeightInLandscapeMode;
  46. @property(weak, nonatomic) IBOutlet NSLayoutConstraint *imageTopToCardTopInLandscapeMode;
  47. @property(weak, nonatomic) IBOutlet NSLayoutConstraint *bodyTopToTitleBottomInLandScapeMode;
  48. @end
  49. static CGFloat VerticalSpacingBetweenTitleAndBody = 24;
  50. static CGFloat VerticalSpacingBetweenBodyAndActionButton = 24;
  51. // the padding between the content and view card's top and bottom edges
  52. static CGFloat TopBottomPaddingAroundContent = 24;
  53. // the minimal padding size between msg card and app window's top and bottom
  54. static CGFloat TopBottomPaddingAroundMsgCard = 30;
  55. // the horizontal spacing between image column and text/button column in landscape mode
  56. static CGFloat LandScapePaddingBetweenImageAndTextColumn = 24;
  57. @implementation FIRIAMModalViewController
  58. + (FIRIAMModalViewController *)
  59. instantiateViewControllerWithResourceBundle:(NSBundle *)resourceBundle
  60. displayMessage:(FIRInAppMessagingModalDisplay *)modalMessage
  61. displayDelegate:
  62. (id<FIRInAppMessagingDisplayDelegate>)displayDelegate
  63. timeFetcher:(id<FIRIAMTimeFetcher>)timeFetcher {
  64. UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"FIRInAppMessageDisplayStoryboard"
  65. bundle:resourceBundle];
  66. if (storyboard == nil) {
  67. FIRLogError(kFIRLoggerInAppMessagingDisplay, @"I-FID300001",
  68. @"Storyboard '"
  69. "FIRInAppMessageDisplayStoryboard' not found in bundle %@",
  70. resourceBundle);
  71. return nil;
  72. }
  73. FIRIAMModalViewController *modalVC = (FIRIAMModalViewController *)[storyboard
  74. instantiateViewControllerWithIdentifier:@"modal-view-vc"];
  75. modalVC.displayDelegate = displayDelegate;
  76. modalVC.modalDisplayMessage = modalMessage;
  77. modalVC.timeFetcher = timeFetcher;
  78. return modalVC;
  79. }
  80. - (FIRInAppMessagingDisplayMessage *)inAppMessage {
  81. return self.modalDisplayMessage;
  82. }
  83. - (IBAction)closeButtonClicked:(id)sender {
  84. [self dismissView:FIRInAppMessagingDismissTypeUserTapClose];
  85. }
  86. - (IBAction)actionButtonTapped:(id)sender {
  87. #pragma clang diagnostic push
  88. #pragma clang diagnostic ignored "-Wdeprecated-declarations"
  89. FIRInAppMessagingAction *action = [[FIRInAppMessagingAction alloc]
  90. initWithActionText:self.modalDisplayMessage.actionButton.buttonText
  91. actionURL:self.modalDisplayMessage.actionURL];
  92. #pragma clang diagnostic pop
  93. [self followAction:action];
  94. }
  95. - (void)viewDidLoad {
  96. [super viewDidLoad];
  97. // make the background half transparent
  98. [self.view setBackgroundColor:[UIColor.grayColor colorWithAlphaComponent:0.5]];
  99. self.messageCardView.layer.cornerRadius = 4;
  100. // populating values for display elements
  101. self.titleLabel.text = self.modalDisplayMessage.title;
  102. self.bodyTextView.text = self.modalDisplayMessage.bodyText;
  103. if (self.modalDisplayMessage.imageData) {
  104. [self.imageView
  105. setImage:[UIImage imageWithData:self.modalDisplayMessage.imageData.imageRawData]];
  106. self.imageView.contentMode = UIViewContentModeScaleAspectFit;
  107. self.imageView.accessibilityLabel = self.inAppMessage.campaignInfo.campaignName;
  108. } else {
  109. self.imageView.isAccessibilityElement = NO;
  110. }
  111. self.messageCardView.backgroundColor = self.modalDisplayMessage.displayBackgroundColor;
  112. self.titleLabel.textColor = self.modalDisplayMessage.textColor;
  113. self.bodyTextView.textColor = self.modalDisplayMessage.textColor;
  114. self.bodyTextView.selectable = NO;
  115. if (self.modalDisplayMessage.actionButton.buttonText.length != 0) {
  116. [self.actionButton setTitle:self.modalDisplayMessage.actionButton.buttonText
  117. forState:UIControlStateNormal];
  118. self.actionButton.backgroundColor = self.modalDisplayMessage.actionButton.buttonBackgroundColor;
  119. [self.actionButton setTitleColor:self.modalDisplayMessage.actionButton.buttonTextColor
  120. forState:UIControlStateNormal];
  121. self.actionButton.layer.cornerRadius = 4;
  122. if (self.modalDisplayMessage.bodyText.length == 0) {
  123. self.buttonTopToBodyBottomConstraint.constant = 0;
  124. }
  125. } else {
  126. // either action button text is empty or nil
  127. // hide the action button and reclaim the space below the buttom
  128. FIRLogDebug(kFIRLoggerInAppMessagingDisplay, @"I-FID300002",
  129. @"Modal view to be rendered without action button");
  130. self.maxActionButtonHeight.constant = 0;
  131. self.actionButton.clipsToBounds = YES;
  132. self.buttonTopToBodyBottomConstraint.constant = 0;
  133. }
  134. [self.view addConstraint:self.imageActualHeightConstraint];
  135. self.imageActualHeightConstraint.active = YES;
  136. self.fixedMessageCardHeightConstraint.active = NO;
  137. // Close button should be announced last for better VoiceOver experience.
  138. self.view.accessibilityElements = @[
  139. self.titleLabel, self.imageView, self.bodyTextView, self.actionButton, self.closeButton,
  140. self.messageCardView
  141. ];
  142. }
  143. // for text display UIview, which could be a UILabel or UITextView, decide the fit height under a
  144. // given display width
  145. - (CGFloat)determineTextAreaViewFitHeightForView:(UIView *)textView
  146. withWidth:(CGFloat)displayWidth {
  147. CGSize displaySize = CGSizeMake(displayWidth, FLT_MAX);
  148. return [textView sizeThatFits:displaySize].height;
  149. }
  150. // In both landscape or portrait mode, the title, body & button are aligned vertically and they form
  151. // together have an impact on the height for that column. Many times, we need to calculate a
  152. // suitable heights for them to help decide the layout. The height calculation is influced by quite
  153. // a few factors: the text lenght of title and body, the presence/absense of body & button and
  154. // available card/window sizes. So these are wrapped within
  155. // estimateTextButtomColumnHeightWithDisplayWidth which produce a TitleBodyButtonHeightInfo struct
  156. // to give the estimates of the heights of different elements.
  157. struct TitleBodyButtonHeightInfo {
  158. CGFloat titleHeight;
  159. CGFloat bodyHeight;
  160. // this is the total height of title plus body plus the button. Notice that button or body are
  161. // optional and the result totaColumnlHeight factor in these cases correctly
  162. CGFloat totaColumnlHeight;
  163. };
  164. - (struct TitleBodyButtonHeightInfo)estimateTextBtnColumnHeightWithDisplayWidth:
  165. (CGFloat)displayWidth
  166. withMaxColumnHeight:(CGFloat)maxHeight {
  167. struct TitleBodyButtonHeightInfo resultHeightInfo;
  168. CGFloat titleFitHeight = [self determineTextAreaViewFitHeightForView:self.titleLabel
  169. withWidth:displayWidth];
  170. CGFloat bodyFitHeight = self.modalDisplayMessage.bodyText.length == 0
  171. ? 0
  172. : [self determineTextAreaViewFitHeightForView:self.bodyTextView
  173. withWidth:displayWidth];
  174. CGFloat bodyFitHeightWithPadding = self.modalDisplayMessage.bodyText.length == 0
  175. ? 0
  176. : bodyFitHeight + VerticalSpacingBetweenTitleAndBody;
  177. CGFloat buttonHeight =
  178. self.modalDisplayMessage.actionButton == nil
  179. ? 0
  180. : self.actionButton.frame.size.height + VerticalSpacingBetweenBodyAndActionButton;
  181. // we keep the spacing even if body or button is absent.
  182. CGFloat fitColumnHeight = titleFitHeight + bodyFitHeightWithPadding + buttonHeight;
  183. if (fitColumnHeight < maxHeight) {
  184. // every element get space that can fit the content
  185. resultHeightInfo.bodyHeight = bodyFitHeight;
  186. resultHeightInfo.titleHeight = titleFitHeight;
  187. resultHeightInfo.totaColumnlHeight = fitColumnHeight;
  188. } else {
  189. // need to restrict heights of certain elements
  190. resultHeightInfo.totaColumnlHeight = maxHeight;
  191. if (self.modalDisplayMessage.bodyText.length == 0) {
  192. // no message body, title will try to expand to take all the available height
  193. resultHeightInfo.bodyHeight = 0;
  194. if (self.modalDisplayMessage.actionButton == nil) {
  195. resultHeightInfo.titleHeight = maxHeight;
  196. } else {
  197. // button height, if not 0, already accommodates the space above it
  198. resultHeightInfo.titleHeight = maxHeight - buttonHeight;
  199. }
  200. } else {
  201. // first give title up to 40% of available height
  202. resultHeightInfo.titleHeight = fmin(titleFitHeight, maxHeight * 2 / 5);
  203. CGFloat availableBodyHeight = 0;
  204. if (self.modalDisplayMessage.actionButton == nil) {
  205. availableBodyHeight =
  206. maxHeight - resultHeightInfo.titleHeight - VerticalSpacingBetweenTitleAndBody;
  207. } else {
  208. // body takes the rest minus button space
  209. availableBodyHeight = maxHeight - resultHeightInfo.titleHeight - buttonHeight -
  210. VerticalSpacingBetweenTitleAndBody;
  211. }
  212. if (availableBodyHeight > bodyFitHeight) {
  213. resultHeightInfo.bodyHeight = bodyFitHeight;
  214. // give some back to title height since body does not use up all the allocation
  215. resultHeightInfo.titleHeight += (availableBodyHeight - bodyFitHeight);
  216. } else {
  217. resultHeightInfo.bodyHeight = availableBodyHeight;
  218. }
  219. }
  220. }
  221. FIRLogDebug(kFIRLoggerInAppMessagingDisplay, @"I-FID300003",
  222. @"In heights calculation (max-height = %lf, width = %lf), title heights is %lf, "
  223. "body height is %lf, button height is %lf, total column heights are %lf",
  224. maxHeight, displayWidth, resultHeightInfo.titleHeight, resultHeightInfo.bodyHeight,
  225. buttonHeight, resultHeightInfo.totaColumnlHeight);
  226. return resultHeightInfo;
  227. }
  228. // the following two layoutFineTunexx methods make additional adjustments for the view layout
  229. // in portrait and landscape mode respectively. They are supposed to be triggered from
  230. // viewDidLayoutSubviews since certain dimension sizes are only available there
  231. - (void)layoutFineTuneInPortraitMode {
  232. // for tablet case, since we use a fixed card height, the reference would be just the card height
  233. // for non-tablet case, we want to use a dynamic height , so the reference would be the window
  234. // height
  235. CGFloat heightCalcReference = 0;
  236. if (self.messageCardHeightMaxInTabletCase.active) {
  237. heightCalcReference =
  238. self.messageCardView.frame.size.height - TopBottomPaddingAroundContent * 2;
  239. } else {
  240. heightCalcReference = self.view.window.frame.size.height - TopBottomPaddingAroundContent * 2 -
  241. TopBottomPaddingAroundMsgCard * 2;
  242. // Factor in space for the top notch on iPhone X*.
  243. #if defined(__IPHONE_11_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000
  244. if (@available(iOS 11.0, *)) {
  245. heightCalcReference -= self.view.safeAreaInsets.top;
  246. }
  247. #endif // defined(__IPHONE_11_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000
  248. }
  249. FIRLogDebug(kFIRLoggerInAppMessagingDisplay, @"I-FID300004",
  250. @"The height calc reference is %lf "
  251. "with frame height as %lf",
  252. heightCalcReference, self.view.window.frame.size.height);
  253. // this makes sure titleLable gets correct width to be ready for later's height estimate for the
  254. // text & button column
  255. [self.messageCardView layoutIfNeeded];
  256. // we reserve approximately 1/3 vertical space for image
  257. CGFloat textBtnTotalAvailableHeight =
  258. self.modalDisplayMessage.imageData ? heightCalcReference * 2 / 3 : heightCalcReference;
  259. struct TitleBodyButtonHeightInfo heights =
  260. [self estimateTextBtnColumnHeightWithDisplayWidth:self.titleLabel.frame.size.width
  261. withMaxColumnHeight:textBtnTotalAvailableHeight];
  262. self.titleLabelHeightConstraint.constant = heights.titleHeight;
  263. self.bodyTextViewHeightConstraint.constant = heights.bodyHeight;
  264. if (self.modalDisplayMessage.imageData) {
  265. UIImage *image = [UIImage imageWithData:self.modalDisplayMessage.imageData.imageRawData];
  266. CGSize imageAvailableSpace = CGSizeMake(self.titleLabel.frame.size.width,
  267. heightCalcReference - heights.totaColumnlHeight -
  268. self.imageTopToTitleBottomInPortraitMode.constant);
  269. CGSize imageDisplaySize = [self fitImageInRegionSize:imageAvailableSpace
  270. withImageSize:image.size];
  271. FIRLogDebug(kFIRLoggerInAppMessagingDisplay, @"I-FID300005",
  272. @"Given actual image size %@ and available image display size %@, the actual"
  273. "image display size is %@",
  274. NSStringFromCGSize(image.size), NSStringFromCGSize(imageAvailableSpace),
  275. NSStringFromCGSize(imageDisplaySize));
  276. // for portrait mode, no need to change image width since no content is shown side to
  277. // the image
  278. self.imageActualHeightConstraint.constant = imageDisplaySize.height;
  279. } else {
  280. // no image case
  281. self.imageActualHeightConstraint.constant = 0;
  282. self.imageTopToTitleBottomInPortraitMode.constant = 0;
  283. }
  284. }
  285. - (CGSize)fitImageInRegionSize:(CGSize)regionSize withImageSize:(CGSize)imageSize {
  286. if (imageSize.height <= regionSize.height && imageSize.width <= regionSize.width) {
  287. return imageSize; // image can be fully rendered at its original dimension
  288. } else {
  289. CGFloat regionRatio = regionSize.width / regionSize.height;
  290. CGFloat imageRaio = imageSize.width / imageSize.height;
  291. if (regionRatio < imageRaio) {
  292. // bound on the width dimension
  293. return CGSizeMake(regionSize.width, regionSize.width / imageRaio);
  294. } else {
  295. return CGSizeMake(regionSize.height * imageRaio, regionSize.height);
  296. }
  297. }
  298. }
  299. // for devices of 4 inches or below (iphone se, iphone 5/5s and iphone 4s), reduce
  300. // the padding sizes between elements in the text/button column for landscape mode
  301. - (void)applySmallerSpacingForInLandscapeMode {
  302. if (self.modalDisplayMessage.bodyText.length != 0) {
  303. VerticalSpacingBetweenTitleAndBody = self.bodyTopToTitleBottomInLandScapeMode.constant = 12;
  304. }
  305. if (self.modalDisplayMessage.actionButton != nil &&
  306. self.modalDisplayMessage.bodyText.length != 0) {
  307. VerticalSpacingBetweenBodyAndActionButton = self.buttonTopToBodyBottomConstraint.constant = 12;
  308. }
  309. }
  310. - (void)layoutFineTuneInLandscapeMode {
  311. // smaller spacing threshold is applied for screens equal or larger than 4.7 inches
  312. if (self.view.window.frame.size.height <= 321) {
  313. [self applySmallerSpacingForInLandscapeMode];
  314. }
  315. if (self.modalDisplayMessage.imageData) {
  316. UIImage *image = [UIImage imageWithData:self.modalDisplayMessage.imageData.imageRawData];
  317. CGFloat maxImageHeight = self.view.window.frame.size.height -
  318. TopBottomPaddingAroundContent * 2 - TopBottomPaddingAroundMsgCard * 2;
  319. CGFloat maxImageWidth = self.messageCardView.frame.size.width * 2 / 5;
  320. CGSize imageDisplaySize = [self fitImageInRegionSize:CGSizeMake(maxImageWidth, maxImageHeight)
  321. withImageSize:image.size];
  322. FIRLogDebug(kFIRLoggerInAppMessagingDisplay, @"I-FID300008",
  323. @"In landscape mode, image fit size is %@", NSStringFromCGSize(imageDisplaySize));
  324. // resize image per imageSize
  325. self.imageWidthInLandscapeMode.constant = imageDisplaySize.width;
  326. self.imageActualHeightConstraint.constant = imageDisplaySize.height;
  327. // now we can estimate the new card width given the desired image size
  328. // this assumes we use half of the window width for diplaying the text/button column
  329. CGFloat cardFitWidth = imageDisplaySize.width + self.view.window.frame.size.width / 2 +
  330. LandScapePaddingBetweenImageAndTextColumn;
  331. self.cardLeadingMarginInLandscapeMode.constant =
  332. fmax(15, (self.view.window.frame.size.width - cardFitWidth) / 2);
  333. } else {
  334. self.imageWidthInLandscapeMode.constant = 0;
  335. self.imageActualHeightConstraint.constant = 0;
  336. // card would be of 3/5 width of the screen in landscape
  337. self.cardLeadingMarginInLandscapeMode.constant = self.view.window.frame.size.width / 5;
  338. }
  339. // this makes sure titleLable gets correct width to be ready for later's height estimate for the
  340. // text & button column
  341. [self.messageCardView layoutIfNeeded];
  342. struct TitleBodyButtonHeightInfo heights =
  343. [self estimateTextBtnColumnHeightWithDisplayWidth:self.titleLabel.frame.size.width
  344. withMaxColumnHeight:self.view.frame.size.height -
  345. TopBottomPaddingAroundContent * 2 -
  346. TopBottomPaddingAroundMsgCard * 2];
  347. self.titleLabelHeightConstraint.constant = heights.titleHeight;
  348. self.bodyTextViewHeightConstraint.constant = heights.bodyHeight;
  349. // Adjust the height of the card
  350. // are we bound by the text/button column height or image height ?
  351. CGFloat cardHeight = fmax(self.imageActualHeightConstraint.constant, heights.totaColumnlHeight) +
  352. TopBottomPaddingAroundContent * 2;
  353. self.maxCardHeightInLandscapeMode.constant = cardHeight;
  354. // with the new card height, align the image and the text/btn column to center vertically
  355. self.imageTopToCardTopInLandscapeMode.constant =
  356. (cardHeight - self.imageActualHeightConstraint.constant) / 2;
  357. self.titleTopToCardViewTop.constant = (cardHeight - heights.totaColumnlHeight) / 2;
  358. }
  359. - (void)viewDidLayoutSubviews {
  360. [super viewDidLayoutSubviews];
  361. if (self.traitCollection.horizontalSizeClass == UIUserInterfaceSizeClassRegular ||
  362. self.traitCollection.verticalSizeClass == UIUserInterfaceSizeClassCompact) {
  363. FIRLogDebug(kFIRLoggerInAppMessagingDisplay, @"I-FID300010",
  364. @"Modal view rendered in landscape mode");
  365. [self layoutFineTuneInLandscapeMode];
  366. } else {
  367. FIRLogDebug(kFIRLoggerInAppMessagingDisplay, @"I-FID300009",
  368. @"Modal view rendered in portrait mode");
  369. [self layoutFineTuneInPortraitMode];
  370. }
  371. // always scroll to the top in case the body area is scrollable
  372. [self.bodyTextView setContentOffset:CGPointZero];
  373. }
  374. - (void)viewWillAppear:(BOOL)animated {
  375. [super viewWillAppear:animated];
  376. // close any potential keyboard, which would conflict with the modal in-app messagine view
  377. [[UIApplication sharedApplication] sendAction:@selector(resignFirstResponder)
  378. to:nil
  379. from:nil
  380. forEvent:nil];
  381. if (self.modalDisplayMessage.campaignInfo.renderAsTestMessage) {
  382. FIRLogDebug(kFIRLoggerInAppMessagingDisplay, @"I-FID300011",
  383. @"Flushing the close button since this is a test message.");
  384. [self flashCloseButton:self.closeButton];
  385. }
  386. }
  387. - (void)viewDidAppear:(BOOL)animated {
  388. [super viewDidAppear:animated];
  389. // Announce via VoiceOver that the modal message has appeared. Highlight the title label.
  390. UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, self.titleLabel);
  391. }
  392. - (void)flashCloseButton:(UIButton *)closeButton {
  393. closeButton.alpha = 1.0f;
  394. [UIView animateWithDuration:2.0
  395. delay:0.0
  396. options:UIViewAnimationOptionCurveEaseInOut | UIViewAnimationOptionRepeat |
  397. UIViewAnimationOptionAutoreverse |
  398. UIViewAnimationOptionAllowUserInteraction
  399. animations:^{
  400. closeButton.alpha = 0.1f;
  401. }
  402. completion:^(BOOL finished){
  403. // Do nothing
  404. }];
  405. }
  406. @end
  407. #endif // TARGET_OS_IOS