Skip to content

Commit c7e9b2d

Browse files
authored
add firebase in-app messaging display pod (#1798)
1 parent ed30bc9 commit c7e9b2d

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+4662
-1
lines changed
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
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+
17+
#import <Foundation/Foundation.h>
18+
19+
#import "FIDBaseRenderingViewController.h"
20+
21+
@class FIRInAppMessagingBannerDisplay;
22+
@class FIDBaseRenderingViewController;
23+
@protocol FIDTimeFetcher;
24+
@protocol FIRInAppMessagingDisplayDelegate;
25+
26+
NS_ASSUME_NONNULL_BEGIN
27+
@interface FIDBannerViewController : FIDBaseRenderingViewController
28+
+ (FIDBannerViewController *)
29+
instantiateViewControllerWithResourceBundle:(NSBundle *)resourceBundle
30+
displayMessage:(FIRInAppMessagingBannerDisplay *)bannerMessage
31+
displayDelegate:
32+
(id<FIRInAppMessagingDisplayDelegate>)displayDelegate
33+
timeFetcher:(id<FIDTimeFetcher>)timeFetcher;
34+
@end
35+
NS_ASSUME_NONNULL_END
Lines changed: 293 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,293 @@
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+
17+
#import <FirebaseInAppMessaging/FIRInAppMessagingRendering.h>
18+
19+
#import "FIDBannerViewController.h"
20+
#import "FIRCore+InAppMessagingDisplay.h"
21+
22+
@interface FIDBannerViewController ()
23+
24+
@property(nonatomic, readwrite) FIRInAppMessagingBannerDisplay *bannerDisplayMessage;
25+
26+
@property(weak, nonatomic) IBOutlet NSLayoutConstraint *imageViewWidthConstraint;
27+
@property(weak, nonatomic) IBOutlet NSLayoutConstraint *imageViewHeightConstraint;
28+
29+
@property(weak, nonatomic)
30+
IBOutlet NSLayoutConstraint *imageBottomAlignWithBodyLabelBottomConstraint;
31+
@property(weak, nonatomic) IBOutlet UIImageView *imageView;
32+
@property(weak, nonatomic) IBOutlet UILabel *titleLabel;
33+
@property(weak, nonatomic) IBOutlet UILabel *bodyLabel;
34+
35+
// Banner view will be rendered and dismissed with animation. Within viewDidLayoutSubviews function,
36+
// we would position the view so that it's out of UIWindow range on the top so that later on it can
37+
// slide in with animation. However, viewDidLayoutSubviews is also triggred in other scenarios
38+
// like split view on iPad or device orientation changes where we don't want to hide the banner for
39+
// animations. So to have different logic, we use this property to tell the two different
40+
// cases apart and apply different positioning logic accordingly in viewDidLayoutSubviews.
41+
@property(nonatomic) BOOL hidingForAnimation;
42+
43+
@property(nonatomic, nullable) NSTimer *autoDismissTimer;
44+
@end
45+
46+
// The image display area dimension in points
47+
static const CGFloat kBannerViewImageWidth = 60;
48+
static const CGFloat kBannerViewImageHeight = 60;
49+
50+
static const NSTimeInterval kBannerViewAnimationDuration = 0.3; // in seconds
51+
52+
// Banner view will auto dismiss after this amount of time of showing if user does not take
53+
// any other actions. It's in seconds.
54+
static const NSTimeInterval kBannerAutoDimissTime = 12;
55+
56+
// If the window width is larger than this threshold, we cap banner view width
57+
// by it: showing a non full-width banner when it happens.
58+
static const CGFloat kBannerViewMaxWidth = 736;
59+
60+
static const CGFloat kSwipeUpThreshold = -10.0f;
61+
62+
@implementation FIDBannerViewController
63+
64+
+ (FIDBannerViewController *)
65+
instantiateViewControllerWithResourceBundle:(NSBundle *)resourceBundle
66+
displayMessage:(FIRInAppMessagingBannerDisplay *)bannerMessage
67+
displayDelegate:
68+
(id<FIRInAppMessagingDisplayDelegate>)displayDelegate
69+
timeFetcher:(id<FIDTimeFetcher>)timeFetcher {
70+
UIStoryboard *storyboard =
71+
[UIStoryboard storyboardWithName:@"FIRInAppMessageDisplayStoryboard" bundle:resourceBundle];
72+
73+
if (storyboard == nil) {
74+
FIRLogError(kFIRLoggerInAppMessagingDisplay, @"I-FID300002",
75+
@"Storyboard '"
76+
"FIRInAppMessageDisplayStoryboard' not found in bundle %@",
77+
resourceBundle);
78+
return nil;
79+
}
80+
FIDBannerViewController *bannerVC = (FIDBannerViewController *)[storyboard
81+
instantiateViewControllerWithIdentifier:@"banner-view-vc"];
82+
bannerVC.displayDelegate = displayDelegate;
83+
bannerVC.bannerDisplayMessage = bannerMessage;
84+
bannerVC.timeFetcher = timeFetcher;
85+
86+
return bannerVC;
87+
}
88+
89+
- (void)setupRecognizers {
90+
UIPanGestureRecognizer *panSwipeRecognizer =
91+
[[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePanSwipe:)];
92+
[self.view addGestureRecognizer:panSwipeRecognizer];
93+
94+
UITapGestureRecognizer *tapGestureRecognizer =
95+
[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(messageTapped:)];
96+
tapGestureRecognizer.delaysTouchesBegan = YES;
97+
tapGestureRecognizer.numberOfTapsRequired = 1;
98+
99+
[self.view addGestureRecognizer:tapGestureRecognizer];
100+
}
101+
102+
- (void)handlePanSwipe:(UIPanGestureRecognizer *)recognizer {
103+
// Detect the swipe gesture
104+
if (recognizer.state == UIGestureRecognizerStateEnded) {
105+
CGPoint vel = [recognizer velocityInView:recognizer.view];
106+
if (vel.y < kSwipeUpThreshold) {
107+
[self closeViewFromManualDismiss];
108+
}
109+
}
110+
}
111+
112+
- (void)viewDidLoad {
113+
[super viewDidLoad];
114+
// Do any additional setup after loading the view from its nib.
115+
116+
[self setupRecognizers];
117+
118+
self.titleLabel.text = self.bannerDisplayMessage.title;
119+
self.bodyLabel.text = self.bannerDisplayMessage.bodyText;
120+
121+
if (self.bannerDisplayMessage.imageData) {
122+
self.imageView.contentMode = UIViewContentModeScaleAspectFit;
123+
124+
UIImage *image = [UIImage imageWithData:self.bannerDisplayMessage.imageData.imageRawData];
125+
126+
if (fabs(image.size.width / image.size.height - 1) > 0.02) {
127+
// width and height differ by at least 2%, need to adjust image view
128+
// size to respect the ratio
129+
130+
// reduce height or width of the image view to retain the ratio of the image
131+
if (image.size.width > image.size.height) {
132+
CGFloat newImageHeight = kBannerViewImageWidth * image.size.height / image.size.width;
133+
self.imageViewHeightConstraint.constant = newImageHeight;
134+
} else {
135+
CGFloat newImageWidth = kBannerViewImageHeight * image.size.width / image.size.height;
136+
self.imageViewWidthConstraint.constant = newImageWidth;
137+
}
138+
}
139+
self.imageView.image = image;
140+
} else {
141+
// Hide image and remove the bottom constraint between body label and image view.
142+
self.imageViewWidthConstraint.constant = 0;
143+
self.imageBottomAlignWithBodyLabelBottomConstraint.active = NO;
144+
}
145+
146+
// Set some rendering effects based on settings.
147+
self.view.backgroundColor = self.bannerDisplayMessage.displayBackgroundColor;
148+
self.titleLabel.textColor = self.bannerDisplayMessage.textColor;
149+
self.bodyLabel.textColor = self.bannerDisplayMessage.textColor;
150+
151+
self.view.layer.masksToBounds = NO;
152+
self.view.layer.shadowOffset = CGSizeMake(2, 1);
153+
self.view.layer.shadowRadius = 2;
154+
self.view.layer.shadowOpacity = 0.4;
155+
156+
// When created, we are hiding it for later animation
157+
self.hidingForAnimation = YES;
158+
[self setupAutoDismissTimer];
159+
}
160+
161+
- (void)dismissViewWithAnimation:(void (^)(void))completion {
162+
CGRect rectInNormalState = self.view.frame;
163+
CGAffineTransform hidingTransform =
164+
CGAffineTransformMakeTranslation(0, -rectInNormalState.size.height);
165+
166+
[UIView animateWithDuration:kBannerViewAnimationDuration
167+
delay:0
168+
options:UIViewAnimationOptionCurveEaseInOut
169+
animations:^{
170+
self.view.transform = hidingTransform;
171+
}
172+
completion:^(BOOL finished) {
173+
completion();
174+
}];
175+
}
176+
177+
- (void)closeViewFromAutoDismiss {
178+
FIRLogDebug(kFIRLoggerInAppMessagingDisplay, @"I-FID300001", @"Auto dismiss the banner view");
179+
[self dismissViewWithAnimation:^(void) {
180+
[self dismissView:FIRInAppMessagingDismissTypeAuto];
181+
}];
182+
}
183+
184+
- (void)closeViewFromManualDismiss {
185+
FIRLogDebug(kFIRLoggerInAppMessagingDisplay, @"I-FID300003", @"Manually dismiss the banner view");
186+
[self.autoDismissTimer invalidate];
187+
[self dismissViewWithAnimation:^(void) {
188+
[self dismissView:FIRInAppMessagingDismissTypeUserSwipe];
189+
}];
190+
}
191+
192+
- (void)messageTapped:(UITapGestureRecognizer *)recognizer {
193+
[self.autoDismissTimer invalidate];
194+
[self dismissViewWithAnimation:^(void) {
195+
[self followActionURL];
196+
}];
197+
}
198+
199+
- (void)adjustBodyLabelViewHeight {
200+
// These lines make sure that we only change the height of the label view
201+
// to fit the content. Doing [self.bodyLabel sizeToFit] only could potentially
202+
// change the width as well.
203+
CGRect theFrame = self.bodyLabel.frame;
204+
[self.bodyLabel sizeToFit];
205+
theFrame.size.height = self.bodyLabel.frame.size.height;
206+
self.bodyLabel.frame = theFrame;
207+
}
208+
209+
- (void)viewDidLayoutSubviews {
210+
[super viewDidLayoutSubviews];
211+
212+
CGFloat bannerViewHeight = 0;
213+
214+
[self adjustBodyLabelViewHeight];
215+
216+
if (self.bannerDisplayMessage.imageData) {
217+
CGFloat imageBottom = CGRectGetMaxY(self.imageView.frame);
218+
CGFloat bodyBottom = CGRectGetMaxY(self.bodyLabel.frame);
219+
bannerViewHeight = MAX(imageBottom, bodyBottom);
220+
} else {
221+
bannerViewHeight = CGRectGetMaxY(self.bodyLabel.frame);
222+
}
223+
224+
bannerViewHeight += 5; // Add some padding margin on the bottom of the view
225+
226+
CGFloat appWindowWidth = [self.view.window bounds].size.width;
227+
CGFloat bannerViewWidth = appWindowWidth;
228+
229+
if (bannerViewWidth > kBannerViewMaxWidth) {
230+
bannerViewWidth = kBannerViewMaxWidth;
231+
self.view.layer.cornerRadius = 4;
232+
}
233+
234+
CGRect viewRect =
235+
CGRectMake((appWindowWidth - bannerViewWidth) / 2, 0, bannerViewWidth, bannerViewHeight);
236+
self.view.frame = viewRect;
237+
238+
if (self.hidingForAnimation) {
239+
// Move the banner to be just above the top of the window to hide it.
240+
self.view.center = CGPointMake(appWindowWidth / 2, -viewRect.size.height / 2);
241+
}
242+
}
243+
244+
- (void)viewDidAppear:(BOOL)animated {
245+
[super viewDidAppear:animated];
246+
CGRect rectInNormalState = self.view.frame;
247+
CGPoint normalCenterPoint =
248+
CGPointMake(rectInNormalState.origin.x + rectInNormalState.size.width / 2,
249+
rectInNormalState.size.height / 2);
250+
251+
self.hidingForAnimation = NO;
252+
[UIView animateWithDuration:kBannerViewAnimationDuration
253+
delay:0
254+
options:UIViewAnimationOptionCurveEaseInOut
255+
animations:^{
256+
self.view.center = normalCenterPoint;
257+
}
258+
completion:nil];
259+
}
260+
261+
- (void)setupAutoDismissTimer {
262+
NSTimeInterval remaining = kBannerAutoDimissTime - super.aggregateImpressionTimeInSeconds;
263+
264+
FIRLogDebug(kFIRLoggerInAppMessagingDisplay, @"I-FID300004",
265+
@"Remaining banner auto dismiss time is %lf", remaining);
266+
267+
// Set up the auto dismiss behavior.
268+
__weak id weakSelf = self;
269+
self.autoDismissTimer =
270+
[NSTimer scheduledTimerWithTimeInterval:remaining
271+
target:weakSelf
272+
selector:@selector(closeViewFromAutoDismiss)
273+
userInfo:nil
274+
repeats:NO];
275+
}
276+
277+
// Handlers for app become active inactive so that we can better adjust our auto dismiss feature
278+
- (void)appDidBecomeInactive:(UIApplication *)application {
279+
[super appDidBecomeInactive:application];
280+
[self.autoDismissTimer invalidate];
281+
}
282+
283+
- (void)appDidBecomeActive:(UIApplication *)application {
284+
[super appDidBecomeActive:application];
285+
[self setupAutoDismissTimer];
286+
}
287+
288+
- (void)dealloc {
289+
FIRLogDebug(kFIRLoggerInAppMessagingDisplay, @"I-FID300005",
290+
@"-[FIRIAMBannerViewController dealloc] triggered for %p", self);
291+
[self.autoDismissTimer invalidate];
292+
}
293+
@end
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
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+
17+
#import <UIKit/UIKit.h>
18+
19+
NS_ASSUME_NONNULL_BEGIN
20+
@interface FIDBannerViewUIWindow : UIWindow
21+
22+
@end
23+
NS_ASSUME_NONNULL_END
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
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+
17+
#import "FIDBannerViewUIWindow.h"
18+
19+
@implementation FIDBannerViewUIWindow
20+
21+
// For banner view message, we still allow the user to interact with the app's underlying view
22+
// outside banner view's visible area.
23+
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
24+
if (self.rootViewController && self.rootViewController.view) {
25+
return CGRectContainsPoint(self.rootViewController.view.frame, point);
26+
} else {
27+
return NO;
28+
}
29+
}
30+
@end

0 commit comments

Comments
 (0)