iOS SDK - 기본 연동

문서

전제 조건

Singular SDK 연동의 단계를 완료합니다: 계획 및 사전 요구 사항의 단계를 완료한 후에 이 연동을 진행하세요.

중요: 이러한 필수 단계는 모든 Singular SDK 연동에 필요합니다.

설치

원하는 설치 방법을 선택합니다. 대부분의 프로젝트에는 CocoaPods를 권장합니다.

빠른 결정:

  • 이미 CocoaPods를 사용하고 계신가요? 방법 1 사용
  • SPM 전용 프로젝트인가요? 방법 2 사용
  • 패키지 관리자가 없으신가요? 방법 3 사용

설치 방법

방법 1: CocoaPods(권장)
#

방법 1: CocoaPods(권장)

요구 사항

  • CocoaPods 설치(설치 가이드)
  • 프로젝트 디렉토리에 대한 터미널 액세스

설치 단계:

  1. 포드파일 초기화 (이미 있는 경우 건너뛰기):
    Terminal
    cd /path/to/your/project
    pod init
  2. Podfile에 Singular SDK를 추가합니다:
    Podfile
    platform :ios, '12.0'
    
    target 'YourAppName' do
      use_frameworks!
      
      # Singular SDK
      pod 'Singular-SDK'
      
    end
  3. 종속 요소를 설치한다:
    Terminal
    pod install
  4. 워크스페이스 열기: 이제부터는 .xcodeproj대신 .xcworkspace 을 엽니다.
  5. Swift 프로젝트만 해당: 브리징 헤더를 생성합니다(아래 참조).
방법 2: Swift 패키지 관리자
#

방법 2: Swift 패키지 관리자

설치 단계:

  1. Xcode: 파일 → 패키지 추가
  2. 리포지토리 URL 입력: https://github.com/singular-labs/Singular-iOS-SDK
  3. 버전을 선택하고 패키지 추가를클릭합니다.
  4. 필요한 프레임워크를 추가합니다:
    빌드 단계 → 라이브러리와 바이너리 연결로 이동하여 추가합니다:
    • 필요한 라이브러리를 연결합니다:
      빌드 단계 → 라이브러리와 바이너리 링크로 이동하여 추가합니다:
      • Libsqlite3.0.tbd
      • SystemConfiguration.framework
      • Security.framework
      • Libz.tbd
      • AdSupport.framework
      • WebKit.framework
      • StoreKit.프레임워크
      • AdServices.framework(선택 사항으로 표시)
  5. Swift 프로젝트만 해당: 브리징 헤더 생성(아래 참조)
방법 3: 수동 프레임워크 설치
#

방법 3: 수동 프레임워크 설치

사용 시기: 코코아팟 또는 SPM을 사용할 수 없는 경우에만 이 방법을 사용하세요.

프레임워크 다운로드:

설치 단계:

  1. 다운로드한 프레임워크의 압축을 풉니다.
  2. Xcode에서: 프로젝트 → [프로젝트]에 파일 추가를마우스 오른쪽 버튼으로 클릭합니다.
  3. 그룹 생성을 선택하고 프레임워크 폴더를 추가합니다.
  4. 필요한 라이브러리를 연결합니다:
    빌드 단계 → 라이브러리와 바이너리 링크로이동하여 추가합니다:
    • Libsqlite3.0.tbd
    • SystemConfiguration.framework
    • Security.framework
    • Libz.tbd
    • AdSupport.framework
    • WebKit.framework
    • StoreKit.프레임워크
    • AdServices.framework(선택 사항으로 표시)
  5. 프레임워크 임베드:
    일반 → 프레임워크, 라이브러리 및 임베드된 콘텐츠로 이동합니다.
    Singular 프레임워크를 임베드 및 서명으로설정합니다.

스위프트 브리징 헤더

중요: 코코아팟 또는 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를 초기화합니다.


구성 개체 생성

기본 구성

SDK 자격 증명과 선택적 기능을 사용하여 SingularConfig 개체를 만듭니다. 이 구성은 모든 앱 아키텍처에 공통적으로 적용됩니다.

자격 증명 가져오기: 개발자 도구 → SDK 연동의 Singular 플랫폼에서 SDK 키와 SDK 시크릿을 찾을 수 있습니다.

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: 프로젝트에 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 - 유니버설 링크
  • 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+)

코드를 추가할 위치: 기본 App구조 파일

초기화할 엔트리 포인트:

  • .onOpenURL(of: scenePhase) - 사용자 정의 URL 체계 처리
  • .onContinueUserActivity(of: scenePhase) - 유니버설 링크 처리(Singular 딥링크)
  • .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: 앱 델리게이트(iOS 13 이전)
#

레거시 iOS: 앱디렉티브(iOS 13 이전)

코드를 추가할 위치: AppDelegate.swift 또는 AppDelegate.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)
    }
}

설치 확인

비행 전 체크리스트

연동을 빌드하고 테스트하기 전에 다음 항목을 확인하세요.

  • 코코아팟, SPM 또는 수동 프레임워크를 통해 설치된 SDK
  • 스위프트 브리징 헤더 생성(스위프트를 사용하는 경우)
  • getConfig() 구현된 함수
  • Singular.start(config) 모든 진입점에서 호출됨
  • 구성에 SDK 키 및 SDK 시크릿 추가됨
  • ATT 타임아웃 구성됨(ATT 프롬프트가 표시되는 경우에만)
  • 딥링크 핸들러 구성됨(딥링크를 사용하는 경우에만)
  • 오류 없이 앱 빌드

다음 단계:

  • 앱 빌드 및 실행
  • 콘솔에서 IDFV 인쇄 문 확인
  • IDFV를 사용하여 Singular SDK 콘솔에서테스트하기
  • 1~2분 이내에 SDK 콘솔에 세션이 표시되는지 확인합니다.

선택 사항: 앱 추적 투명성(ATT)

IDFA 액세스에 대한 사용자 권한을 요청하고 어트리뷰션 정확도를 개선하도록 ATT를 구성합니다.

다음과 같은 경우 이 섹션을 건너뛰세요: 앱에 ATT 프롬프트가 표시되지 않는 경우.

ATT 동의를 요청하는 이유는 무엇인가요?

IDFA의 이점

iOS 14.5부터 앱은 기기의 IDFA(광고주 식별자)에 액세스하기 위해 사용자에게 권한을 요청해야 합니다.

IDFA가 있는 경우와 없는 경우의 어트리뷰션 비교:

  • IDFA 사용: 정밀한 디바이스 수준 어트리뷰션 및 정확한 인스톨 매칭
  • IDFA 미사용: IP, 사용자 에이전트, 디바이스 핑거프린팅을 사용한 확률론적 어트리뷰션

권장 사항: 어트리뷰션 정확도를 높이기 위해 ATT 동의를 요청하세요. IDFA 없이도 Singular 어트리뷰션이 가능하지만 정확도가 떨어집니다.


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가 데이터를 전송합니다.

모범 사례: 어트리뷰션에 대한 IDFA 가용성을 극대화하려면 가능한 한 빨리(이상적으로는 앱을 처음 실행할 때) ATT 프롬프트를 표시하세요.