How to integrate UniConsent CMP SDK for iOS with mobile apps

UniConsent CMP is a package for handling GDPR IAB TCF 2.3 consent management in iOS. You can find a demo app integrated with UniConsent CMP in the "UniConsentDemo" directory.

Prerequisites

  • UniConsent CMP plan with Mobile app support
  • iOS >= 15.0
  • UniConsent CMP SDK package (request from support)

Getting started

Add UniConsent.xcframework into your project. In your target's General > Frameworks, Libraries, and Embedded Content, set UniConsent.xcframework to Embed & Sign.

Before integrating the SDK, customize the consent banner appearance in the UniConsent Dashboard to match your brand and optimize consent rates.

Step 1: Brand Styling in Dashboard

Go to Projects → Select your Project → Settings → Step 5: UI & Style Settings to configure:

  • Main Button Colour — Set the primary action button color to match your brand
  • Main Button Text Colour — Adjust text color on the primary button for readability
  • Background Colour — Set the banner background color to blend with your app
  • Text Colour — Ensure body text has appropriate contrast

Step 2: Advanced Styling with Custom CSS (Optional)

For more precise control, add custom CSS in the CSS Content field under Step 5. This is recommended to make the banner feel native to your app and achieve the best consent rate:

/* Example: Style the accept button to match your brand */
.unic-btn-accept {
  background-color: #4CAF50;
  border-radius: 8px;
  font-weight: 600;
}

/* Example: Adjust banner font */
.unic-banner {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}

/* Example: Make the reject button less prominent */
.unic-btn-reject {
  background-color: transparent;
  border: 1px solid #ccc;
  color: #666;
}

Tip: A well-branded consent UI that feels native to your app typically achieves higher consent rates. Users are more likely to engage positively with a banner that matches the look and feel they expect.

Usage

To use the UniConsent CMP in your app, follow these steps:

Initialize the CMP with an App ID from your account manager:

import UniConsent

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    // Init CMP with appId
    UniConsentCMP.shared.initialize(apiId: "YOUR_APP_ID_CHANGE_THIS")
    return true
}

Display the CMP UI:

// Display CMP as full-screen modal (default)
UniConsentCMP.shared.setUIStage(.GDPRFirstScreen)
UniConsentCMP.shared.launchCMP(rootVC: self)

// Display CMP as a modal bottom sheet
UniConsentCMP.shared.launchCMP(rootVC: self, displayMode: .modalSheet)

// Display CMP as a modal bottom sheet at 60% of the available height
UniConsentCMP.shared.launchCMP(rootVC: self, displayMode: .modalSheet, heightRatio: 0.6)

// Display CMP as a center dialog
UniConsentCMP.shared.launchCMP(rootVC: self, displayMode: .dialog)

// Display CMP as a center dialog at 80% width and 60% height
UniConsentCMP.shared.launchCMP(rootVC: self, displayMode: .dialog, widthRatio: 0.8, heightRatio: 0.6)

Available display modes (CMPDisplayMode):

  • .fullScreen — Full-screen modal presentation (default)
  • .modalSheet — Bottom sheet with drag handle (iOS 15+)
  • .dialog — Centered dialog with dimmed background

Custom size (optional)

Available from SDK version 26.8.0.

Both ratios are optional — every display mode works without them. Pass a ratio (a fraction of the screen, clamped to 0.1...1.0) only to override the default size:

ParameterApplies toDefault when omitted
heightRatio.modalSheet, .dialogSheet: 90% of the available height. Dialog: 70% of the screen
widthRatio.dialog only90% of the screen width, up to 500pt

Ratios that don't apply to the chosen display mode are ignored (.fullScreen ignores both). On iOS 16+ the sheet uses the exact heightRatio; iOS 15 falls back to the system medium detent (ratio ≤ 0.5) or large detent (ratio > 0.5).

Event Handler

Listen for SDK lifecycle events with CMPEventHandler. This is the recommended way to know when the SDK has finished initializing, when the UI is displayed, and when it is closed.

Available events (CMPEventType):

  • .sdkInit — SDK initialization started
  • .ready — SDK initialization completed (GEO, config, and vendors loaded)
  • .uiDisplay — Consent UI is displayed
  • .uiClose — Consent UI is closed

Subscribe to events before calling initialize() to receive all events:

import UniConsent

@main
class AppDelegate: UIResponder, UIApplicationDelegate, CMPEventHandler {

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        UniConsentCMP.shared.subscribe(self)
        UniConsentCMP.shared.initialize(apiId: "YOUR_APP_ID")
        return true
    }

    func handle(event: CMPEvent) {
        switch event.type {
        case .ready:
            // SDK is ready — check consent and launch CMP if needed
            if UniConsentCMP.shared.shouldRequestConsent() {
                // launch CMP from your root view controller
            }
        case .uiClose:
            // User finished interacting with consent UI
            print("TC String:", UniConsentCMP.shared.getTCString())
        default:
            break
        }
    }
}

You can also subscribe from a view controller:

class ViewController: UIViewController, CMPEventHandler {

    override func viewDidLoad() {
        super.viewDidLoad()
        UniConsentCMP.shared.subscribe(self)
    }

    func handle(event: CMPEvent) {
        switch event.type {
        case .uiDisplay:
            print("CMP UI is now visible")
        case .uiClose:
            print("Consent given, vendor 1 allowed:", UniConsentCMP.shared.isAllowVendorById(vendorId: 1))
        default:
            break
        }
    }
}

To unsubscribe:

UniConsentCMP.shared.unsubscribe(self)

CMPUIDelegate (legacy)

You can also listen for consent dismiss with CMPUIDelegate:

class ViewController: UIViewController, CMPUIDelegate {

