iOS SDK - Basic Integration


Prerequisites

Complete the steps in Integrating a Singular SDK: Planning and Prerequisites before proceeding with this integration.

Important: These prerequisite steps are required for any Singular SDK integration.

Installation

Choose your preferred installation method. We recommend CocoaPods for most projects.

Quick Decision:

  • Using CocoaPods already? Use Method 1
  • SPM-only project? Use Method 2
  • No package manager? Use Method 3

Installation Methods

Method 1: CocoaPods (Recommended)
#

Method 1: CocoaPods (Recommended)

Requirements:

Installation Steps:

  1. Initialize Podfile (skip if you already have one):
    Terminal
    cd /path/to/your/project
    pod init
  2. Add Singular SDK to your Podfile:
    Podfile
    platform :ios, '12.0'
    
    target 'YourAppName' do
      use_frameworks!
      
      # Singular SDK
      pod 'Singular-SDK'
      
    end
  3. Install dependencies:
    Terminal
    pod install
  4. Open workspace: From now on, open .xcworkspace instead of .xcodeproj
  5. Swift projects only: Create bridging header (see below)
Method 2: Swift Package Manager
#

Method 2: Swift Package Manager

Installation Steps:

  1. In Xcode: File → Add Packages
  2. Enter repository URL: https://github.com/singular-labs/Singular-iOS-SDK
  3. Select version and click Add Package
  4. Add required frameworks:
    Go to Build Phases → Link Binary with Libraries and add:
    • Link required libraries:
      Go to Build Phases → Link Binary With Libraries and add:
      • Libsqlite3.0.tbd
      • SystemConfiguration.framework
      • Security.framework
      • Libz.tbd
      • AdSupport.framework
      • WebKit.framework
      • StoreKit.framework
      • AdServices.framework (mark as Optional)
  5. Swift projects only: Create bridging header (see below)
Method 3: Manual Framework Installation
#

Method 3: Manual Framework Installation

When to use: Only use this method if you cannot use CocoaPods or SPM.

Download Framework:

Installation Steps:

  1. Unzip the downloaded framework
  2. In Xcode: Right-click project → Add Files To [Project]
  3. Select Create Groups and add the framework folder
  4. Link required libraries:
    Go to Build Phases → Link Binary With Libraries and add:
    • Libsqlite3.0.tbd
    • SystemConfiguration.framework
    • Security.framework
    • Libz.tbd
    • AdSupport.framework
    • WebKit.framework
    • StoreKit.framework
    • AdServices.framework (mark as Optional)
  5. Embed framework:
    Go to General → Frameworks, Libraries, and Embedded Content
    Set Singular framework to Embed & Sign

Swift Bridging Header

Important: Required for Swift projects using CocoaPods or SPM.

  1. Create header file:
    Xcode → File → New → File → Header File
    Name it YourProjectName-Bridging-Header.h
  2. Add import:
    Bridging Header
    #import <Singular/Singular.h>
  3. Link in build settings:
    Build Settings → Objective-C Bridging Header
    Set to: YourProjectName/YourProjectName-Bridging-Header.h

Configure and Initialize SDK

Create a configuration object and initialize the SDK at your app's entry points.


Create Configuration Object

Basic Configuration

Create a SingularConfig object with your SDK credentials and optional features. This configuration is universal across all app architectures.

Get Your Credentials: Find your SDK Key and SDK Secret in the Singular platform under Developer Tools → SDK Integration.

SwiftObjective-C
// MARK: - Singular Configuration
private func getConfig() -> SingularConfig? {
    // Create config with your credentials
    guard let config = SingularConfig(
        apiKey: "YOUR_SDK_KEY",
        andSecret: "YOUR_SDK_SECRET"
    ) else {
        return nil
    }
    
    // OPTIONAL: Wait for ATT consent (if showing ATT prompt)
    // Remove this line if NOT using App Tracking Transparency
    config.waitForTrackingAuthorizationWithTimeoutInterval = 300
    
    // OPTIONAL: Support custom ESP domains for deep links
    config.espDomains = ["links.your-domain.com"]
    
    // OPTIONAL: Handle deep links
    config.singularLinksHandler = { params in
        if let params = params {
          self.handleDeeplink(params)
        }
    }
    
    return config
}

