Integrate the Genuin iOS SDK
Add Genuin embeds, placements, single sign-on, deep links, and push notifications to an iOS app with SDK 2.1.5.
The Genuin iOS SDK renders your brand's communities, groups, and video feeds inside your own app. You add one or more of four modules, initialize the SDK with your API key, and place a view where the content goes. Everything the view shows, from which community to the carousel or grid layout, is configured in Brand Control Center and identified by an embed ID or placement ID.
| Module | Product | Binary size | Adds | Requires |
|---|---|---|---|---|
| Core | GenuinCore | 17.5 MB | Embeds, placements, sign-in, deep links, push notification handling, the video player, in-feed ads | — |
| UI | GenuinUI | 6.9 MB | Comments, search, notifications screen, community and group detail screens, deep-link destinations | Core |
| Camera | GenuinCamera | 3.8 MB | Recording and uploading video posts | Core, UI |
| AI | GenuinAI | 2.2 MB | AI-assisted creation of communities, groups, and avatars | Core, UI, Camera |
Sizes are the arm64 device binaries inside the 2.1.5 xcframeworks, before App Store compression. Core also ships the Google Mobile Ads SDK 13.3.0 (13.7 MB) and the Pangle SDK 8.2.1.0; Camera ships ffmpeg-kit 6.0 (16.9 MB across eight libraries) and the Giphy SDK. Add only the modules you use, and keep them on the same version.
Requirements
| Minimum | |
|---|---|
| iOS | 15.0. Every 2.1.5 framework declares MinimumOSVersion 15.0, and the podspecs set ios.deployment_target 15.0. |
| Xcode | 26.6, the version the 2.1.5 frameworks were built with (Swift 6.3.3, library evolution enabled). Older Xcode versions aren't tested against these binaries. |
| Language | Swift. GenuinSDK, GenuinConfiguration, and the configuration classes are @objcMembers, so Objective-C code can call them. |
| Orientation | Portrait. The SDK's navigation controller locks supportedInterfaceOrientations to .portrait. |
| Form factor | iPhone. The frameworks declare UIDeviceFamily 1 and 2, but the example app targets iPhone only. |
| UI toolkit | UIKit views and view controllers, or SwiftUI through UIViewRepresentable and UIViewControllerRepresentable. |
| Package manager | Swift Package Manager or CocoaPods, both resolving from the Git repository https://github.com/genuininc/genuin_ios_sdk. |
| Git LFS | Required on every machine that resolves the package. The Pangle binaries are stored with Git LFS. |
You also need an API key for your brand and at least one embed ID or placement ID. Both come from Brand Control Center; the key is under Build > Publish Brand Community > SDK Integration. The key is the same one the Web and Android SDKs use, and the QA environment has its own key.
Step 1: Add the SDK
Both package managers clone the distribution repository, so install Git LFS first. Without it, the two PAGAdSDK binaries arrive as 130-byte pointer files and the link step fails with ld: framework not found PAGAdSDK.
brew install git-lfs
git lfs installInstall with Swift Package Manager
- In Xcode, go to File > Add Package Dependencies.
- Enter the repository URL
https://github.com/genuininc/genuin_ios_sdk. - Set Dependency Rule to Exact Version and enter
2.1.5. - Click Add Package, then select the products you need:
GenuinCore, and optionallyGenuinUI,GenuinCamera, andGenuinAI. - Assign each product to your app target and click Add Package.
The package pins three dependencies to exact versions: lottie-spm 4.6.0, swift-package-manager-google-interactive-media-ads-ios 3.22.1, and giphy-ios-sdk 2.1.22. If your app already declares one of them at another version, SwiftPM fails to resolve until the versions match.
Install with CocoaPods
The pods aren't published to the CocoaPods trunk at 2.x. Point each pod at the Git repository and tag:
platform :ios, '15.0'
target 'YourApp' do
use_frameworks!
genuin_sdk_version = '2.1.5'
pod 'GenuinCore', :git => 'https://github.com/genuininc/genuin_ios_sdk.git', :tag => genuin_sdk_version
pod 'GenuinUI', :git => 'https://github.com/genuininc/genuin_ios_sdk.git', :tag => genuin_sdk_version
pod 'GenuinCamera', :git => 'https://github.com/genuininc/genuin_ios_sdk.git', :tag => genuin_sdk_version
pod 'GenuinAI', :git => 'https://github.com/genuininc/genuin_ios_sdk.git', :tag => genuin_sdk_version
end
post_install do |installer|
installer.pods_project.targets.each do |target|
if target.name == 'Giphy'
`xcrun -sdk iphoneos bitcode_strip -r Pods/Giphy/GiphySDK/GiphyUISDK.xcframework/ios-arm64_armv7/GiphyUISDK.framework/GiphyUISDK -o Pods/Giphy/GiphySDK/GiphyUISDK.xcframework/ios-arm64_armv7/GiphyUISDK.framework/GiphyUISDK`
end
target.build_configurations.each do |config|
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '15.0'
config.build_settings['BUILD_LIBRARY_FOR_DISTRIBUTION'] = 'YES'
end
end
endModules depend on each other in a fixed chain (GenuinAI → GenuinCamera → GenuinUI → GenuinCore), so an app that uses the AI module declares all four. The post_install block strips bitcode from the Giphy framework, which the Camera module pulls in, and keeps every pod on the same deployment target.
Note: Mixing module versions fails at runtime, not at build time. Pin them to one variable, as the Podfile above does.
Step 2: Declare Info.plist keys
The SDK reads six usage-description keys from your Info.plist before it opens a feature. If a key is missing, the SDK shows a "feature unavailable" alert instead of requesting the permission. Add the keys for the features you ship:
| Key | Add it for | Module |
|---|---|---|
| NSCameraUsageDescription | Recording video posts, video comments, and avatar photos | Camera, AI |
| NSMicrophoneUsageDescription | Recording video and voice comments, recording an avatar voice | Camera, AI |
| NSPhotoLibraryUsageDescription | Uploading from the camera roll, saving videos, profile and banner images | Core, Camera, AI |
| NSSpeechRecognitionUsageDescription | Transcribing a recording so the user can overlay the text | Camera |
| NSContactsUsageDescription | Adding contacts as community and group members | UI |
| NSLocationWhenInUseUsageDescription | Suggesting community details from the user's location | Core |
| NSUserTrackingUsageDescription | Personalized ads through App Tracking Transparency. See Step 10. | Core |
Core bundles the Google Mobile Ads SDK, which checks for GADApplicationIdentifier at launch and crashes the app if the key is missing, even if your brand never shows an ad. Add it together with the SKAdNetworkItems entries for Google and Pangle:
<key>GADApplicationIdentifier</key>
<string>ca-app-pub-{{PUBLISHER_ID}}~{{APP_ID}}</string>
<key>SKAdNetworkItems</key>
<array>
<dict><key>SKAdNetworkIdentifier</key><string>cstr6suwn9.skadnetwork</string></dict>
<dict><key>SKAdNetworkIdentifier</key><string>238da6jt44.skadnetwork</string></dict>
<dict><key>SKAdNetworkIdentifier</key><string>22mmun2rn5.skadnetwork</string></dict>
</array>
<key>NSUserTrackingUsageDescription</key>
<string>This identifier will be used to deliver personalized ads to you.</string>
<key>NSCameraUsageDescription</key>
<string>Genuin needs your camera and microphone to record videos you can share with other people.</string>
<key>NSMicrophoneUsageDescription</key>
<string>Genuin needs your camera and microphone to record videos you can share with other people.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Genuin needs your photo library to upload videos and save them to your phone.</string>cstr6suwn9.skadnetwork is Google's network ID; 238da6jt44.skadnetwork and 22mmun2rn5.skadnetwork are Pangle's. Keep any SKAdNetwork IDs you already have, and add the third-party buyer IDs your Ad Manager account uses. The GADApplicationIdentifier comes from the Ad Manager UI; if you don't run your own Ad Manager account, ask your Genuin contact for the ID to use.
Step 3: Initialize the SDK
Call GenuinSDK.shared.initialize once at launch, before any Genuin view is created. If you added GenuinUI, GenuinCamera, or GenuinAI, call each module's initialize() first so it registers with Core:
import GenuinCore
import GenuinUI
import GenuinCamera
import GenuinAI
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
let configuration = GenuinConfiguration.shared
configuration.environment = .prod // default; use .qa only with a QA key
configuration.setCustomLoader("{{LOTTIE_FILE_NAME}}", bundle: .main) // optional
GenuinUI.shared.initialize() // only if GenuinUI is integrated
GenuinCamera.shared.initialize() // only if GenuinCamera is integrated
GenuinAI.shared.initialize() // only if GenuinAI is integrated
GenuinSDK.shared.initialize(apiKey: "<<YOUR_API_KEY>>", configuration: configuration) { error in
// error is nil once the brand details have loaded
}
return true
}In a scene-based app you can make the same calls in scene(_:willConnectTo:options:) of your SceneDelegate, and in a SwiftUI app in the App initializer. The SDK has no scene-connection method of its own. initialize fetches your brand's details and sizes the image cache (100 MB in memory, 500 MB on disk).
Configuration parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
| environment | GenuinEnvironment | .prod | .prod or .qa. Keys aren't shared between environments, so a QA key only works with .qa. |
| setCustomLoader(_:bundle:) | String, Bundle | Genuin's loader, Bundle.main | The name of a Lottie JSON animation in the given bundle, shown while content loads. |
| setFontsConfig(boldFontName:boldItalicFontName:regularFontName:heavyItalicFontName:demiBoldFontName:demiBoldItalicFontName:mediumFontName:fontIncrementValue:) | String × 7, CGFloat | SDK fonts, 0.0 | PostScript names of fonts already registered in your app, and a point size added to every text style. |
Note: Don't ship a build that points at QA. The two environments have separate brands, users, and content.
Step 4: Show an embed
An embed is a feed of videos from a community, group, or your whole brand, configured in Brand Control Center. It renders as a carousel, grid, full-screen feed, or standard wall. The layout comes from the embed's configuration, so the same code shows whatever the embed is set to.
For a carousel or grid inside your own screen, use GenuinEmbedView. For a full-screen feed or a standard wall that takes over a screen, use initializeEmbedFeed or initializeStandardWall.
Add a GenuinEmbedView
Give the container view a fixed height, then let the SDK create the view inside it:
import GenuinCore
let embedConfiguration = EmbedConfiguration()
embedConfiguration.isShowProfileEnabled = false
embedConfiguration.isDirectDeepLinkEnabled = false
GenuinSDK.shared.initializeEmbed(
embedId: "{{EMBED_ID}}",
uniqueId: "{{UNIQUE_ID}}", // optional
containerView: carouselContainer, // a UIView with a fixed height
viewConroller: self, // the API spells the label this way
ssoToken: "{{SSO_TOKEN}}", // optional: signs the viewer in
params: ["name": "John Doe", "email": "john.doe@example.com", "nickname": "john"],
contextualParams: ["page_context": "{{PAGE_CONTEXT}}", "geo": ["lat": 40.7128, "long": -74.0060]],
embedConfiguration: embedConfiguration
)initializeEmbed removes any GenuinEmbedView already in the container, builds a GenuinEmbedAttributes object from the arguments, adds the view, and calls fetchDataAndLoad(). To hold a reference to the view yourself, create the attributes and the view directly:
let attributes = GenuinEmbedAttributes(embedId: "{{EMBED_ID}}", viewController: self)
let embedView = GenuinEmbedView(attributes: attributes) // or GenuinEmbedView() + initWith(attributes:)
carouselContainer.addSubview(embedView)
embedView.fetchDataAndLoad()To place the view in a storyboard, drag a UIView into a fixed-height container. In the Identity Inspector, set its class to GenuinEmbedView and its module to GenuinCore. Connect an @IBOutlet, then call initWith(attributes:) and fetchDataAndLoad() on the outlet.
#### Embed attributes
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| embedId | String | Yes | — | The embed to load, from Brand Control Center. |
| viewController | UIViewController | Yes | — | The view controller that presents full-screen playback, comments, and sign-in. Held weakly. |
| uniqueId | String? | No | nil | Set a different value for each instance when you show the same embed on several screens, or more than once on one screen. |
| ssoToken | String? | No | nil | Signs the viewer in with single sign-on before the feed loads. |
| params | [String: Any]? | No | nil | User details used when the SSO account is created. See User parameters. |
| contextualParams | [String: Any]? | No | nil | Signals that rank the feed toward the current screen and viewer. See Contextual parameters. |
| embedConfiguration | EmbedConfiguration | No | EmbedConfiguration() | Behavior and layout options. See the table below. |
#### EmbedConfiguration
| Property | Type | Default | Description |
|---|---|---|---|
| isShowProfileEnabled | Bool | false | When true, signed-in viewers see their profile picture in the top-right corner of the full-screen view, with account settings and sign-out. |
| isDirectDeepLinkEnabled | Bool | false | When true, interactions in the full-screen view open the specific video in your white-labelled app instead of the SDK's own flow. |
| isDirectDeepLinkWithLoginEnabled | Bool | false | Same as isDirectDeepLinkEnabled, and the redirect carries the viewer's sign-in. Setting it to true also sets isDirectDeepLinkEnabled. |
| showNavigationBar | Bool | false | When true, the full-screen feed shows a navigation bar. |
| genuinCarouselConfiguration | GenuinCarouselConfiguration | defaults below | Tile layout for carousel and grid embeds. |
GenuinCarouselConfiguration has three properties: interTileSpacing (default 8.0 points), viewInset (default UIEdgeInsets(top: 8, left: 16, bottom: 0, right: 16)), and tileCornerRadius (default 8.0).
Note: Coming from the 1.x GenuinSDK pod? Its EmbedConfiguration had interactionDeepLink, and its carousel configuration had aspectRatio, scrollDirection, and carouselInset. None of these exist in 2.x. Use isDirectDeepLinkEnabled for the redirect and viewInset for the inset; tile aspect ratio and scroll direction now come from the embed's layout in Brand Control Center.
#### User parameters
Pass these only together with an SSO token. They pre-fill the viewer's Genuin profile when their account is created.
| Key | Type | Description |
|---|---|---|
| name | String | Display name |
| String | Email address | |
| mobile | String | Phone number |
| nickname | String | Used as the viewer's nickname if it's free; otherwise the SDK generates one |
| profile_image | String | URL of a profile image to show in the SDK |
#### Contextual parameters
| Key | Type | Description |
|---|---|---|
| page_context | String | What the current screen is about. The feed is ranked toward it. |
| geo | [String: Any] | lat and long as Double, both required together. If you omit geo, the SDK adds its own location information to the request. |
Placements accept a longer list. See Contextual parameters for placements.
Load in the background
fetchDataAndLoad() shows the view immediately with a skeleton while data arrives. To keep the view out of the hierarchy until the feed is ready, fetch first and add the view in the completion:
let embedView = GenuinEmbedView()
embedView.initWith(attributes: GenuinEmbedAttributes(embedId: "{{EMBED_ID}}", viewController: self))
embedView.fetchDataInBackground { success in
guard success else { return } // false: unknown embed, wrong key, or no network
self.carouselContainer.addSubview(embedView)
embedView.load()
}To warm the cache before a screen appears, call GenuinSDK.shared.loadEmbedData(embedId:uniqueId:contextualParams:); a later initializeEmbed for the same embed and unique ID reuses the result.
Control playback
Videos autoplay. The view exposes play(), pause(), mute(), and unmute(), so pause it when your screen leaves the foreground and resume it in viewWillAppear. Calling pause() from viewWillDisappear stops audio from continuing on the previous screen.
Show a full-screen feed or standard wall
Both take a container view that fills the screen, and the embed's configuration decides which layout appears. The parameters are the same as initializeEmbed:
override func viewDidLoad() {
super.viewDidLoad()
let embedConfiguration = EmbedConfiguration()
embedConfiguration.isShowProfileEnabled = true
GenuinSDK.shared.initializeStandardWall( // or initializeEmbedFeed for a full-screen feed
embedId: "{{EMBED_ID}}",
containerView: view,
viewConroller: self,
embedConfiguration: embedConfiguration
)
}To push a standard wall from your own navigation, GenuinSDK.shared.getStandardWallController(embedId:uniqueId:ssoToken:params:contextualParams:embedConfiguration:) returns a UINavigationController that you present or embed yourself. It returns nil when the controller can't be created.
Use SwiftUI
Wrap the carousel in a UIViewControllerRepresentable, because the SDK needs a UIViewController to present from:
import SwiftUI
import GenuinCore
struct GenuinCarousel: UIViewControllerRepresentable {
let embedId: String
func makeUIViewController(context: Context) -> UIViewController {
let host = UIViewController()
GenuinSDK.shared.initializeEmbed(embedId: embedId, containerView: host.view, viewConroller: host)
return host
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
}
struct ContentView: View {
var body: some View {
GenuinCarousel(embedId: "{{EMBED_ID}}")
.frame(height: 320) // the container must have a fixed height
}
}For a standard wall, return GenuinSDK.shared.getStandardWallController(embedId:) ?? UINavigationController() from makeUIViewController and apply .ignoresSafeArea(edges: .top).
Step 5: Show a placement
A placement is an embed whose content is chosen at render time from the context you pass. That context is the page, the viewer's location and segment, and the brands or users to draw from. It needs a placement ID and a style ID, both from Brand Control Center. It renders through GenuinPlacementView, which has the same loading and playback methods as GenuinEmbedView.
let placementConfiguration = PlacementConfiguration()
GenuinSDK.shared.initializePlacement(
placementId: "{{PLACEMENT_ID}}",
styleId: "{{STYLE_ID}}",
uniqueId: "{{UNIQUE_ID}}", // optional
containerView: placementContainer,
viewController: self,
ssoToken: "{{SSO_TOKEN}}", // optional
contextualParams: [
"page_context": "{{PAGE_CONTEXT}}",
"geo": ["lat": 40.7128, "long": -74.0060, "radius_limit": 10],
"user_segments": ["age": 33, "gender": "F"]
],
placementConfiguration: placementConfiguration
)You can also build a GenuinPlacementAttributes(placementId:styleId:uniqueId:viewController:ssoToken:params:contextualParams:placementConfiguration:) object and pass it to GenuinPlacementView(attributes:) or initWith(attributes:), then call fetchDataAndLoad() or fetchDataInBackground(completion:). In a storyboard, set the view's class to GenuinPlacementView and its module to GenuinCore.
PlacementConfiguration has the same isShowProfileEnabled, isDirectDeepLinkEnabled, isDirectDeepLinkWithLoginEnabled, and showNavigationBar properties as EmbedConfiguration, plus a tileConfiguration of type GenuinTileConfiguration with the same three layout values.
Contextual parameters for placements
Every key is optional. Nested values are dictionaries and arrays, not JSON strings.
| Key | Type | Description |
|---|---|---|
| page_context | String | What the current screen is about |
| previous_page_context | String | What the previous screen was about |
| user_context | String | Anything else you know about the viewer's intent |
| url | String | The web URL this screen corresponds to |
| time | Int | A Unix timestamp to rank against scheduled content |
| geo | [String: Any] | lat, long (Double), and radius_limit (Int) |
| place | [String: Any] | country (code), state, city, zipcode |
| user_segments | [String: Any] | age, min_age, max_age, segment, gender (M, F, or O), race |
| user_interests | [String] | Interests to rank toward |
| brands_ids | [Int] | Restrict to these brands |
| posted_by_user_ids | [String] | Restrict to posts by these users |
The _ids keys scope the feed; the others only change its order. Which signals a placement honours is set per placement under Grow > Placement > Implement in Brand Control Center.
Step 6: Sign users in
Viewers can use embeds without an account. To let them post, comment, or follow as themselves, sign them in with single sign-on. Your server issues an SSO token for the user, and the SDK creates or matches their Genuin account from it. Genuin's own login screens never appear.
Sign in with an SSO token
Passing ssoToken to an embed or placement signs the viewer in for that view. To sign them in once for the whole app, call ssoLogin after your own authentication completes:
GenuinSDK.shared.ssoLogin(
ssoToken: "{{SSO_TOKEN}}",
params: ["name": "John Doe", "nickname": "john", "mobile": "{{PHONE_NUMBER}}"] // optional
) { success in
// false when the token is empty, the brand lookup fails, or the token is rejected
}Calling ssoLogin again with the same token completes with true without a network round trip. A different token clears the previous session first.
Sign the user out of the SDK when they sign out of your app:
GenuinSDK.shared.ssoLogout { success in }Let the SDK ask your app to sign in
If the viewer isn't signed in and taps an action that needs an account, the SDK calls your GenuinDelegate instead of showing its own login. Present your sign-in flow on the view controller the SDK gives you, and finish with ssoLogin:
GenuinSDK.shared.delegate = self
extension AppDelegate: GenuinDelegate {
func presentLoginViewController(baseViewController: UIViewController) {
let loginVC = YourLoginViewController()
loginVC.onSignedIn = { token in
GenuinSDK.shared.ssoLogin(ssoToken: token)
}
baseViewController.present(loginVC, animated: true)
}
}Without a delegate, the SDK uses its own sign-in screens.
For how tokens are issued and validated, see Choose how members sign in to the Genuin SDK and How Auto Login works.
Step 7: Handle deep links
Links to Genuin content, such as a shared video or an invitation, use your white-labelled community domain. To open them in your app instead of the browser:
- White-label your community so links use your domain.
- In the Apple Developer portal, enable Associated Domains for your app identifier under Certificates, Identifiers & Profiles.
- In Xcode, open your target's Signing & Capabilities tab, click + Capability, add Associated Domains, and enter
applinks:{{YOUR_DOMAIN}}andapplinks:www.{{YOUR_DOMAIN}}.
- Send your Genuin contact your Apple Team ID and bundle ID. Genuin serves
apple-app-site-associationon your white-labelled domain from your brand's configuration, so you don't host it. Handle Genuin deep links in your app shows how to check it. - When your app receives a universal link, ask the SDK whether it's a Genuin link, then hand it over with the view controller the content opens from:
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard scene is UIWindowScene else { return }
handleGenuinDeepLink(connectionOptions.userActivities.first?.webpageURL)
}
func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
handleGenuinDeepLink(userActivity.webpageURL)
}
private func handleGenuinDeepLink(_ url: URL?) {
guard let url, GenuinSDK.shared.willHandleDeepLink(url: url),
let rootVC = window?.rootViewController else { return }
DispatchQueue.main.async {
GenuinSDK.shared.handleDeeplink(viewController: rootVC)
}
}In an AppDelegate-only app, make the same calls from application(_:continue:restorationHandler:). willHandleDeepLink returns false for URLs the SDK doesn't recognize, so your own links are unaffected. It also stores the link, so you can defer handleDeeplink(viewController:) until your home screen has loaded, or call it from that screen's viewDidAppear. handleDeeplink(completion:) only resolves and stores the destination without presenting it, so use the view-controller version to open content.
Intercept link-outs
Videos can carry link-outs to product pages. When a link-out points at a domain your app handles, the SDK gives you the URL instead of opening the browser:
GenuinSDK.shared.registerLinkoutInterceptor(
associateDomains: ["{{YOUR_DOMAIN}}", "www.{{YOUR_DOMAIN}}"],
linkOutInterceptorDelegate: self
)
extension AppDelegate: GenuinLinkOutInterceptorDelegate {
func onLinkOutIntercept(urlString: String, viewController: UIViewController?) {
// Route the URL inside your app, presenting from viewController
}
}Domains are compared in lowercase. The SDK keeps only a weak reference to the delegate, so hold it in an object that lives as long as the app.
Step 8: Handle push notifications
Genuin sends notifications for replies, mentions, and community activity through Firebase Cloud Messaging (FCM), so the token you register has to be an FCM token. Genuin also needs your Firebase project's credentials to send anything. Set up push notifications in the Genuin SDK covers that step, foreground presentation, the payload, and every notification type.
- In your target's Signing & Capabilities tab, add the Push Notifications capability, and add the
FirebaseMessagingpackage by following Set up a Firebase Cloud Messaging client app on Apple platforms. - Set the delegates in
AppDelegatebefore launch finishes, so a tap that launches the app isn't lost, and register the FCM token with the SDK:
import FirebaseCore
import FirebaseMessaging
import GenuinCore
// In application(_:didFinishLaunchingWithOptions:), after GenuinSDK.shared.initialize(...):
FirebaseApp.configure()
Messaging.messaging().delegate = self
UNUserNotificationCenter.current().delegate = self
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { _, _ in }
application.registerForRemoteNotifications()
func application(_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
Messaging.messaging().apnsToken = deviceToken
}
extension AppDelegate: MessagingDelegate {
func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) {
if let fcmToken { GenuinSDK.shared.registerFCMToken(token: fcmToken) }
}
}- When the member taps a notification, let the SDK open its own notifications and leave the rest to your app:
extension AppDelegate: UNUserNotificationCenterDelegate {
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void) {
let userInfo = response.notification.request.content.userInfo
Messaging.messaging().appDidReceiveMessage(userInfo)
if GenuinSDK.shared.willHandleNotification(userInfo: userInfo),
let rootVC = UIApplication.shared.connectedScenes
.compactMap({ ($0 as? UIWindowScene)?.keyWindow }).first?.rootViewController {
GenuinSDK.shared.handleNotifications(viewController: rootVC)
}
completionHandler()
}
}willHandleNotification returns true only for payloads whose type is one of the SDK's notification types, and stores the payload. handleNotifications(viewController:) then presents the destination. Call completionHandler() yourself, because the SDK never calls it. registerFCMToken sends the token only while a member is signed in, and sends the stored token when a member signs in later.
Caution: Don't call handleNotifications from willPresent. It opens the destination as soon as the notification arrives, without a tap.
Step 9: Listen to SDK delegates (Optional)
Set these on GenuinSDK.shared to react to what happens inside Genuin views. All are held weakly.
| Property | Protocol | Fires when |
|---|---|---|
| delegate | GenuinDelegate.presentLoginViewController(baseViewController:) | The viewer needs to sign in. See Step 6. |
| playbackDelegate | GenuinPlaybackDelegate: onPlay(isMuted:fromFullScreen:), onPause(fromFullScreen:), and optional onPlaybackError(error:), onAudioStateChanged(isMuted:isPlaying:fromFullScreen:), onFullScreenOpened(isMuted:), onFullScreenClosed(isMuted:isPlaying:) | Playback or full-screen state changes, for pausing your own media or adjusting your UI |
| analyticsDelegate | GenuinAnalyticsDelegate.onAnalyticsEvent(eventName:properties:) | The SDK records an analytics event, for forwarding to your own analytics |
| interactionDelegate | GenuinInteractionDelegate.onInteraction(info:onCompletion:) | The viewer interacts with a post; call onCompletion to let the SDK continue |
| linkOutInterceptorDelegate | GenuinLinkOutInterceptorDelegate.onLinkOutIntercept(urlString:viewController:) | A link-out targets a domain you registered. See Intercept link-outs. |
GenuinSDK.shared.clearNavigationStack() dismisses every screen the SDK has presented, which is useful before you switch brands or users.
Step 10: Enable monetization (Optional)
If your brand runs ads in embeds, request tracking authorization so the ad SDKs can read the advertising identifier. Without it the identifier is zeroed and personalized ads don't serve. The SDK reads the authorization status but never shows the prompt itself, so request it when the scene becomes active:
import AppTrackingTransparency
func sceneDidBecomeActive(_ scene: UIScene) {
DispatchQueue.main.async {
ATTrackingManager.requestTrackingAuthorization { _ in }
}
}In an AppDelegate-only app, make the call from applicationDidBecomeActive(_:). The prompt shows the NSUserTrackingUsageDescription text from Step 2.
Then declare the data use in App Store Connect. Open the app and go to App Privacy > Data Collection, then select Advertising Data under Usage Data. Choose the purposes Third-Party Advertising, Analytics, Product Personalization, and App Functionality, and click Save. Builds that collect advertising data without the declaration are rejected at review.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| The app crashes at launch with "The Google Mobile Ads SDK was initialized without an application ID" | GADApplicationIdentifier is missing from Info.plist | Add the key with your Ad Manager app ID, or the ID your Genuin contact provides. |
| Linking fails with ld: framework not found PAGAdSDK, or the PAGAdSDK file is about 130 bytes of text | Git LFS wasn't installed when the package was resolved | Run git lfs install, then reset package caches in Xcode or run pod install again. |
| SwiftPM can't resolve the package | Your app pins Lottie, Google IMA, or Giphy at a different version | Match the SDK's exact versions: Lottie 4.6.0, IMA 3.22.1, Giphy 2.1.22. |
| An embed renders nothing | The container has no fixed height, or fetchDataAndLoad was called before initWith(attributes:) | Give the container a height constraint and set attributes first; a missing attributes object stops with a fatal error. |
| A feature opens an alert saying it's unavailable | The matching NS…UsageDescription key is missing | Add the key from Step 2. |
| ssoLogin completes with false | The token is empty, the API key or environment is wrong, or your server issued an invalid token | Check configuration.environment against the key, and verify the token against the authentication guide. |
| A deep link opens in Safari instead of the app | The AASA file isn't served from /.well-known/, or the applinks entries don't match the domain | Verify the file and the Associated Domains entries, then reinstall the app so iOS refetches the file. |
| The feed shows QA content in a production build | configuration.environment is .qa | Set .prod, the default, and use the production key. |
Limitations
- One version for all modules:
GenuinCore,GenuinUI,GenuinCamera, andGenuinAImust be the same version. Mismatches fail at runtime. - Minimum iOS 15: The frameworks declare
MinimumOSVersion15.0. The distributionPackage.swiftstill says iOS 13, so a lower deployment target builds but the app fails to load the frameworks on iOS 13 and 14. - Portrait only: The SDK's screens lock to portrait and are laid out for iPhone.
- Fixed-height containers: Embed and placement views render nothing in a container without a height.
- View controller required: Every view needs a
UIViewControllerto present from, so SwiftUI hosts wrap one. - Pinned dependencies: The package requires Lottie 4.6.0, Google IMA 3.22.1, and Giphy 2.1.22 exactly.
- Bundled ad SDKs: Core ships Google Mobile Ads 13.3.0 and Pangle 8.2.1.0 whether or not your brand runs ads, and the app crashes at launch without
GADApplicationIdentifier. - One Pangle app ID per process: The Pangle SDK starts with the first app ID a feed delivers and refuses entries that carry a different one.
- Git LFS: Resolving the package without Git LFS produces pointer files instead of the Pangle binaries.
See also
- Integrate the Genuin Android SDK
- Choose how members sign in to the Genuin SDK
- How Auto Login works
- Integrate the React Native SDK
- Integrate the Genuin SDK with Flutter apps
- Add iAB Standard Size Experiences to an iOS app
- Customize the Genuin video player
- Handle Genuin deep links in your app
- Set up push notifications in the Genuin SDK

