iOS SDK

Installation

The minimum iOS version is iOS 12.

The SDK can be integrated into iOS projects using several methods:

Swift Package Manager (SPM)

  • Go to your project → Package Dependencies tab

  • Click the + sign to add a new package

  • Add the following SPM dependency URL

    https://github.com/UtiqTech/ios-sdk
    

Binary Framework

  • Head to this Repo.

  • Download Utiq-{VERSION}.zip.

  • Extract this file Utiq.xcframework.

  • Drag and drop it to your project.

  • Make sure you always have the latest stable update of the framework.

  1. Initialize the SDK.

Please make sure that you have the latest version from https://github.com/UtiqTech/ios-sdk/releases

Initializing the SDK

Once added to your App, you can initialize the SDK in the AppDelegate or the main class of your iOS application.

Please contact onboard@utiq.com to request a new SDK Token for your mobile App.

Basic initialization

Swift
Utiq.shared.initialize(sdkToken: SDK_TOKEN)

Initializing with custom options

UtiqOptions() is an optional parameter for SDK initialization. You can initialize the SDK without it, but if you want to enable or disable specific options, configure them in UtiqOptions and pass it to the initializer.

Swift
let options = UtiqOptions().enableLogging()
Utiq.shared.initialize(sdkToken: "test123", options: options)

Currently, the only available options are enabling.

Basic usage

Once the SDK is initialized, all the functions can be called by calling Utiq.shared. to access all the SDK functions.

Fetch Utiq data

Swift
try? Utiq.shared.fetchIdConnectData(dataCallback: { [weak self] idcData in
            
        }, errorCallback: { [weak self] error in
        
})

You can use a stub token to test the Utiq service if you don’t have an eligible SIM card from one of the supported Telcos.
Please contact onboard@utiq.com to generate a new stub token for your App.

Swift
try? Utiq.shared.fetchIdConnectData(stubToken: STUB_TOKEN, dataCallback: { [weak self] idcData in
            
        }, errorCallback: { [weak self] error in
        
})

App Tracking Transparency (ATT)

You must use the AppTrackingTransparency framework if you intend to use the AdTechPass and share it with other companies for cross-app and cross-website tracking purposes.

The AppTrackingTransparency framework displays a tracking authorization request to the user and provides the tracking authorization status.

For more details, see here.

Request ATT

  • Display the ATT pop-up. If the user accepts, proceed to the next step. If the user rejects, you can still continue, but only the MarTechPass will be returned.

  • Display the Utiq consent pop-up. If the user accepts, proceed to the next step; if the user rejects, stop the flow.

ATT Validity Check - User rejects the ATT

Apple allows users to change or withdraw ATT permissions from the App settings, even when the app is not open.

To do this, go to Settings > Privacy & Security > Tracking to see a list of Apps that have requested tracking permissions. You can toggle tracking on or off for a specific App.

In some cases, the ATT might be accepted but later withdrawn, so you should periodically check the ATT status (for example, on each App start). If the status has changed to rejected, you must also reject the Utiq consent and remove the AdTechPass if it was previously set.

ios-16-iphone-14-pro-settings-privacy-security-tracking-allow-apps-to-request-to-track.png

Dedicated guidelines for Utiq Privacy Requirements and Consent Experience on Mobile Apps can be found at this page Consent Experience on Mobile App

The SDK does not provide a built-in consent dialog. Your application must collect user consent using either a standard Consent Management Platform (CMP) or a custom pop-up that matches your app's look and feel.

User consent for the Utiq technology must be in place before calling the fetchIdConnectData() function.

While the pop-up may match the look and feel of your app, the consent description must follow our guidelines, which can be found here.

Swift
try? Utiq.shared.rejectConsent()

This function can be used to reject the user’s consent if he changed his mind, or to reset the consent status.

Since this is an API call, you may want to handle what happens when the request succeeds or fails. For this, you can use the same function as above, but with success and failure closures.

Swift
try? Utiq.shared.rejectConsent(successCallback: { [weak self] in
            
        }, errorCallback: { [weak self] error in
            
})

It is mandatory to notify the user that their consent has been successfully rejected if it was previously accepted, and the prompt must follow the text provided in the guidelines.

consenthub URL

If you need to access the ConsentHub URL from within the mobile app (for example, in a web view), you can do so by calling the function that returns the ConsentHub URL.

Swift
do {
    let consentHubUrl = try Utiq.shared.consentHubUrl()

    // With a stub token:
    // let consentHubUrl = try Utiq.shared.consentHubUrl(stubToken: STUB_TOKEN)
} catch {
    // Handle the error
}

