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.

ModuleProductBinary sizeAddsRequires
CoreGenuinCore17.5 MBEmbeds, placements, sign-in, deep links, push notification handling, the video player, in-feed ads—
UIGenuinUI6.9 MBComments, search, notifications screen, community and group detail screens, deep-link destinationsCore
CameraGenuinCamera3.8 MBRecording and uploading video postsCore, UI
AIGenuinAI2.2 MBAI-assisted creation of communities, groups, and avatarsCore, 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
iOS15.0. Every 2.1.5 framework declares MinimumOSVersion 15.0, and the podspecs set ios.deployment_target 15.0.
Xcode26.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.
LanguageSwift. GenuinSDK, GenuinConfiguration, and the configuration classes are @objcMembers, so Objective-C code can call them.
OrientationPortrait. The SDK's navigation controller locks supportedInterfaceOrientations to .portrait.
Form factoriPhone. The frameworks declare UIDeviceFamily 1 and 2, but the example app targets iPhone only.
UI toolkitUIKit views and view controllers, or SwiftUI through UIViewRepresentable and UIViewControllerRepresentable.
Package managerSwift Package Manager or CocoaPods, both resolving from the Git repository https://github.com/genuininc/genuin_ios_sdk.
Git LFSRequired 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.

Terminal
bash
brew install git-lfs
git lfs install

Install with Swift Package Manager

  1. In Xcode, go to File > Add Package Dependencies.
  2. Enter the repository URL https://github.com/genuininc/genuin_ios_sdk.
  3. Set Dependency Rule to Exact Version and enter 2.1.5.
  4. Click Add Package, then select the products you need: GenuinCore, and optionally GenuinUI, GenuinCamera, and GenuinAI.
  5. 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:

Podfile
ruby
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
end

Modules 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:

KeyAdd it forModule
NSCameraUsageDescriptionRecording video posts, video comments, and avatar photosCamera, AI
NSMicrophoneUsageDescriptionRecording video and voice comments, recording an avatar voiceCamera, AI
NSPhotoLibraryUsageDescriptionUploading from the camera roll, saving videos, profile and banner imagesCore, Camera, AI
NSSpeechRecognitionUsageDescriptionTranscribing a recording so the user can overlay the textCamera
NSContactsUsageDescriptionAdding contacts as community and group membersUI
NSLocationWhenInUseUsageDescriptionSuggesting community details from the user's locationCore
NSUserTrackingUsageDescriptionPersonalized 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:

Info.plist
xml
<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:

AppDelegate.swift
swift
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

ParameterTypeDefaultDescription
environmentGenuinEnvironment.prod.prod or .qa. Keys aren't shared between environments, so a QA key only works with .qa.
setCustomLoader(_:bundle:)String, BundleGenuin's loader, Bundle.mainThe 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, CGFloatSDK fonts, 0.0PostScript 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:

EmbedViewController.swift
swift
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:

With GenuinEmbedAttributes
swift
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

ParameterTypeRequiredDefaultDescription
embedIdStringYes—The embed to load, from Brand Control Center.
viewControllerUIViewControllerYes—The view controller that presents full-screen playback, comments, and sign-in. Held weakly.
uniqueIdString?NonilSet a different value for each instance when you show the same embed on several screens, or more than once on one screen.
ssoTokenString?NonilSigns the viewer in with single sign-on before the feed loads.
params[String: Any]?NonilUser details used when the SSO account is created. See User parameters.
contextualParams[String: Any]?NonilSignals that rank the feed toward the current screen and viewer. See Contextual parameters.
embedConfigurationEmbedConfigurationNoEmbedConfiguration()Behavior and layout options. See the table below.

#### EmbedConfiguration

PropertyTypeDefaultDescription
isShowProfileEnabledBoolfalseWhen true, signed-in viewers see their profile picture in the top-right corner of the full-screen view, with account settings and sign-out.
isDirectDeepLinkEnabledBoolfalseWhen true, interactions in the full-screen view open the specific video in your white-labelled app instead of the SDK's own flow.
isDirectDeepLinkWithLoginEnabledBoolfalseSame as isDirectDeepLinkEnabled, and the redirect carries the viewer's sign-in. Setting it to true also sets isDirectDeepLinkEnabled.
showNavigationBarBoolfalseWhen true, the full-screen feed shows a navigation bar.
genuinCarouselConfigurationGenuinCarouselConfigurationdefaults belowTile 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.

