Skip to content

Commit e22c7a7

Browse files
authored
Display custom FIAMs with SwiftUI (#7496)
* Revert to previous nil check on experiment JSON * Add Swift extensions podspec * Add viewmodifier for displaying custom in-app messages w/ SwiftUI * Add Multiplatform project to integration test SwiftUI pod * We don't support macOS yet, comment that out from the Podfile * Fix variable renaming bug * View modifier implementation that works, but design is questionable * Super basic integration * Split view modifiers based on in-app message subtype * Fix versioning in podspec * Configure Firebase in test app * Remove trailing whitespace * Clean up SDK fetching code, sample app * Updates to sample code, fully functional modal message now * Update implementing app example * Better code readability for view modifiers * Add unit tests for DelegateBridge * Add test target to pod spec * Add copyright notices * Remove whitespace from pod spec * Add auto-complete documentation for in-app message extensions * Lowered min target * Update Package.swift * First pass at CI updates * @available(13) for SwiftUI structs * Use singleton DelegateBridge * Update inappmessaging.yml * Run scripts/style.sh * Update build.sh * Modify build.sh * Update CHANGELOG, try something new with GHA configuration * Let's try this for GHA * Fully qualify FIAM message enums * Remove redundant type declaration * Let's try direct instantiation of messages in unit tests * Test CI build works without actual test cases * Let's try this, making init available. * Try exposing initializer * Expose simpler initializer for in-app message superclass * Fix up UI test app initializers * Update CHANGELOG, GHA workflows * Cleaner GHA test flow * Clean up custom in-app message view modifier implementation * Configuration cleanup * Add some tests to ensure FIAM SwiftUI API compiles * Add SpecsDev to Podfile of integration testing app. Make @State var private
1 parent a80e30f commit e22c7a7

File tree

22 files changed

+1327
-3
lines changed

22 files changed

+1327
-3
lines changed

.github/workflows/inappmessaging.yml

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,15 @@ jobs:
1818
if: (github.repository == 'Firebase/firebase-ios-sdk' && github.event_name == 'schedule') || github.event_name == 'pull_request'
1919

2020
runs-on: macOS-latest
21+
strategy:
22+
matrix:
23+
podspec: [FirebaseInAppMessaging.podspec, FirebaseInAppMessagingSwift.podspec]
2124
steps:
2225
- uses: actions/checkout@v2
2326
- name: Setup Bundler
2427
run: scripts/setup_bundler.sh
2528
- name: FirebaseInAppMessaging
26-
run: scripts/third_party/travis/retry.sh scripts/pod_lib_lint.rb FirebaseInAppMessaging.podspec
29+
run: scripts/third_party/travis/retry.sh scripts/pod_lib_lint.rb ${{ matrix.podspec}}
2730

2831
tests:
2932
# Don't run on private repo unless it is a PR.
@@ -38,9 +41,9 @@ jobs:
3841
- name: Setup Bundler
3942
run: scripts/setup_bundler.sh
4043
- name: Prereqs
41-
run: scripts/install_prereqs.sh InAppMessaging ${{ matrix.platform }} xcodebuild
44+
run: scripts/install_prereqs.sh InAppMessaging ${{ matrix.platform }} xcodebuild
4245
- name: Build and test
43-
run: scripts/third_party/travis/retry.sh scripts/build.sh InAppMessaging ${{ matrix.platform }} xcodebuild
46+
run: scripts/third_party/travis/retry.sh scripts/build.sh InAppMessaging ${{ matrix.platform }} xcodebuild
4447

4548
spm:
4649
# Don't run on private repo unless it is a PR.

FirebaseInAppMessaging/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
# 2021-3 -- v.7.9.0
2+
- [added] Added support for building custom in-app messages with SwiftUI (#7496).
3+
14
# 2021-2 -- v.7.7.0
25
- [fixed] Fixed accessibility experience for in-app messages (#7445).
36
- [fixed] Fixed conversion tracking for in-app messages with a conversion event but not a button / action URL (#7306).

FirebaseInAppMessaging/Sources/Public/FirebaseInAppMessaging/FIRInAppMessagingRendering.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,13 @@ NS_SWIFT_NAME(InAppMessagingDisplayMessage)
164164
/// Unavailable.
165165
- (instancetype)init NS_UNAVAILABLE;
166166

167+
/// Exposed for unit testing only. Don't instantiate this in your app directly.
168+
- (instancetype)initWithMessageID:(NSString *)messageID
169+
campaignName:(NSString *)campaignName
170+
renderAsTestMessage:(BOOL)renderAsTestMessage
171+
messageType:(FIRInAppMessagingDisplayMessageType)messageType
172+
triggerType:(FIRInAppMessagingDisplayTriggerType)triggerType;
173+
167174
@end
168175

169176
NS_SWIFT_NAME(InAppMessagingCardDisplay)
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
// Copyright 2021 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import FirebaseInAppMessaging
16+
import SwiftUI
17+
18+
// MARK: Image-only messages.
19+
20+
@available(iOS 13, tvOS 13, *)
21+
struct ImageOnlyInAppMessageDisplayViewModifier<DisplayMessage: View>: ViewModifier {
22+
var closure: (InAppMessagingImageOnlyDisplay, InAppMessagingDisplayDelegate) -> DisplayMessage
23+
@ObservedObject var delegateBridge = DelegateBridge.shared
24+
25+
func body(content: Content) -> some View {
26+
return content.overlay(overlayView())
27+
}
28+
29+
@ViewBuilder
30+
func overlayView() -> some View {
31+
if let (message, delegate) = delegateBridge.inAppMessageData,
32+
let imageOnlyMessage = message as? InAppMessagingImageOnlyDisplay {
33+
closure(imageOnlyMessage, delegate)
34+
.onAppear { delegate.impressionDetected?(for: imageOnlyMessage) }
35+
} else {
36+
EmptyView()
37+
}
38+
}
39+
}
40+
41+
@available(iOS 13, tvOS 13, *)
42+
public extension View {
43+
/// Overrides the default display of an image only in-app message as defined on the Firebase console.
44+
func imageOnlyInAppMessage<Content: View>(closure: @escaping (InAppMessagingImageOnlyDisplay,
45+
InAppMessagingDisplayDelegate)
46+
-> Content)
47+
-> some View {
48+
modifier(ImageOnlyInAppMessageDisplayViewModifier(closure: closure))
49+
}
50+
}
51+
52+
// MARK: Banner messages.
53+
54+
@available(iOS 13, tvOS 13, *)
55+
struct BannerInAppMessageDisplayViewModifier<DisplayMessage: View>: ViewModifier {
56+
var closure: (InAppMessagingBannerDisplay, InAppMessagingDisplayDelegate) -> DisplayMessage
57+
@ObservedObject var delegateBridge = DelegateBridge.shared
58+
59+
func body(content: Content) -> some View {
60+
return content.overlay(overlayView())
61+
}
62+
63+
@ViewBuilder
64+
func overlayView() -> some View {
65+
if let (message, delegate) = delegateBridge.inAppMessageData,
66+
let bannerMessage = message as? InAppMessagingBannerDisplay {
67+
closure(bannerMessage, delegate).onAppear { delegate.impressionDetected?(for: bannerMessage) }
68+
} else {
69+
EmptyView()
70+
}
71+
}
72+
}
73+
74+
@available(iOS 13, tvOS 13, *)
75+
public extension View {
76+
/// Overrides the default display of a banner in-app message as defined on the Firebase console.
77+
func bannerInAppMessage<Content: View>(closure: @escaping (InAppMessagingBannerDisplay,
78+
InAppMessagingDisplayDelegate)
79+
-> Content)
80+
-> some View {
81+
modifier(BannerInAppMessageDisplayViewModifier(closure: closure))
82+
}
83+
}
84+
85+
// MARK: Modal messages.
86+
87+
@available(iOS 13, tvOS 13, *)
88+
struct ModalInAppMessageDisplayViewModifier<DisplayMessage: View>: ViewModifier {
89+
var closure: (InAppMessagingModalDisplay, InAppMessagingDisplayDelegate) -> DisplayMessage
90+
@ObservedObject var delegateBridge = DelegateBridge.shared
91+
92+
func body(content: Content) -> some View {
93+
return content.overlay(overlayView())
94+
}
95+
96+
@ViewBuilder
97+
func overlayView() -> some View {
98+
if let (message, delegate) = delegateBridge.inAppMessageData,
99+
let modalMessage = message as? InAppMessagingModalDisplay {
100+
closure(modalMessage, delegate).onAppear { delegate.impressionDetected?(for: modalMessage) }
101+
} else {
102+
EmptyView()
103+
}
104+
}
105+
}
106+
107+
@available(iOS 13, tvOS 13, *)
108+
public extension View {
109+
/// Overrides the default display of a modal in-app message as defined on the Firebase console.
110+
func modalInAppMessage<Content: View>(closure: @escaping (InAppMessagingModalDisplay,
111+
InAppMessagingDisplayDelegate)
112+
-> Content)
113+
-> some View {
114+
modifier(ModalInAppMessageDisplayViewModifier(closure: closure))
115+
}
116+
}
117+
118+
// MARK: Card messages.
119+
120+
@available(iOS 13, tvOS 13, *)
121+
struct CardInAppMessageDisplayViewModifier<DisplayMessage: View>: ViewModifier {
122+
var closure: (InAppMessagingCardDisplay, InAppMessagingDisplayDelegate) -> DisplayMessage
123+
@ObservedObject var delegateBridge = DelegateBridge.shared
124+
125+
func body(content: Content) -> some View {
126+
return content.overlay(overlayView())
127+
}
128+
129+
@ViewBuilder
130+
func overlayView() -> some View {
131+
if let (message, delegate) = delegateBridge.inAppMessageData,
132+
let cardMessage = message as? InAppMessagingCardDisplay {
133+
closure(cardMessage, delegate).onAppear { delegate.impressionDetected?(for: cardMessage) }
134+
} else {
135+
EmptyView()
136+
}
137+
}
138+
}
139+
140+
@available(iOS 13, tvOS 13, *)
141+
public extension View {
142+
/// Overrides the default display of a card in-app message as defined on the Firebase console.
143+
func cardInAppMessage<Content: View>(closure: @escaping (InAppMessagingCardDisplay,
144+
InAppMessagingDisplayDelegate)
145+
-> Content)
146+
-> some View {
147+
modifier(CardInAppMessageDisplayViewModifier(closure: closure))
148+
}
149+
}
150+
151+
// MARK: Bridge to Firebase In-App Messaging SDK.
152+
153+
/**
154+
* A singleton that acts as the bridge between view modifiers for displaying custom in-app messages and the
155+
* in-app message fetch/display/interaction flow.
156+
*/
157+
@available(iOS 13, tvOS 13, *)
158+
class DelegateBridge: NSObject, InAppMessagingDisplay, InAppMessagingDisplayDelegate,
159+
ObservableObject {
160+
@Published var inAppMessageData: (InAppMessagingDisplayMessage,
161+
InAppMessagingDisplayDelegate)? = nil
162+
163+
static let shared = DelegateBridge()
164+
165+
override init() {
166+
super.init()
167+
InAppMessaging.inAppMessaging().messageDisplayComponent = self
168+
InAppMessaging.inAppMessaging().delegate = self
169+
}
170+
171+
func displayMessage(_ messageForDisplay: InAppMessagingDisplayMessage,
172+
displayDelegate: InAppMessagingDisplayDelegate) {
173+
DispatchQueue.main.async {
174+
self.inAppMessageData = (messageForDisplay, displayDelegate)
175+
}
176+
}
177+
178+
func messageClicked(_ inAppMessage: InAppMessagingDisplayMessage,
179+
with action: InAppMessagingAction) {
180+
DispatchQueue.main.async {
181+
self.inAppMessageData = nil
182+
}
183+
}
184+
185+
func messageDismissed(_ inAppMessage: InAppMessagingDisplayMessage,
186+
dismissType: FIRInAppMessagingDismissType) {
187+
DispatchQueue.main.async {
188+
self.inAppMessageData = nil
189+
}
190+
}
191+
}

0 commit comments

Comments
 (0)