During implementation, you may need to pass the stub token to the consentHubUrl(stubToken: STUB_TOKEN) function if you are not using an eligible SIM.

Error handling

All functions with callbacks described in the previous sections include both success and failure callbacks (or closures). The failure callback returns a custom error that is specific to the function. You can use this error to take a defined action based on the error type.

:info:

All of the following errors are of type UtiqError that inherits from NSError.

Displaying errors from the SDK directly to the end user is not recommended. Instead, errors are returned so that developers can handle them appropriately and take the necessary actions based on the error code.

Error

Code

Description

HttpException

HTTP Codes

Represents an HTTP exception that includes an error message and a status code.

InvalidSdkTokenException

-1982

Indicates that an invalid token was used to initialize the SDK.

SdkTokenCanNotBeEmptyException

-1986

This error is thrown when an empty token is passed to the SDK’s initializer.


SdkNotInitializedException


-1987

This error is thrown if you attempt to call any SDK function before the SDK has been initialized.



FailedToFetchConfigsException



-1988

This error is thrown when the SDK fails to fetch configuration data from the server and has no local configuration file to fall back on. This situation occurs if there are no cached configurations from a previous session, preventing the SDK from initializing correctly.

UtiqConsentExpiredException

-1959

This error is thrown when the user’s previously saved consent has expired.


TemplateDataUrlNotFoundException


-1960

This error is thrown when the templateDataUrl is missing from the config API response.

InvalidConsentVersionsException

-1961

This error is thrown when the consent version is invalid.


UnKnowUserStatusException


-1962

When starting the Utiq identification flow, the user status must be one of the following: NEW, OK, or NotCreated. If the status is any value other than the expected one, this error will be thrown.

UnKnownConnectionTypeException

-1963

Unknown connection type

EmptySetCookieHeaderException

-1964

Indicates that the Set-Cookie header is missing from the response header.

InvalidStubTokenException

-1965

Indicates that an invalid stub token was used to initialize the Utiq SDK.

MnoIneligibleException

-1966

Telco (SIM card operator) is not supported by Utiq.

UtiqConsentNotSetException

-1967

User has not provided consent or has not been prompted to accept or reject it.



UserOptedOutFromUtiqException



-1968

The user has deleted his data from ConsentHub. In this case, the SDK clears any cached data, and you should prompt the user to provide consent again, and finally fetch the IdConnect data to ensure the app has the latest information.

UnknownTelcoUseCaseException

-1969

Indicates that the SIM operator belongs to a use case that is unknown to Utiq.

MnoUrlNotFoundException

-1970

Indicates that the mobile MNO URL was not found.

DataValueNotFoundException

-1971

Indicates that the data value was not found.

DataDomainNotFoundException

-1972

Indicates that the data domain was not found.

NetworkIdentificationException

-1977

Indicates that the fetchIdConnectData function was not called.

IdConnectDataNotFoundException

-1978

Indicates that ID Connect data was not found.

GenericException

-1981

Indicates an error that does not match any of the other errors listed in this table.

Example CMP agnostic integration (Didomi)

