mirror of
https://github.com/firebase/firebase-ios-sdk
synced 2026-11-09 22:36:01 +05:45
* FIRInAppMessagingDisplayMessage and FIRInAppMessagingDisplayDelegate refactor + downstream changes * More downstream changes from FIRInAppMessagingDisplayDelegate, calling through to app-side delegate from default display executor * Add delegate property on FIRInAppMessaging * Display SDK changes to pass through in app message with callbacks * Headless SDK unit test refactoring * Display test app downstream changes * Pull in headless SDK in display app, no need to have this come in as a framework anymore * Run scripts/style.sh * Remove WIP test * Refactor FIRIAMDIsplayExecutor to take an InAppMessaging object for testing * Depend on FirebaseAnalyticsInterop in UI test app * File imports for FIRInAppMessaging files * Remove FirebaseInAppMessaging imports
456 lines
21 KiB
Objective-C
456 lines
21 KiB
Objective-C
/*
|
|
* Copyright 2018 Google
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
#import <UIKit/UIKit.h>
|
|
|
|
#import "FIDModalViewController.h"
|
|
#import "FIRCore+InAppMessagingDisplay.h"
|
|
#import "FIRInAppMessagingRendering.h"
|
|
|
|
@interface FIDModalViewController ()
|
|
|
|
@property(nonatomic, readwrite) FIRInAppMessagingModalDisplay *modalDisplayMessage;
|
|
|
|
@property(weak, nonatomic) IBOutlet UIImageView *imageView;
|
|
@property(weak, nonatomic) IBOutlet UILabel *titleLabel;
|
|
@property(weak, nonatomic) IBOutlet UIButton *actionButton;
|
|
|
|
@property(weak, nonatomic) IBOutlet UIView *messageCardView;
|
|
@property(weak, nonatomic) IBOutlet UITextView *bodyTextView;
|
|
@property(weak, nonatomic) IBOutlet UIButton *closeButton;
|
|
|
|
// this is only needed for removing the layout errors in interface builder. At runtime
|
|
// we determine the height via its content size. So disable this at runtime.
|
|
@property(weak, nonatomic) IBOutlet NSLayoutConstraint *fixedMessageCardHeightConstraint;
|
|
@property(weak, nonatomic) IBOutlet NSLayoutConstraint *messageCardHeightMaxInTabletCase;
|
|
|
|
@property(weak, nonatomic) IBOutlet NSLayoutConstraint *maxActionButtonHeight;
|
|
@property(weak, nonatomic) IBOutlet NSLayoutConstraint *bodyTextViewHeightConstraint;
|
|
@property(weak, nonatomic) IBOutlet NSLayoutConstraint *buttonTopToBodyBottomConstraint;
|
|
@property(weak, nonatomic) IBOutlet NSLayoutConstraint *imageActualHeightConstraint;
|
|
|
|
// constraints manipulated further in portrait mode
|
|
@property(weak, nonatomic) IBOutlet NSLayoutConstraint *titleLabelHeightConstraint;
|
|
@property(weak, nonatomic) IBOutlet NSLayoutConstraint *buttonBottomToContainerBottomInPortraitMode;
|
|
@property(weak, nonatomic) IBOutlet NSLayoutConstraint *imageTopToTitleBottomInPortraitMode;
|
|
|
|
// constraints manipulated further in landscape mode
|
|
@property(weak, nonatomic) IBOutlet NSLayoutConstraint *imageWidthInLandscapeMode;
|
|
@property(weak, nonatomic) IBOutlet NSLayoutConstraint *titleTopToCardViewTop;
|
|
@property(weak, nonatomic) IBOutlet NSLayoutConstraint *cardLeadingMarginInLandscapeMode;
|
|
@property(weak, nonatomic) IBOutlet NSLayoutConstraint *maxCardHeightInLandscapeMode;
|
|
@property(weak, nonatomic) IBOutlet NSLayoutConstraint *imageTopToCardTopInLandscapeMode;
|
|
@property(weak, nonatomic) IBOutlet NSLayoutConstraint *bodyTopToTitleBottomInLandScapeMode;
|
|
@end
|
|
|
|
static CGFloat VerticalSpacingBetweenTitleAndBody = 24;
|
|
static CGFloat VerticalSpacingBetweenBodyAndActionButton = 24;
|
|
|
|
// the padding between the content and view card's top and bottom edges
|
|
static CGFloat TopBottomPaddingAroundContent = 24;
|
|
// the minimal padding size between msg card and app window's top and bottom
|
|
static CGFloat TopBottomPaddingAroundMsgCard = 30;
|
|
|
|
// the horizontal spacing between image column and text/button column in landscape mode
|
|
static CGFloat LandScapePaddingBetweenImageAndTextColumn = 24;
|
|
|
|
@implementation FIDModalViewController
|
|
|
|
+ (FIDModalViewController *)
|
|
instantiateViewControllerWithResourceBundle:(NSBundle *)resourceBundle
|
|
displayMessage:(FIRInAppMessagingModalDisplay *)modalMessage
|
|
displayDelegate:
|
|
(id<FIRInAppMessagingDisplayDelegate>)displayDelegate
|
|
timeFetcher:(id<FIDTimeFetcher>)timeFetcher {
|
|
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"FIRInAppMessageDisplayStoryboard"
|
|
bundle:resourceBundle];
|
|
|
|
if (storyboard == nil) {
|
|
FIRLogError(kFIRLoggerInAppMessagingDisplay, @"I-FID300001",
|
|
@"Storyboard '"
|
|
"FIRInAppMessageDisplayStoryboard' not found in bundle %@",
|
|
resourceBundle);
|
|
return nil;
|
|
}
|
|
FIDModalViewController *modalVC = (FIDModalViewController *)[storyboard
|
|
instantiateViewControllerWithIdentifier:@"modal-view-vc"];
|
|
modalVC.displayDelegate = displayDelegate;
|
|
modalVC.modalDisplayMessage = modalMessage;
|
|
modalVC.timeFetcher = timeFetcher;
|
|
|
|
return modalVC;
|
|
}
|
|
|
|
- (FIRInAppMessagingDisplayMessage *)inAppMessage {
|
|
return self.modalDisplayMessage;
|
|
}
|
|
|
|
- (IBAction)closeButtonClicked:(id)sender {
|
|
[self dismissView:FIRInAppMessagingDismissTypeUserTapClose];
|
|
}
|
|
|
|
- (IBAction)actionButtonTapped:(id)sender {
|
|
[self followActionURL];
|
|
}
|
|
|
|
- (void)viewDidLoad {
|
|
[super viewDidLoad];
|
|
|
|
// make the background half transparent
|
|
[self.view setBackgroundColor:[UIColor.grayColor colorWithAlphaComponent:0.5]];
|
|
|
|
// populating values for display elements
|
|
|
|
self.titleLabel.text = self.modalDisplayMessage.title;
|
|
self.bodyTextView.text = self.modalDisplayMessage.bodyText;
|
|
|
|
if (self.modalDisplayMessage.imageData) {
|
|
[self.imageView
|
|
setImage:[UIImage imageWithData:self.modalDisplayMessage.imageData.imageRawData]];
|
|
self.imageView.contentMode = UIViewContentModeScaleAspectFit;
|
|
}
|
|
|
|
self.messageCardView.backgroundColor = self.modalDisplayMessage.displayBackgroundColor;
|
|
self.messageCardView.layer.cornerRadius = 4;
|
|
|
|
self.titleLabel.textColor = self.modalDisplayMessage.textColor;
|
|
self.bodyTextView.textColor = self.modalDisplayMessage.textColor;
|
|
self.bodyTextView.selectable = NO;
|
|
|
|
if (self.modalDisplayMessage.actionButton.buttonText.length != 0) {
|
|
[self.actionButton setTitle:self.modalDisplayMessage.actionButton.buttonText
|
|
forState:UIControlStateNormal];
|
|
self.actionButton.backgroundColor = self.modalDisplayMessage.actionButton.buttonBackgroundColor;
|
|
[self.actionButton setTitleColor:self.modalDisplayMessage.actionButton.buttonTextColor
|
|
forState:UIControlStateNormal];
|
|
self.actionButton.layer.cornerRadius = 4;
|
|
|
|
if (self.modalDisplayMessage.bodyText.length == 0) {
|
|
self.buttonTopToBodyBottomConstraint.constant = 0;
|
|
}
|
|
} else {
|
|
// either action button text is empty or nil
|
|
|
|
// hide the action button and reclaim the space below the buttom
|
|
FIRLogDebug(kFIRLoggerInAppMessagingDisplay, @"I-FID300002",
|
|
@"Modal view to be rendered without action button");
|
|
self.maxActionButtonHeight.constant = 0;
|
|
self.actionButton.clipsToBounds = YES;
|
|
self.buttonTopToBodyBottomConstraint.constant = 0;
|
|
}
|
|
|
|
[self.view addConstraint:self.imageActualHeightConstraint];
|
|
self.imageActualHeightConstraint.active = YES;
|
|
self.fixedMessageCardHeightConstraint.active = NO;
|
|
}
|
|
|
|
// for text display UIview, which could be a UILabel or UITextView, decide the fit height under a
|
|
// given display width
|
|
- (CGFloat)determineTextAreaViewFitHeightForView:(UIView *)textView
|
|
withWidth:(CGFloat)displayWidth {
|
|
CGSize displaySize = CGSizeMake(displayWidth, FLT_MAX);
|
|
return [textView sizeThatFits:displaySize].height;
|
|
}
|
|
|
|
// In both landscape or portrait mode, the title, body & button are aligned vertically and they form
|
|
// together have an impact on the height for that column. Many times, we need to calculate a
|
|
// suitable heights for them to help decide the layout. The height calculation is influced by quite
|
|
// a few factors: the text lenght of title and body, the presence/absense of body & button and
|
|
// available card/window sizes. So these are wrapped within
|
|
// estimateTextButtomColumnHeightWithDisplayWidth which produce a TitleBodyButtonHeightInfo struct
|
|
// to give the estimates of the heights of different elements.
|
|
struct TitleBodyButtonHeightInfo {
|
|
CGFloat titleHeight;
|
|
CGFloat bodyHeight;
|
|
|
|
// this is the total height of title plus body plus the button. Notice that button or body are
|
|
// optional and the result totaColumnlHeight factor in these cases correctly
|
|
CGFloat totaColumnlHeight;
|
|
};
|
|
|
|
- (struct TitleBodyButtonHeightInfo)estimateTextBtnColumnHeightWithDisplayWidth:
|
|
(CGFloat)displayWidth
|
|
withMaxColumnHeight:(CGFloat)maxHeight {
|
|
struct TitleBodyButtonHeightInfo resultHeightInfo;
|
|
|
|
CGFloat titleFitHeight = [self determineTextAreaViewFitHeightForView:self.titleLabel
|
|
withWidth:displayWidth];
|
|
CGFloat bodyFitHeight = self.modalDisplayMessage.bodyText.length == 0
|
|
? 0
|
|
: [self determineTextAreaViewFitHeightForView:self.bodyTextView
|
|
withWidth:displayWidth];
|
|
|
|
CGFloat bodyFitHeightWithPadding = self.modalDisplayMessage.bodyText.length == 0
|
|
? 0
|
|
: bodyFitHeight + VerticalSpacingBetweenTitleAndBody;
|
|
|
|
CGFloat buttonHeight =
|
|
self.modalDisplayMessage.actionButton == nil
|
|
? 0
|
|
: self.actionButton.frame.size.height + VerticalSpacingBetweenBodyAndActionButton;
|
|
|
|
// we keep the spacing even if body or button is absent.
|
|
CGFloat fitColumnHeight = titleFitHeight + bodyFitHeightWithPadding + buttonHeight;
|
|
|
|
if (fitColumnHeight < maxHeight) {
|
|
// every element get space that can fit the content
|
|
resultHeightInfo.bodyHeight = bodyFitHeight;
|
|
resultHeightInfo.titleHeight = titleFitHeight;
|
|
resultHeightInfo.totaColumnlHeight = fitColumnHeight;
|
|
} else {
|
|
// need to restrict heights of certain elements
|
|
resultHeightInfo.totaColumnlHeight = maxHeight;
|
|
if (self.modalDisplayMessage.bodyText.length == 0) {
|
|
// no message body, title will try to expand to take all the available height
|
|
resultHeightInfo.bodyHeight = 0;
|
|
if (self.modalDisplayMessage.actionButton == nil) {
|
|
resultHeightInfo.titleHeight = maxHeight;
|
|
} else {
|
|
// button height, if not 0, already accommodates the space above it
|
|
resultHeightInfo.titleHeight = maxHeight - buttonHeight;
|
|
}
|
|
} else {
|
|
// first give title up to 40% of available height
|
|
resultHeightInfo.titleHeight = fmin(titleFitHeight, maxHeight * 2 / 5);
|
|
|
|
CGFloat availableBodyHeight = 0;
|
|
if (self.modalDisplayMessage.actionButton == nil) {
|
|
availableBodyHeight =
|
|
maxHeight - resultHeightInfo.titleHeight - VerticalSpacingBetweenTitleAndBody;
|
|
} else {
|
|
// body takes the rest minus button space
|
|
availableBodyHeight = maxHeight - resultHeightInfo.titleHeight - buttonHeight -
|
|
VerticalSpacingBetweenTitleAndBody;
|
|
}
|
|
|
|
if (availableBodyHeight > bodyFitHeight) {
|
|
resultHeightInfo.bodyHeight = bodyFitHeight;
|
|
// give some back to title height since body does not use up all the allocation
|
|
resultHeightInfo.titleHeight += (availableBodyHeight - bodyFitHeight);
|
|
} else {
|
|
resultHeightInfo.bodyHeight = availableBodyHeight;
|
|
}
|
|
}
|
|
}
|
|
|
|
FIRLogDebug(kFIRLoggerInAppMessagingDisplay, @"I-FID300003",
|
|
@"In heights calculation (max-height = %lf, width = %lf), title heights is %lf, "
|
|
"body height is %lf, button height is %lf, total column heights are %lf",
|
|
maxHeight, displayWidth, resultHeightInfo.titleHeight, resultHeightInfo.bodyHeight,
|
|
buttonHeight, resultHeightInfo.totaColumnlHeight);
|
|
|
|
return resultHeightInfo;
|
|
}
|
|
|
|
// the following two layoutFineTunexx methods make additional adjustments for the view layout
|
|
// in portrait and landscape mode respectively. They are supposed to be triggered from
|
|
// viewDidLayoutSubviews since certain dimension sizes are only available there
|
|
- (void)layoutFineTuneInPortraitMode {
|
|
// for tablet case, since we use a fixed card height, the reference would be just the card height
|
|
// for non-tablet case, we want to use a dynamic height , so the reference would be the window
|
|
// height
|
|
CGFloat heightCalcReference =
|
|
self.messageCardHeightMaxInTabletCase.active
|
|
? self.messageCardView.frame.size.height - TopBottomPaddingAroundContent * 2
|
|
: self.view.window.frame.size.height - TopBottomPaddingAroundContent * 2 -
|
|
TopBottomPaddingAroundMsgCard * 2;
|
|
|
|
FIRLogDebug(kFIRLoggerInAppMessagingDisplay, @"I-FID300004",
|
|
@"The height calc reference is %lf "
|
|
"with frame height as %lf",
|
|
heightCalcReference, self.view.window.frame.size.height);
|
|
|
|
// this makes sure titleLable gets correct width to be ready for later's height estimate for the
|
|
// text & button column
|
|
[self.messageCardView layoutIfNeeded];
|
|
|
|
// we reserve approximately 1/3 vertical space for image
|
|
CGFloat textBtnTotalAvailableHeight =
|
|
self.modalDisplayMessage.imageData ? heightCalcReference * 2 / 3 : heightCalcReference;
|
|
|
|
struct TitleBodyButtonHeightInfo heights =
|
|
[self estimateTextBtnColumnHeightWithDisplayWidth:self.titleLabel.frame.size.width
|
|
withMaxColumnHeight:textBtnTotalAvailableHeight];
|
|
|
|
self.titleLabelHeightConstraint.constant = heights.titleHeight;
|
|
self.bodyTextViewHeightConstraint.constant = heights.bodyHeight;
|
|
|
|
if (self.modalDisplayMessage.imageData) {
|
|
UIImage *image = [UIImage imageWithData:self.modalDisplayMessage.imageData.imageRawData];
|
|
CGSize imageAvailableSpace = CGSizeMake(self.titleLabel.frame.size.width,
|
|
heightCalcReference - heights.totaColumnlHeight -
|
|
self.imageTopToTitleBottomInPortraitMode.constant);
|
|
|
|
CGSize imageDisplaySize = [self fitImageInRegionSize:imageAvailableSpace
|
|
withImageSize:image.size];
|
|
|
|
FIRLogDebug(kFIRLoggerInAppMessagingDisplay, @"I-FID300005",
|
|
@"Given actual image size %@ and available image display size %@, the actual"
|
|
"image display size is %@",
|
|
NSStringFromCGSize(image.size), NSStringFromCGSize(imageAvailableSpace),
|
|
NSStringFromCGSize(imageDisplaySize));
|
|
|
|
// for portrait mode, no need to change image width since no content is shown side to
|
|
// the image
|
|
self.imageActualHeightConstraint.constant = imageDisplaySize.height;
|
|
} else {
|
|
// no image case
|
|
self.imageActualHeightConstraint.constant = 0;
|
|
self.imageTopToTitleBottomInPortraitMode.constant = 0;
|
|
}
|
|
}
|
|
|
|
- (CGSize)fitImageInRegionSize:(CGSize)regionSize withImageSize:(CGSize)imageSize {
|
|
if (imageSize.height <= regionSize.height && imageSize.width <= regionSize.width) {
|
|
return imageSize; // image can be fully rendered at its original dimension
|
|
} else {
|
|
CGFloat regionRatio = regionSize.width / regionSize.height;
|
|
CGFloat imageRaio = imageSize.width / imageSize.height;
|
|
|
|
if (regionRatio < imageRaio) {
|
|
// bound on the width dimension
|
|
return CGSizeMake(regionSize.width, regionSize.width / imageRaio);
|
|
} else {
|
|
return CGSizeMake(regionSize.height * imageRaio, regionSize.height);
|
|
}
|
|
}
|
|
}
|
|
|
|
// for devices of 4 inches or below (iphone se, iphone 5/5s and iphone 4s), reduce
|
|
// the padding sizes between elements in the text/button column for landscape mode
|
|
- (void)applySmallerSpacingForInLandscapeMode {
|
|
if (self.modalDisplayMessage.bodyText.length != 0) {
|
|
VerticalSpacingBetweenTitleAndBody = self.bodyTopToTitleBottomInLandScapeMode.constant = 12;
|
|
}
|
|
|
|
if (self.modalDisplayMessage.actionButton != nil &&
|
|
self.modalDisplayMessage.bodyText.length != 0) {
|
|
VerticalSpacingBetweenBodyAndActionButton = self.buttonTopToBodyBottomConstraint.constant = 12;
|
|
}
|
|
}
|
|
|
|
- (void)layoutFineTuneInLandscapeMode {
|
|
// smaller spacing threshold is applied for screens equal or larger than 4.7 inches
|
|
if (self.view.window.frame.size.height <= 321) {
|
|
[self applySmallerSpacingForInLandscapeMode];
|
|
}
|
|
|
|
if (self.modalDisplayMessage.imageData) {
|
|
UIImage *image = [UIImage imageWithData:self.modalDisplayMessage.imageData.imageRawData];
|
|
|
|
CGFloat maxImageHeight = self.view.window.frame.size.height -
|
|
TopBottomPaddingAroundContent * 2 - TopBottomPaddingAroundMsgCard * 2;
|
|
CGFloat maxImageWidth = self.messageCardView.frame.size.width * 2 / 5;
|
|
CGSize imageDisplaySize = [self fitImageInRegionSize:CGSizeMake(maxImageWidth, maxImageHeight)
|
|
withImageSize:image.size];
|
|
|
|
FIRLogDebug(kFIRLoggerInAppMessagingDisplay, @"I-FID300008",
|
|
@"In landscape mode, image fit size is %@", NSStringFromCGSize(imageDisplaySize));
|
|
|
|
// resize image per imageSize
|
|
self.imageWidthInLandscapeMode.constant = imageDisplaySize.width;
|
|
self.imageActualHeightConstraint.constant = imageDisplaySize.height;
|
|
|
|
// now we can estimate the new card width given the desired image size
|
|
|
|
// this assumes we use half of the window width for diplaying the text/button column
|
|
CGFloat cardFitWidth = imageDisplaySize.width + self.view.window.frame.size.width / 2 +
|
|
LandScapePaddingBetweenImageAndTextColumn;
|
|
|
|
self.cardLeadingMarginInLandscapeMode.constant =
|
|
fmax(15, (self.view.window.frame.size.width - cardFitWidth) / 2);
|
|
} else {
|
|
self.imageWidthInLandscapeMode.constant = 0;
|
|
self.imageActualHeightConstraint.constant = 0;
|
|
|
|
// card would be of 3/5 width of the screen in landscape
|
|
self.cardLeadingMarginInLandscapeMode.constant = self.view.window.frame.size.width / 5;
|
|
}
|
|
|
|
// this makes sure titleLable gets correct width to be ready for later's height estimate for the
|
|
// text & button column
|
|
[self.messageCardView layoutIfNeeded];
|
|
|
|
struct TitleBodyButtonHeightInfo heights =
|
|
[self estimateTextBtnColumnHeightWithDisplayWidth:self.titleLabel.frame.size.width
|
|
withMaxColumnHeight:self.view.frame.size.height -
|
|
TopBottomPaddingAroundContent * 2 -
|
|
TopBottomPaddingAroundMsgCard * 2];
|
|
|
|
self.titleLabelHeightConstraint.constant = heights.titleHeight;
|
|
self.bodyTextViewHeightConstraint.constant = heights.bodyHeight;
|
|
|
|
// Adjust the height of the card
|
|
// are we bound by the text/button column height or image height ?
|
|
CGFloat cardHeight = fmax(self.imageActualHeightConstraint.constant, heights.totaColumnlHeight) +
|
|
TopBottomPaddingAroundContent * 2;
|
|
self.maxCardHeightInLandscapeMode.constant = cardHeight;
|
|
|
|
// with the new card height, align the image and the text/btn column to center vertically
|
|
self.imageTopToCardTopInLandscapeMode.constant =
|
|
(cardHeight - self.imageActualHeightConstraint.constant) / 2;
|
|
self.titleTopToCardViewTop.constant = (cardHeight - heights.totaColumnlHeight) / 2;
|
|
}
|
|
|
|
- (void)viewDidLayoutSubviews {
|
|
[super viewDidLayoutSubviews];
|
|
|
|
if (self.traitCollection.horizontalSizeClass == UIUserInterfaceSizeClassRegular ||
|
|
self.traitCollection.verticalSizeClass == UIUserInterfaceSizeClassCompact) {
|
|
FIRLogDebug(kFIRLoggerInAppMessagingDisplay, @"I-FID300010",
|
|
@"Modal view rendered in landscape mode");
|
|
[self layoutFineTuneInLandscapeMode];
|
|
} else {
|
|
FIRLogDebug(kFIRLoggerInAppMessagingDisplay, @"I-FID300009",
|
|
@"Modal view rendered in portrait mode");
|
|
[self layoutFineTuneInPortraitMode];
|
|
}
|
|
|
|
// always scroll to the top in case the body area is scrollable
|
|
[self.bodyTextView setContentOffset:CGPointZero];
|
|
}
|
|
|
|
- (void)viewWillAppear:(BOOL)animated {
|
|
[super viewWillAppear:animated];
|
|
// close any potential keyboard, which would conflict with the modal in-app messagine view
|
|
[[UIApplication sharedApplication] sendAction:@selector(resignFirstResponder)
|
|
to:nil
|
|
from:nil
|
|
forEvent:nil];
|
|
|
|
if (self.modalDisplayMessage.campaignInfo.renderAsTestMessage) {
|
|
FIRLogDebug(kFIRLoggerInAppMessagingDisplay, @"I-FID300011",
|
|
@"Flushing the close button since this is a test message.");
|
|
[self flashCloseButton:self.closeButton];
|
|
}
|
|
}
|
|
|
|
- (void)flashCloseButton:(UIButton *)closeButton {
|
|
closeButton.alpha = 1.0f;
|
|
[UIView animateWithDuration:2.0
|
|
delay:0.0
|
|
options:UIViewAnimationOptionCurveEaseInOut | UIViewAnimationOptionRepeat |
|
|
UIViewAnimationOptionAutoreverse |
|
|
UIViewAnimationOptionAllowUserInteraction
|
|
animations:^{
|
|
closeButton.alpha = 0.1f;
|
|
}
|
|
completion:^(BOOL finished){
|
|
// Do nothing
|
|
}];
|
|
}
|
|
@end
|