// MARK: - OPTIONAL: Deep link handler implementation
private func handleDeeplink(_ params: SingularLinkParams) {
    // Guard clause: Exit if no deep link provided
    guard let deeplink = params.getDeepLink() else {
      return
    }
        
    // Extract deep link parameters
    let passthrough = params.getPassthrough()
    let isDeferred = params.isDeferred()
    let urlParams = params.getUrlParameters()
        
    #if DEBUG
    // Debug logging only - stripped from production builds
    print("Singular Links Handler")
    print("Singular deeplink received:", deeplink)
    print("Singular passthrough received:", passthrough ?? "none")
    print("Singular isDeferred received:", isDeferred ? "YES" : "NO")
    print("Singular URL Params received:", urlParams ?? [:])
    #endif
        
    // TODO: Navigate to appropriate screen based on deep link
    // Add deep link handling code here. Navigate to appropriate screen.
}

SKAdNetwork Auto-Enabled: Starting with SDK version 12.0.6, SKAdNetwork is enabled by default. No additional configuration needed.


Initialize SDK

Choose Your App Architecture

Initialize the SDK at every app entry point. The initialization pattern depends on your app's architecture.

Which architecture do I have?

  • SceneDelegate: Check if your project has SceneDelegate.swift or SceneDelegate.m
  • SwiftUI: Your app starts with @main struct YourApp: App
  • AppDelegate only: Pre-iOS 13 apps or apps without SceneDelegate

Modern iOS: SceneDelegate (iOS 13+)
#

Modern iOS: SceneDelegate (iOS 13+)

Where to add code: SceneDelegate.swift or SceneDelegate.m

Entry points to initialize:

  • willConnectTo session - App launch
  • continue userActivity - Universal Links
  • openURLContexts - Deep link schemes
SwiftObjective-C
import Singular

class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    
    var window: UIWindow?
    
    // 1️⃣ App launch
    func scene(_ scene: UIScene, willConnectTo session: UISceneSession,
              options connectionOptions: UIScene.ConnectionOptions) {
        
        // ANTI-SWIZZLING: Capture deep link parameters IMMEDIATELY before any 
        // other code runs. This prevents third-party SDKs from intercepting 
        // or modifying these values.
        let userActivity = connectionOptions.userActivities.first
        let urlContext = connectionOptions.urlContexts.first
        let openUrl = urlContext?.url
        
        #if DEBUG
        // Log captured values to detect swizzling interference
        print("[SWIZZLE CHECK] UserActivity captured:", userActivity?.webpageURL?.absoluteString ?? "none")
        print("[SWIZZLE CHECK] URL Context captured:", openUrl?.absoluteString ?? "none")
        print("IDFV:", UIDevice.current.identifierForVendor?.uuidString ?? "N/A")
        #endif
        
        // Create window from windowScene
        guard let windowScene = scene as? UIWindowScene else { return }
        window = UIWindow(windowScene: windowScene)
        window?.rootViewController = UIViewController() // Replace with your root VC
        window?.makeKeyAndVisible()
        
        // Singular initialization - uses captured values to avoid swizzling conflicts
        guard let config = getConfig() else { return }
        
        // Pass Universal Link if available
        if let userActivity = userActivity {
            config.userActivity = userActivity
        }
        
        // Pass URL scheme if available
        // CRITICAL for custom URL scheme attribution
        if let openUrl = openUrl {
            config.openUrl = openUrl
        }
        
        // Initialize Singular SDK
        Singular.start(config)
    }
    
    // 2️⃣ Universal Links
    func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
        guard let config = getConfig() else { return }
        config.userActivity = userActivity
        
        // Initialize Singular SDK
        Singular.start(config)
    }
    
    // 3️⃣ Deep link schemes
    func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
        guard let config = getConfig() else { return }
        
        if let url = URLContexts.first?.url {
            config.openUrl = url
        }
        
        // Initialize Singular SDK
        Singular.start(config)
    }
}
Modern iOS: SwiftUI App (iOS 14+)
#

Modern iOS: SwiftUI App (iOS 14+)

Where to add code: Your main App struct file