    func showCMP() {
        UniConsentCMP.shared.view.delegate = self
        UniConsentCMP.shared.setUIStage(.GDPRFirstScreen)
        UniConsentCMP.shared.launchCMP(rootVC: self, displayMode: .modalSheet)
    }

    func onDismiss() {
        // Called when the user closes the consent UI
        print("TC String:", UniConsentCMP.shared.getTCString())
    }
}

Automatically check if consent is expired when the vendorList updates:

// Automatic check if consent is expired when vendorList updates
if UniConsentCMP.shared.shouldRequestConsent() {
    UniConsentCMP.shared.launchCMP(rootVC: self)
}

Get the tcString if required:

// Get tcString if required
UniConsentCMP.shared.getTCString()

Read the consent status:

// Check consent for a specific IAB purpose
UniConsentCMP.shared.isAllowPurposeById(purposeId: 1)

// Check consent for a specific IAB vendor
UniConsentCMP.shared.isAllowVendorById(vendorId: 1)

// Get all allowed purpose/vendor IDs
UniConsentCMP.shared.getAllowedPurposeIds()
UniConsentCMP.shared.getAllowedVendorIds()

// Check if GDPR applies
UniConsentCMP.shared.gdprApplies

// Check if any consent has been given
UniConsentCMP.shared.isConsentGiven()

Reset consent status if required:

// Reset consent status if required
UniConsentCMP.shared.clearConsentData()

Custom First Layer (Agree All / Reject All)

Available from SDK version 26.7.0. If you build your own first-layer consent UI instead of showing the CMP webview, you can save consent directly:

// Grant consent for all purposes, special features and vendors
let saved = UniConsentCMP.shared.agreeAll()

// Reject all purposes, special features and vendors
let saved = UniConsentCMP.shared.rejectAll()

Both APIs save the same consent values as the CMP UI flow (IAB TCF keys, additional consent and consent mode signals) and fire the same .uiClose event for subscribers.

Note: These APIs fetch the vendor list and project config over the network synchronously and return false if consent could not be saved (e.g. offline). Call them after initialize() and off the main thread.

Important: Your custom first layer must still meet IAB TCF requirements (disclose purposes and vendors, and offer accept and reject with equal prominence). Users should still be able to open the full CMP UI (launchCMP()) for granular choices.

If your app opens a WKWebView that loads a web page with the UniConsent CMP tag, you can pass the native consent status so the CMP tag recognises existing consent and does not show the banner again.

Call syncConsent(to:) before loading the web page:

import WebKit
import UniConsent

let webView = WKWebView(frame: .zero, configuration: WKWebViewConfiguration())

// Inject native consent before page load
UniConsentCMP.shared.syncConsent(to: webView)

// Then load your web page
webView.load(URLRequest(url: URL(string: "https://example.com")!))

Objective-C

// Initialize with event handler
@interface AppDelegate () <CMPEventHandler>
@end

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    [[UniConsentCMP shared] subscribe:self];
    [[UniConsentCMP shared] initializeWithApiId:@"YOUR_APP_ID"];
    return YES;
}

- (void)handleWithEvent:(CMPEvent *)event {
    switch (event.type) {
        case CMPEventTypeReady:
            NSLog(@"CMP ready");
            break;
        case CMPEventTypeUiClose:
            NSLog(@"TC String: %@", [[UniConsentCMP shared] getTCString]);
            break;
        default:
            break;
    }
}

@end

// Display CMP (full screen by default)
[UniConsentCMP.shared launchCMPWithRootVC:self];

// Display CMP with a specific display mode
// Use CMPDisplayModeFullScreen, CMPDisplayModeModalSheet, or CMPDisplayModeDialog
[UniConsentCMP.shared launchCMPWithRootVC:self displayMode:CMPDisplayModeModalSheet];

Set up default consent status KV:

<key>FIREBASE_ANALYTICS_COLLECTION_ENABLED</key> <false/>
<key>GOOGLE_ANALYTICS_DEFAULT_ALLOW_ANALYTICS_STORAGE</key> <false/>
<key>GOOGLE_ANALYTICS_DEFAULT_ALLOW_AD_STORAGE</key> <false/>
<key>GOOGLE_ANALYTICS_DEFAULT_ALLOW_AD_USER_DATA</key> <false/>
<key>GOOGLE_ANALYTICS_DEFAULT_ALLOW_AD_PERSONALIZATION_SIGNALS</key> <false/>

Control analytics based on the consent flags:

// SDK Version <= 25.6.1
public func onDismiss() {

    if(UniConsentCMP.shared.isAllowPurposeById(purposeId: 1)) {
        Analytics.setConsent([
          .analyticsStorage: .granted,
          .adStorage: .granted,
          .adUserData: .granted,
          .adPersonalization: .granted,
        ])
        Analytics.setAnalyticsCollectionEnabled(true);
    } else {
        Analytics.setConsent([
          .analyticsStorage: .denied,
          .adStorage: .denied,
          .adUserData: .denied,
          .adPersonalization: .denied,
        ])
        Analytics.setAnalyticsCollectionEnabled(false);
    }

    // other logic such as send analytics events
}

Find more info at Set up consent mode for apps

Notes

  1. Users should have the ability to access a "Privacy Settings" button or link in the settings section of your application in order to open the CMP UI.
  2. You can utilize the shouldRequestConsent() function to check whether you should request new consent based on the status. Display the CMP UI as needed when a user opens the application.
  3. Starting with SDK version 25.6.1, you no longer need to manually send consent options for FirebaseAnalytics or certain supported AAP SDKs — they are handled automatically by the SDK. For more information, see What is Google AAP and Google MMP for Mobile Apps?.