iOS SDK - 基础集成


前提条件

在继续此集成之前,请完成 集成 Singular SDK:规划与前提条件 中的步骤。

重要: 这些前提步骤是任何 Singular SDK 集成的必要条件。

iOS 特定设置

在初始化 SDK 之前,请确认以下项目已在您的 Xcode 项目中配置妥当。缺少任何一项都是导致 SDK 静默失败的常见原因。

Info.plist 条目:

  • NSUserTrackingUsageDescription — 一段面向用户的字符串,说明您的应用为何请求跟踪权限。如果您曾向用户展示 App Tracking Transparency (ATT) 提示或读取 IDFA,Apple 要求必须配置此项。Singular SDK 会自动遵循 ATT;若未配置此键,SDK 仍可运行,但在 iOS 14.5+ 上无法访问 IDFA。
  • URL Types 和 CFBundleURLSchemes — 如果您的深度链接策略使用自定义 URI scheme(例如 myapp:// ),则需要配置此项。通过 HTTPS 使用 Universal Links / Singular Links 时无需配置。

Entitlements (Signing & Capabilities):

  • Associated Domains — 为每个 Singular 品牌域名添加一个条目,格式为 applinks:yourcompany.sng.link 。Universal Link / Singular Link 归因需要此配置。这个 Singular 托管域名将托管一个 apple-app-site-association (AASA) JSON 文件,Apple 会在安装时下载该文件。当在 Singular 平台为应用创建链接时,AASA 文件会自动生成和更新。
  • Push Notifications — 卸载跟踪和推送归因再互动需要此项。请参阅"Supporting Push Notifications"和"Uninstall Tracking"文章。

所需系统框架(仅用于手动集成 — CocoaPods 和 SwiftPM 会自动链接):

  • StoreKit.framework — IAP 和 SKAdNetwork。
  • AdServices.framework — Apple Search Ads 归因令牌(iOS 14.3+)。
  • AppTrackingTransparency.framework — ATT 提示和授权状态(iOS 14.5+)。
  • UserNotifications.framework — 推送通知。
  • WebKit.framework — 仅当您使用 Singular WKWebView JS 桥接时才需要。

安装

选择您首选的安装方式。我们推荐大多数项目使用 CocoaPods。

快速决策:

  • 已在使用 CocoaPods? 使用方法 1
  • 仅使用 SPM 的项目? 使用方法 2
  • 未使用任何包管理器? 使用方法 3

安装方法

方法 1:CocoaPods(推荐)

方法 1:CocoaPods(推荐)

要求:

  • 已安装 CocoaPods( 安装指南 )
  • 可以通过终端访问您的项目目录

安装步骤:

  1. 初始化 Podfile (如果已有则跳过):

    Terminal
    cd /path/to/your/project
    pod init
  2. 添加 Singular SDK 到您的 Podfile:

    Podfile
    platform :ios, '12.0'
    
    target 'YourAppName' do
      use_frameworks!
    
      # Singular SDK
      pod 'Singular-SDK'
    
    end
  3. 安装依赖项:

    Terminal
    pod install
  4. 打开 workspace: 从现在起,打开 .xcworkspace 而不是 .xcodeproj
  5. 仅 Swift 项目: 创建桥接头文件 (请参阅下方)
方法 2:Swift Package Manager

方法 2:Swift Package Manager

安装步骤:

  1. 在 Xcode 中: File → Add Packages
  2. 输入仓库 URL: https://github.com/singular-labs/Singular-iOS-SDK
  3. 选择版本并点击 Add Package
  4. 添加所需框架:
    前往 Build Phases → Link Binary with Libraries 并添加:
    • 链接所需的库:
      前往 Build Phases → Link Binary With Libraries 并添加:
      • Libsqlite3.0.tbd
      • SystemConfiguration.framework
      • Security.framework
      • Libz.tbd
      • AdSupport.framework
      • WebKit.framework
      • StoreKit.framework
      • AdServices.framework(标记为 Optional)
  5. 仅 Swift 项目: 创建桥接头文件 (请参阅下方)
方法 3:手动 Framework 安装

方法 3:手动 Framework 安装

适用场景: 仅在无法使用 CocoaPods 或 SPM 时使用此方法。

下载 Framework:

安装步骤:

  1. 解压下载的 framework
  2. 在 Xcode 中:右键点击项目 → Add Files To [Project]
  3. 选择 Create Groups 并添加 framework 文件夹
  4. 链接所需的库:
    前往 Build Phases → Link Binary With Libraries 并添加:
    • Libsqlite3.0.tbd
    • SystemConfiguration.framework
    • Security.framework
    • Libz.tbd
    • AdSupport.framework
    • WebKit.framework
    • StoreKit.framework
    • AdServices.framework(标记为 Optional)
  5. 嵌入 framework:
    前往 General → Frameworks, Libraries, and Embedded Content
    将 Singular framework 设为 Embed & Sign

Swift 桥接头文件

重要: 使用 CocoaPods 或 SPM 的 Swift 项目需要此配置。

  1. 创建头文件:
    Xcode → File → New → File → Header File
    命名为 YourProjectName-Bridging-Header.h
  2. 添加 import:

    Bridging Header
    #import <Singular/Singular.h>
  3. 在 build settings 中链接:
    Build Settings → Objective-C Bridging Header
    设置为: YourProjectName/YourProjectName-Bridging-Header.h

配置并初始化 SDK

创建一个配置对象,并在应用的入口点初始化 SDK。


创建配置对象

基础配置

使用您的 SDK 凭据和可选功能创建一个 SingularConfig 对象。此配置适用于所有应用架构。

获取您的凭据: 在 Singular 平台的 Developer Tools → SDK Integration 下找到您的 SDK Key 和 SDK Secret。

Swift Objective-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 自动启用: 从 SDK 12.0.6 版本开始,SKAdNetwork 默认启用。无需额外配置。


初始化 SDK

选择您的应用架构

在每个应用入口点初始化 SDK。初始化模式取决于您应用的架构。

我使用的是哪种架构?

  • SceneDelegate: 检查您的项目是否有 SceneDelegate.swift SceneDelegate.m
  • SwiftUI: 您的应用以 @main struct YourApp: App 开头
  • 仅 AppDelegate: iOS 13 之前的应用或没有 SceneDelegate 的应用

现代 iOS:SceneDelegate(iOS 13+)

现代 iOS:SceneDelegate(iOS 13+)

添加代码的位置: SceneDelegate.swift SceneDelegate.m

需要初始化的入口点:

  • willConnectTo session - 应用启动
  • continue userActivity - Universal Links
  • openURLContexts - 深度链接 scheme
Swift Objective-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)
    }
}
现代 iOS:SwiftUI App(iOS 14+)