The following is a sample integration. Feel free to organize the code in the way that best suits your project, but ensure you follow the general guidelines.

  1. Create a class that encapsulates all required Didomi functions. This can be a singleton, or an interface with an implementation that you inject using any DI framework or service locator.

    Swift
    import OSLog
    import Didomi
    
    class DidomiSdk {
        private var didomiEventListener: EventListener?
        private var isUtiqVendorEnabled = false
        private var isUtiqPurposeEnabled = false
        private let didomi = Didomi.shared
        //
        static let shared = DidomiSdk()
        private init() {
            self.didomi.setLogLevel(minLevel: OSLogType.default.rawValue)
        }
    
        func initialize() {
            /*
                The SDK will automatically use the remote configuration
                hosted by Didomi and cache it locally.
                The cached version is refreshed every 60 minutes.
                Config file example
                    {
                       "app": {
                       "name": "My App Name",
                       "privacyPolicyURL": "http://www.website.com/privacy",
                       "vendors": {
                           "iab": {
                              "all": true
                            }
                       },
                       "gdprAppliesGlobally": true,
                       "gdprAppliesWhenUnknown": true
                      }
                   }
            */
    
            let initializeParameters = DidomiInitializeParameters(
                apiKey: YOUR_API_KEY_GOES_HERE,
                localConfigurationPath: nil,
                remoteConfigurationURL: nil,
                providerID: nil,
                disableDidomiRemoteConfig: false,
                languageCode: nil,
                noticeID: YOUR_NOTICE_ID_GOES_HERE
            )
    
            self.didomi.initialize(initializeParameters)
    
            self.whenReady {
                let currentUserStatus = self.didomi.getCurrentUserStatus()
                self.isUtiqVendorEnabled = currentUserStatus.vendors.first(where: { $0.key.lowercased().contains("utiq") })?.value.enabled ?? false
                self.isUtiqPurposeEnabled = currentUserStatus.purposes.first(where: { $0.key.lowercased().contains("utiq") })?.value.enabled ?? false
            }
    
            self.onError {
                os_log("Error while initializing Didomi SDK", log: .default, type: .error, $0)
            }
        }
    
        func startIfNeeded(viewController: UIViewController, forceStart: Bool) {
            self.whenReady {
                self.didomi.setupUI(containerController: viewController)
                if (forceStart) {
                    self.didomi.forceShowNotice()
                }
            }
        }
    
        func startedBefore() -> Bool {
            !self.didomi.shouldUserStatusBeCollected()
        }
    
        func isUtiqEnabled() -> Bool {
            self.isUtiqVendorEnabled && self.isUtiqPurposeEnabled
        }
    
        func reset() {
            self.didomi.reset()
        }
    
        func resetUtiq() {
            self.whenReady {
                let currentUserStatus = self.didomi.getCurrentUserStatus()
                /*
                 If you want to hardcode the ID, The vendor ID
                 can also be found in the Didomi's console in the Data Manager
                 section, select the VENDORS tab, then search for the vendor
                 you want to enable or disable, and the APP ID is the vendor ID.
                 */
    
                let utiqVendorId = currentUserStatus.vendors.keys.first { $0.lowercased().contains("utiq") }
                /*
                 If you want to hardcode the ID, The purpose ID
                 can also be found in the Didomi's console in the Data Manager
                 section, select the Purposes tab, then search for the purpose
                 you want to enable or disable and the APP ID is the purpose ID.
                 */
    
                let utiqPurposeId = currentUserStatus.purposes.keys.first { $0.lowercased().contains("utiq") }
    
                if(utiqVendorId != nil && utiqPurposeId != nil) {
                    _ = self.didomi.openCurrentUserStatusTransaction()
                        .disableVendor(utiqVendorId!)
                        .disablePurpose(utiqPurposeId!)
                        .commit()
                }
            }
        }
    
        func consentStatusDidChange(action: @escaping (_ enabled: Bool) -> ()) {
            /*
             Listen for changes on the user status linked to a specific vendor.
             We always need to listen for changes from Didomi as the user
             might open the screen from another place and reject his consent
             that he granted before, in this case we need to keep the synchronization
             between Didomi and Utiq
             */
    
            self.whenReady {
                if (self.didomiEventListener == nil) {
                    self.didomiEventListener = self.createDidomiEventListener(action: action)
                }
                else {
                    self.didomi.removeEventListener(listener: self.didomiEventListener!)
                }
                self.didomi.addEventListener(listener: self.didomiEventListener!)
            }
        }
    
        func onError(errorAction: @escaping(_ errorMessage: String) -> ()) {
            let eventListener = EventListener()
            eventListener.onError = {
                errorAction($0.localizedDescription)
            }
            self.didomi.addEventListener(listener: eventListener)
        }
    
        func whenReady(action: @Sendable @escaping () -> Void) {
            self.didomi.onReady(callback: action)
        }
    
        private func createDidomiEventListener(action: @escaping (_ enabled: Bool) -> ()) -> EventListener {
            let didomiEventListener = EventListener()
            didomiEventListener.onPreferencesClickVendorAgree = { _, vendorId in
                if (vendorId?.lowercased().contains("utiq") ?? false) {
                    self.isUtiqVendorEnabled = true
                }
            }
    
            didomiEventListener.onPreferencesClickVendorDisagree = { _, vendorId in
                if (vendorId?.lowercased().contains("utiq") ?? false) {
                    self.isUtiqVendorEnabled = false
                }
            }
    
            didomiEventListener.onPreferencesClickAgreeToAllVendors = { _ in
                self.isUtiqVendorEnabled = true
            }
    
            didomiEventListener.onPreferencesClickDisagreeToAllVendors = { _ in
                self.isUtiqVendorEnabled = false
            }
    
            // This will be called when the agree selector switch one of the options of the second layer is selected
            didomiEventListener.onPreferencesClickPurposeAgree = { _, purposeId in
                if (purposeId?.lowercased().contains("utiq") ?? false) {
                    self.isUtiqPurposeEnabled = true
                }
            }
    
            // This will be called when the disagree selector switch one of the options of the second layer is selected
            didomiEventListener.onPreferencesClickPurposeDisagree = { _, purposeId in
                if (purposeId?.lowercased().contains("utiq") ?? false) {
                    self.isUtiqPurposeEnabled = false
                }
            }
    
            // This will be called when the agree all selector switch one of the options of the second layer is selected
            didomiEventListener.onPreferencesClickAgreeToAllPurposes = { _ in
                self.isUtiqPurposeEnabled = true
            }
    
            // This will be called when the disagree all selector switch one of the options of the second layer is selected
            didomiEventListener.onPreferencesClickDisagreeToAllPurposes = { _ in
                self.isUtiqPurposeEnabled = false
            }
    
            didomiEventListener.onPreferencesClickSaveChoices = { event in
                action(self.isUtiqEnabled())
            }
    
            // This will be called when Agree of the first layer is clicked
            didomiEventListener.onNoticeClickAgree = { _ in
                self.isUtiqVendorEnabled = true
                self.isUtiqPurposeEnabled = true
                action(self.isUtiqEnabled())
            }
    
            // This will be called when Disagree of the first layer is clicked
            didomiEventListener.onNoticeClickDisagree = { _ in
                self.isUtiqVendorEnabled = false
                self.isUtiqPurposeEnabled = false
                action(self.isUtiqEnabled())
            }
    
            return didomiEventListener
        }
    }
    
  2. Create a function to start and observe Didomi's status

    Swift
    private func startDidomiAndObserveConsentStatus(forceStart: Bool) {
        DidomiSdk.shared.startIfNeeded(
            viewController: self,
            forceStart: forceStart
        )
    
        DidomiSdk.shared.consentStatusDidChange { accepted in
            if accepted {
                // Only use a stub when testing without an eligible SIM.
                self.stubToken = YOUR_STUB_TOKEN_GOES_HERE
                self.fetchUtiqIds()
            } else {
                self.rejectUtiqConsent()
            }
        }
    }
    
  3. To synchronize Didomi's status with Utiq, you can do the following

    Swift
    DidomiSdk.shared.whenReady {
        if DidomiSdk.shared.startedBefore() {
            if DidomiSdk.shared.isUtiqEnabled() {
                self.stubToken = YOUR_STUB_TOKEN_GOES_HERE
                self.fetchUtiqIds()
            } else {
                self.rejectUtiqConsent()
            }
        } else {
            self.startDidomiAndObserveConsentStatus(forceStart: false)
        }
    }
    
  4. Then fetch Utiq IDs

    Swift
    // Call the function that accepts the stub token if the token is not nil, else call the other function the does not accept the stub token
    if let token = self.stubToken {
                Utiq.shared.fetchIdConnectData(stubToken: token, dataCallback: { [weak self] idcData in
                    /*
                       Do Whatever you want with the IDs
                       AttrPass => idcData.attrid
                       AdTechPass => idcData.atid
                       MarTechPass => idcData.mtid
                    */ 
                }, errorCallback: {
                    let errorCode = ($0 as NSError).code
                    let userOptedOutFromUtiqErrorCode = UserOptedOutFromUtiqException().code
                    let userFrozenUtiqForOneYearErrorCode = UserFrozenUtiqForOneYearException().code
                    if errorCode == userOptedOutFromUtiqErrorCode || errorCode == userFrozenUtiqForOneYearErrorCode {
                        DidomiSdk.shared.resetUtiq()
                    }
                    // Handle the error
                })
            }
            else {
                Utiq.shared.fetchIdConnectData(dataCallback: { [weak self] idcData in
                    /*
                       Do Whatever you want with the IDs
                       AttrPass => idcData.attrid
                       AdTechPass => idcData.atid
                       MarTechPass => idcData.mtid
                    */ 
                }, errorCallback: {
                    if $0 is UserOptedOutFromUtiqException || $0 is UserFrozenUtiqForOneYearException {
                        DidomiSdk.shared.resetUtiq()
                    }
                    // Handle the error
                })
            }
    
  5. Synchronize Didomi when Utiq is manually rejected from within the App (for example, from the Manage Utiq page).

    Swift
     private func rejectUtiqConsent(
        postAction: @escaping () -> Void = {}
    ) {
        Utiq.shared.rejectConsent {
            postAction()
        } errorCallback: { error in
            // Handle the error
        }
    }
    
  6. To update Didomi’s status, call the following function.

    Swift
    self.startDidomiAndObserveConsentStatus(forceStart: true)
    

Support and bug reporting

If you have suggestions, want to report a bug, or have any other inquiries, please contact onboard@utiq.com