Corner radius, insets, and tile spacing on a carousel embed.

#### User parameters

Pass these only together with an SSO token. They pre-fill the viewer's Genuin profile when their account is created.

KeyTypeDescription
nameStringDisplay name
emailStringEmail address
mobileStringPhone number
nicknameStringUsed as the viewer's nickname if it's free; otherwise the SDK generates one
profile_imageStringURL of a profile image to show in the SDK

#### Contextual parameters

KeyTypeDescription
page_contextStringWhat 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:

Load in the background
swift
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:

FeedViewController.swift
swift
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:

GenuinCarousel.swift
swift
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.

PlacementViewController.swift
swift
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.

KeyTypeDescription
page_contextStringWhat the current screen is about
previous_page_contextStringWhat the previous screen was about
user_contextStringAnything else you know about the viewer's intent
urlStringThe web URL this screen corresponds to
timeIntA 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:

Sign in
swift
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:

Sign out
swift
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:

GenuinDelegate
swift
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.

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:

  1. White-label your community so links use your domain.
  2. In the Apple Developer portal, enable Associated Domains for your app identifier under Certificates, Identifiers & Profiles.
  3. In Xcode, open your target's Signing & Capabilities tab, click + Capability, add Associated Domains, and enter applinks:{{YOUR_DOMAIN}} and applinks:www.{{YOUR_DOMAIN}}.
The Associated Domains capability with the two applinks entries for a white-labelled domain.
  1. Send your Genuin contact your Apple Team ID and bundle ID. Genuin serves apple-app-site-association on 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.
  2. 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:
SceneDelegate.swift
swift
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.

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:

Link-out interceptor
swift
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.

  1. In your target's Signing & Capabilities tab, add the Push Notifications capability, and add the FirebaseMessaging package by following Set up a Firebase Cloud Messaging client app on Apple platforms.
  2. Set the delegates in AppDelegate before launch finishes, so a tap that launches the app isn't lost, and register the FCM token with the SDK:
AppDelegate.swift
swift
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) }
    }
}
  1. When the member taps a notification, let the SDK open its own notifications and leave the rest to your app:
AppDelegate.swift
swift
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.

PropertyProtocolFires when
delegateGenuinDelegate.presentLoginViewController(baseViewController:)The viewer needs to sign in. See Step 6.
playbackDelegateGenuinPlaybackDelegate: 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
analyticsDelegateGenuinAnalyticsDelegate.onAnalyticsEvent(eventName:properties:)The SDK records an analytics event, for forwarding to your own analytics
interactionDelegateGenuinInteractionDelegate.onInteraction(info:onCompletion:)The viewer interacts with a post; call onCompletion to let the SDK continue
linkOutInterceptorDelegateGenuinLinkOutInterceptorDelegate.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:

SceneDelegate.swift
swift
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

SymptomCauseFix
The app crashes at launch with "The Google Mobile Ads SDK was initialized without an application ID"GADApplicationIdentifier is missing from Info.plistAdd 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 textGit LFS wasn't installed when the package was resolvedRun git lfs install, then reset package caches in Xcode or run pod install again.
SwiftPM can't resolve the packageYour app pins Lottie, Google IMA, or Giphy at a different versionMatch the SDK's exact versions: Lottie 4.6.0, IMA 3.22.1, Giphy 2.1.22.
An embed renders nothingThe 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 unavailableThe matching NS…UsageDescription key is missingAdd the key from Step 2.
ssoLogin completes with falseThe token is empty, the API key or environment is wrong, or your server issued an invalid tokenCheck configuration.environment against the key, and verify the token against the authentication guide.
A deep link opens in Safari instead of the appThe AASA file isn't served from /.well-known/, or the applinks entries don't match the domainVerify the file and the Associated Domains entries, then reinstall the app so iOS refetches the file.
The feed shows QA content in a production buildconfiguration.environment is .qaSet .prod, the default, and use the production key.

Limitations

  • One version for all modules: GenuinCore, GenuinUI, GenuinCamera, and GenuinAI must be the same version. Mismatches fail at runtime.
  • Minimum iOS 15: The frameworks declare MinimumOSVersion 15.0. The distribution Package.swift still 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 UIViewController to 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

TRUST AND COMPLIANCE

© 2026 Genuin Inc.

Genuin Footer