iOS SDK - 基本集成

文档

前提条件

完成集成 Singular SDK中的步骤:的步骤。

重要:任何 Singular SDK 集成都需要这些前提步骤。

安装

选择您喜欢的安装方法。我们建议大多数项目使用 CocoaPods。

快速决定

  • 已经使用 CocoaPods?使用方法 1
  • 仅使用 SPM 的项目?使用方法 2
  • 没有软件包管理器?使用方法 3

安装方法

方法 1:CocoaPods(推荐)
#

方法 1: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. 打开工作区:从现在起,打开.xcworkspace 而不是.xcodeproj
  5. 仅限 Swift 项目: 创建桥接头(见下文
方法 2:Swift 软件包管理器
#

方法 2:Swift 软件包管理器

安装步骤

  1. 在 Xcode 中文件 → 添加软件包
  2. 输入版本库 URL:https://github.com/singular-labs/Singular-iOS-SDK
  3. 选择版本并单击添加软件包
  4. 添加所需的框架:
    转到构建阶段 → 将二进制文件与库链接并添加:
    • 链接所需库:
      转到构建阶段 → 用库链接二进制文件并添加:
      • Libsqlite3.0.tbd
      • 系统配置框架
      • 安全框架
      • Libz.tbd
      • 广告支持框架
      • WebKit.framework
      • 存储套件框架
      • AdServices.framework (标记为可选)
  5. 仅限 Swift 项目: 创建桥接头(见下文
方法 3:手动安装框架
#

方法 3:手动安装框架

何时使用:仅在无法使用 CocoaPods 或 SPM 时使用此方法。

下载框架

安装步骤

  1. 解压下载的框架
  2. 在 Xcode 中右键单击项目 →添加文件到 [项目
  3. 选择创建组并添加框架文件夹
  4. 链接所需的库:
    转到构建阶段 → 将二进制文件与库链接并添加:
    • Libsqlite3.0.tbd
    • 系统配置框架
    • 安全框架
    • Libz.tbd
    • 广告支持框架
    • WebKit.framework
    • 存储套件框架
    • AdServices.framework (标记为可选)
  5. 嵌入框架:
    转到常规 → 框架、库和嵌入式内容
    将 Singular 框架设置为嵌入和签名

Swift 桥接头

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

  1. 创建头文件:
    Xcode →文件 → 新建 → 文件 → 头文件
    将其命名为YourProjectName-Bridging-Header.h
  2. 添加导入
    Bridging Header
    #import <Singular/Singular.h>
  3. 构建设置中的链接:
    构建设置 → Objective-C 桥接头
    设置为YourProjectName/YourProjectName-Bridging-Header.h

配置和初始化 SDK

在应用程序的入口点创建配置对象并初始化 SDK。


创建配置对象

基本配置

创建SingularConfig 对象,其中包含 SDK 证书和可选功能。此配置适用于所有应用程序架构。

获取凭证:在 Singular 平台的 "开发者工具"→"SDK 集成"下查找 SDK 密钥和 SDK Secret。

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


初始化 SDK

选择您的应用程序架构

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

我使用的是哪种架构

  • 场景代理:检查您的项目是否有SceneDelegate.swiftSceneDelegate.m
  • SwiftUI:您的应用程序以@main struct YourApp: App开始
  • 仅 AppDelegate:iOS 13 之前的应用程序或没有 SceneDelegate 的应用程序

现代 iOS:SceneDelegate (iOS 13+)
#

现代 iOS:SceneDelegate (iOS 13+)

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

初始化的入口点

  • willConnectTo session - 应用程序启动
  • continue userActivity - 通用链接
  • openURLContexts - 深度链接方案
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)
    }
}
现代 iOS:SwiftUI 应用程序 (iOS 14+)
#

现代 iOS:SwiftUI 应用程序 (iOS 14+)

在哪里添加代码您的主Appstruct 文件

初始化的入口点

  • .onOpenURL(of: scenePhase) - 处理自定义 URL 方案
  • .onContinueUserActivity(of: scenePhase) - 处理通用链接(奇异深度链接)
  • .onChange.active - 如果未出现深度链接,则在首次启动时处理初始化。处理延迟的深度链接。
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.swiftAppDelegate.m

初始化的入口点

  • didFinishLaunchingWithOptions - 应用程序启动
  • continue userActivity - 通用链接
  • open url - 深度链接方案
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)
    }
}

验证安装

飞行前检查表

在构建和测试集成之前,请确认这些项目。

  • 通过 CocoaPods、SPM 或手动框架安装了 SDK
  • 创建了 Swift 桥接头(如果使用 Swift)
  • getConfig() 函数已执行
  • Singular.start(config) 在所有入口点调用
  • 将 SDK 密钥和 SDK Secret 添加到配置中
  • 配置 ATT 超时(仅在显示 ATT 提示时)
  • 配置了深度链接处理程序(仅在使用深度链接时)
  • 应用程序构建无误

下一步

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

可选:应用程序跟踪透明度 (ATT)

配置 ATT 以请求用户允许访问 IDFA 并提高归属准确性。

跳过本节,如果您的应用程序中未显示 ATT 提示。

为什么请求 ATT 同意?

IDFA 的好处

从 iOS 14.5 开始,应用程序必须请求用户允许才能访问设备的 IDFA(广告商标识符)。

有 IDFA 与无 IDFA 的归属

  • 有 IDFA:精确的设备级归属和准确的安装匹配
  • 无 IDFA:使用 IP、用户代理和设备指纹的概率归属

建议:请求 ATT 同意以提高归属准确性。奇异可以在没有 IDFA 的情况下进行归属,但准确性会降低。


配置 ATT 延迟

延迟 SDK 初始化

在向 Singular 发送第一个会话之前,添加一个等待用户 ATT 响应的超时。

关键:在发送第一个会话之前,SDK 必须等待 ATT 同意。否则,初始归属事件将不包括 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 流程时间表

下面是配置 ATT 延迟时发生的情况:

  1. 应用程序启动:SDK 开始记录事件,但尚未发送事件
  2. ATT 提示:您的应用程序显示 ATT 同意对话框
  3. 用户响应:用户同意或拒绝
  4. SDK 发送数据:SDK 会立即发送带有 IDFA 的队列事件(如果获得许可
  5. 超时回退:如果 300 秒后仍无响应,SDK 将继续发送数据

最佳实践:尽早显示 ATT 提示(最好是在首次启动应用程序时),以最大限度地提高 IDFA 的可用性,从而实现归因。