Entry points to initialize:

  • .onOpenURL(of: scenePhase) - Handle custom URL schemes
  • .onContinueUserActivity(of: scenePhase) - Handle Universal Links (Singular Deep Links)
  • .onChange.active - Handle initialization on first launch if no deep link occurred. Handles Deferred Deeplinks.
Swift
import SwiftUI
import Singular

@main
struct simpleSwiftUIApp: App {
    @Environment(\.scenePhase) var scenePhase
    @State private var hasInitialized = false
    
    var body: some Scene {
        WindowGroup {
            ContentView()
                // 1️⃣ Handle custom URL schemes (e.g., myapp://path)
                .onOpenURL { url in
                    #if DEBUG
                    print("[Singular] URL Scheme:", url.absoluteString)
                    #endif
                    
                    guard let config = getConfig() else { return }
                    config.openUrl = url
                    Singular.start(config)
                    hasInitialized = true
                }
                
                // 2️⃣ Handle Universal Links (e.g., https://links.your-domain.com)
                .onContinueUserActivity(NSUserActivityTypeBrowsingWeb) { userActivity in
                    #if DEBUG
                    print("[Singular] Universal Link:", userActivity.webpageURL?.absoluteString ?? "none")
                    #endif
                    
                    guard let config = getConfig() else { return }
                    config.userActivity = userActivity
                    Singular.start(config)
                    hasInitialized = true
                }
        }
        .onChange(of: scenePhase) { oldPhase, newPhase in
            switch newPhase {
            case .active:
                // 3️⃣ Initialize ONLY on first launch if no deep link occurred
                guard !hasInitialized else {
                    #if DEBUG
                    print("[Singular] Already initialized, skipping")
                    #endif
                    return
                }
                
                #if DEBUG
                if let idfv = UIDevice.current.identifierForVendor?.uuidString {
                    print("[Singular] IDFV:", idfv)
                }
                #endif
                
                guard let config = getConfig() else { return }
                Singular.start(config)
                hasInitialized = true
                
            case .background:
                #if DEBUG
                print("[Singular] App backgrounded")
                #endif
                
            case .inactive:
                #if DEBUG
                print("[Singular] App inactive")
                #endif
                
            @unknown default:
                break
            }
        }
    }
    
    // Add your getConfig() function here
    // MARK: - Singular Configuration
    private func getConfig() -> SingularConfig? {
        // ... (same as above)
    }
    
    // Add your handleDeeplink() function here
    // MARK: - Deep Link Handler
    private func handleDeeplink(_ params: SingularLinkParams) {
        // ... (same as above)
    }
}
Legacy iOS: AppDelegate (Pre-iOS 13)
#

Legacy iOS: AppDelegate (Pre-iOS 13)

Where to add code: AppDelegate.swift or AppDelegate.m

Entry points to initialize:

  • didFinishLaunchingWithOptions - App launch
  • continue userActivity - Universal Links
  • open url - Deep link schemes
SwiftObjective-C
import Singular
import UIKit


class AppDelegate: UIResponder, UIApplicationDelegate {
    
    // 1️⃣ App launch
    func application(_ application: UIApplication, 
                    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        
        // ANTI-SWIZZLING: Capture deep link parameters IMMEDIATELY before any 
        // other code runs. This prevents third-party SDKs from intercepting 
        // or modifying these values.
        let launchUrl = launchOptions?[.url] as? URL
        let userActivityDictionary = launchOptions?[.userActivityDictionary] as? [String: Any]
        let userActivity = userActivityDictionary?["UIApplicationLaunchOptionsUserActivityKey"] as? NSUserActivity
        
        #if DEBUG
        // Log captured values to detect swizzling interference
        print("[SWIZZLE CHECK] Launch URL captured:", launchUrl?.absoluteString ?? "none")
        print("[SWIZZLE CHECK] UserActivity captured:", userActivity?.webpageURL?.absoluteString ?? "none")
        print("IDFV:", UIDevice.current.identifierForVendor?.uuidString ?? "N/A")
        #endif
        
        // Singular initialization - uses captured values to avoid swizzling conflicts
        guard let config = getConfig() else { return true }
        
        // Pass the entire launchOptions dictionary for Singular's internal processing
        config.launchOptions = launchOptions
        
        // Explicitly pass Universal Link if available
        // CRITICAL for universal link attribution
        if let userActivity = userActivity {
            config.userActivity = userActivity
        }
        
        // Explicitly pass URL scheme if available
        // CRITICAL for custom URL scheme attribution
        if let launchUrl = launchUrl {
            config.openUrl = launchUrl
        }
        
        // Initialize Singular SDK
        Singular.start(config)
        
        return true
    }
    
