# 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 social experience inside your own app: your brand's communities, groups, and video feeds. 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`.

```bash title="Terminal"
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:

```ruby title="Podfile"
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:

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

```xml title="Info.plist"
<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](https://developers.google.com/ad-manager/mobile-ads-sdk/ios/3p-skadnetworks) 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:

```swift title="AppDelegate.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

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

```swift title="EmbedViewController.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:

```swift title="With GenuinEmbedAttributes"
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.

![Three phone screens showing a video carousel, annotated to show what tileCornerRadius, viewInset, and interTileSpacing control: the rounding of each tile, the space between the carousel and the screen edges, and the gap between tiles.](https://cdn.sanity.io/images/lnrtvsvr/development/61fcff766aeb7ea4a2f0c2b17a012d0d0cea37a1-1449x687.jpg)
*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.

| Key | Type | Description |
| --- | --- | --- |
| name | String | Display name |
| email | 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](#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:

```swift title="Load in the background"
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`:

```swift title="FeedViewController.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:

```swift title="GenuinCarousel.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`.

```swift title="PlacementViewController.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.

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

```swift title="Sign in"
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:

```swift title="Sign out"
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`:

```swift title="GenuinDelegate"
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](https://begenuin.com/developers/developer-resources/sdk/user-authentication-mechanisms-in-genuin-sdk) and [How Auto Login works](https://begenuin.com/developers/developer-resources/sdk/genuin-auto-login).

## Step 7: Handle deep links

Links to Genuin content, such as a shared video or an invitation, use your white-label domain. To open them in your app instead of the browser:

1. [White-label your social experience](https://begenuin.com/developers/browse-by-products/onsite/how-can-you-white-label-your-community-under-your-own-domain) 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 Signing & Capabilities tab of an Xcode target with the Associated Domains capability added and two applinks domain entries listed.](https://cdn.sanity.io/images/lnrtvsvr/development/2f63bd4f73ea9421748be44108c6c4688c750523-1580x1138.jpg)
   *The Associated Domains capability with the two applinks entries for a white-labelled domain.*
4. 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](https://begenuin.com/developers/developer-resources/sdk/handle-genuin-deep-links-in-your-app) shows how to check it.
5. 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:

```swift title="SceneDelegate.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.

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

```swift title="Link-out interceptor"
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](https://begenuin.com/developers/get-started/developers/genuin-sdk/creation-n-editing-tools/how-do-push-notifications-work-with-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](https://firebase.google.com/docs/cloud-messaging/ios/client).
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:

```swift title="AppDelegate.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) }
    }
}
```

3. When the member taps a notification, let the SDK open its own notifications and leave the rest to your app:

```swift title="AppDelegate.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.

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

```swift title="SceneDelegate.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](#step-2-declare-infoplist-keys).

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`, 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

- [Integrate the Genuin Android SDK](https://begenuin.com/developers/developer-resources/sdk/how-to-integrate-and-power-your-brand-community-in-android-apps)
- [Choose how members sign in to the Genuin SDK](https://begenuin.com/developers/developer-resources/sdk/user-authentication-mechanisms-in-genuin-sdk)
- [How Auto Login works](https://begenuin.com/developers/developer-resources/sdk/genuin-auto-login)
- [Integrate the React Native SDK](https://begenuin.com/developers/developer-resources/sdk/how-do-you-integrate-the-genuin-react-native-sdk-into-your-application)
- [Integrate the Genuin SDK with Flutter apps](https://begenuin.com/developers/developer-resources/sdk/how-do-you-integrate-the-genuin-flutter-sdk-into-your-application)
- [Add IAB Standard Size Experiences to an iOS app](https://begenuin.com/developers/developer-resources/sdk/integrate-the-genuin-ad-sdk-for-ios)
- [Customize the Genuin video player](https://begenuin.com/developers/browse-by-products/control/how-does-a-brand-customize-the-genuin-video-player-controls)
- [Handle Genuin deep links in your app](https://begenuin.com/developers/developer-resources/sdk/handle-genuin-deep-links-in-your-app)
- [Set up push notifications in the Genuin SDK](https://begenuin.com/developers/get-started/developers/genuin-sdk/creation-n-editing-tools/how-do-push-notifications-work-with-the-genuin-sdk)