现代 iOS:SwiftUI App(iOS 14+)

添加代码的位置: 您的主 App struct 文件

需要初始化的入口点:

  • .onOpenURL(of: scenePhase) - 处理自定义 URL scheme
  • .onContinueUserActivity(of: scenePhase) - 处理 Universal Links(Singular Deep Links)
  • .onChange.active - 处理首次启动时的初始化(如果没有发生深度链接)。处理 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)
    }
}
传统 iOS:AppDelegate(iOS 13 之前)

传统 iOS:AppDelegate(iOS 13 之前)

添加代码的位置: AppDelegate.swift AppDelegate.m

需要初始化的入口点:

  • didFinishLaunchingWithOptions - 应用启动
  • continue userActivity - Universal Links
  • open url - 深度链接 scheme
Swift Objective-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)
    }
}

验证安装

上线前检查清单

在构建和测试您的集成之前,请确认以下事项。

  • 已通过 CocoaPods、SPM 或手动 framework 方式安装 SDK
  • 已创建 Swift 桥接头文件(如果使用 Swift)
  • 已实现 getConfig() 函数
  • 已在所有入口点调用 Singular.start(config)
  • 已将 SDK Key 和 SDK Secret 添加到 config
  • 已配置 ATT 超时(仅当显示 ATT 提示时)
  • 已配置深度链接处理器(仅当使用深度链接时)
  • 应用构建无错误

后续步骤:

  • 构建并运行您的应用
  • 在控制台中检查 IDFV 打印语句
  • 使用您的 IDFV 在 Singular SDK Console 中进行测试
  • 验证会话是否在 1-2 分钟内出现在 SDK Console 中

可选:App Tracking Transparency (ATT)

配置 ATT 以请求用户授权访问 IDFA,并提高归因准确性。

如果存在以下情况,请跳过本节: 您的应用未显示 ATT 提示。

为什么要请求 ATT 授权?

IDFA 的优势

从 iOS 14.5 开始,应用必须请求用户授权才能访问设备的 IDFA(Identifier for Advertisers,广告标识符)。

有 IDFA 与无 IDFA 的归因对比:

  • 有 IDFA: 精准的设备级归因和准确的安装匹配
  • 无 IDFA: 基于 IP、user agent 和设备指纹的概率性归因

建议: 请求 ATT 授权以获得更好的归因准确性。Singular 可以在没有 IDFA 的情况下进行归因,但准确性会降低。


配置 ATT 延迟

延迟 SDK 初始化

添加一个超时,以等待用户的 ATT 响应,然后再向 Singular 发送第一个会话。

关键: 在发送第一个会话之前,SDK 必须等待 ATT 授权。否则,初始归因事件将不包含 IDFA。

Swift Objective-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 流程时间线

当您配置 ATT 延迟后,会发生以下情况:

  1. 应用启动: SDK 开始记录事件,但暂不发送
  2. ATT 提示: 您的应用显示 ATT 授权对话框
  3. 用户响应: 用户同意或拒绝授权
  4. SDK 发送数据: SDK 立即发送已排队的事件(如已授权则包含 IDFA)
  5. 超时回退: 如果 300 秒后仍无响应,SDK 将无论如何发送数据

最佳实践: 尽早显示 ATT 提示(理想情况下在首次启动应用时),以最大化 IDFA 的可用性,提升归因效果。