    // 2️⃣ Universal Links
    func application(_ application: UIApplication, 
                    continue userActivity: NSUserActivity, 
                    restorationHandler: @escaping ([any UIUserActivityRestoring]?) -> Void) -> Bool {
        
        #if DEBUG
        print("[SWIZZLE CHECK] Universal Link handler called:", userActivity.webpageURL?.absoluteString ?? "none")
        #endif
        
        guard let config = getConfig() else { return true }
        config.userActivity = userActivity
        Singular.start(config)
        
        return true
    }
    
    // 3️⃣ Deep link schemes
    func application(_ app: UIApplication, 
                    open url: URL, 
                    options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
        
        #if DEBUG
        print("[SWIZZLE CHECK] URL Scheme handler called:", url.absoluteString)
        #endif
        
        guard let config = getConfig() else { return true }
        config.openUrl = url
        Singular.start(config)
        
        return true
    }

    // Add your getConfig() function here
    // MARK: - Singular Configuration
    private func getConfig() -> SingularConfig? {
        // ... (same as above)
    }
    
    // Add your handleDeeplink() function here
    // MARK: - Deep Link Handler
    private func handleDeeplink(_ params: SingularLinkParams) {
        // ... (same as above)
    }
}

Verify Installation

Pre-Flight Checklist

Confirm these items before building and testing your integration.

  • SDK installed via CocoaPods, SPM, or manual framework
  • Swift bridging header created (if using Swift)
  • getConfig() function implemented
  • Singular.start(config) called at all entry points
  • SDK Key and SDK Secret added to config
  • ATT timeout configured (only if showing ATT prompt)
  • Deep link handler configured (only if using deep links)
  • App builds without errors

Next Steps:

  • Build and run your app
  • Check console for IDFV print statement
  • Test in Singular SDK Console using your IDFV
  • Verify sessions appear in SDK Console within 1-2 minutes

Optional: App Tracking Transparency (ATT)

Configure ATT to request user permission for IDFA access and improve attribution accuracy.

Skip this section if: You're not displaying an ATT prompt in your app.

Why Request ATT Consent?

IDFA Benefits

Starting with iOS 14.5, apps must request user permission to access the device's IDFA (Identifier for Advertisers).

Attribution with vs. without IDFA:

  • With IDFA: Precise device-level attribution and accurate install matching
  • Without IDFA: Probabilistic attribution using IP, user agent, and device fingerprinting

Recommendation: Request ATT consent for better attribution accuracy. Singular can attribute without IDFA, but accuracy decreases.


Configure ATT Delay

Delay SDK Initialization

Add a timeout to wait for the user's ATT response before sending the first session to Singular.

Critical: The SDK must wait for ATT consent before sending the first session. Otherwise, the initial attribution event won't include the IDFA.

SwiftObjective-C
func getSingularConfig() -> SingularConfig? {
    guard let config = SingularConfig(
        apiKey: "YOUR_SDK_KEY",
        andSecret: "YOUR_SDK_SECRET"
    ) else {
        return nil
    }
    
    // Wait up to 300 seconds for ATT response
    config.waitForTrackingAuthorizationWithTimeoutInterval = 300
    
    return config
}

ATT Flow Timeline

Here's what happens when you configure an ATT delay:

  1. App Launch: SDK starts recording events but doesn't send them yet
  2. ATT Prompt: Your app shows the ATT consent dialog
  3. User Response: User grants or denies permission
  4. SDK Sends Data: SDK immediately sends queued events with IDFA (if granted)
  5. Timeout Fallback: If 300 seconds pass without response, SDK sends data anyway

Best Practice: Show your ATT prompt as early as possible (ideally on first app launch) to maximize IDFA availability for attribution.