Apple Search Ads Event Tracking | OpsBlu Docs

Apple Search Ads Event Tracking

Configure conversion events and SKAdNetwork conversion values for Apple Search Ads tracking.

Conversion Event Types

Install Attribution

Track app installations from Search Ads:

import AdServices

func trackInstallAttribution() {
    guard let token = try? AAAttribution.attributionToken() else {
        print("No attribution token available")
        return
    }

    // Send to your server for Apple API validation
    validateAttribution(token: token)
}

func validateAttribution(token: String) {
    let endpoint = "https://api-adservices.apple.com/api/v1/"
    var request = URLRequest(url: URL(string: endpoint)!)
    request.httpMethod = "POST"
    request.setValue("text/plain", forHTTPHeaderField: "Content-Type")
    request.httpBody = token.data(using: .utf8)

    URLSession.shared.dataTask(with: request) { data, response, error in
        guard let data = data else { return }

        if let attribution = try? JSONDecoder().decode(AttributionResponse.self, from: data) {
            print("Attribution: \(attribution)")
            // Store attribution data
        }
    }.resume()
}

struct AttributionResponse: Codable {
    let attribution: Bool
    let orgId: Int?
    let campaignId: Int?
    let conversionType: String?
    let adGroupId: Int?
    let countryOrRegion: String?
    let keywordId: Int?
    let adId: Int?
}

In-App Purchase Events

Track revenue events with SKAdNetwork:

import StoreKit

func trackPurchase(product: SKProduct, quantity: Int) {
    let revenue = product.price.doubleValue * Double(quantity)
    let conversionValue = mapRevenueToConversionValue(revenue)

    if #available(iOS 14.0, *) {
        SKAdNetwork.updateConversionValue(conversionValue)
    }

    // Also send to your analytics
    logPurchaseEvent(product: product, quantity: quantity, revenue: revenue)
}

func mapRevenueToConversionValue(_ revenue: Double) -> Int {
    // Map revenue to 0-63 scale for SKAdNetwork
    switch revenue {
    case 0..<1: return 0
    case 1..<5: return 1
    case 5..<10: return 2
    case 10..<20: return 3
    case 20..<50: return 4
    case 50..<100: return 5
    default: return 6
    }
}

Registration / Sign Up

Track user registration as conversion:

func trackRegistration() {
    if #available(iOS 14.0, *) {
        SKAdNetwork.updateConversionValue(10) // Custom value for registration
    }

    // Send to analytics
    logEvent("user_registration", parameters: [
        "method": "email",
        "timestamp": Date()
    ])
}

Tutorial Completion

Track onboarding completion:

func trackTutorialComplete() {
    if #available(iOS 14.0, *) {
        SKAdNetwork.updateConversionValue(15) // Custom value for tutorial
    }

    logEvent("tutorial_complete", parameters: [
        "steps_completed": 5,
        "time_spent": 120
    ])
}

SKAdNetwork Conversion Value Schema

Basic Schema (iOS 14.0+)

Define a conversion value mapping for 6-bit values (0-63):

enum ConversionEvent: Int {
    case install = 0
    case appOpen = 1
    case registration = 10
    case tutorialComplete = 15
    case addToCart = 20
    case initiateCheckout = 25
    case purchase = 30
    case subscription = 35
    case level5Complete = 40
    case level10Complete = 45
    case highValuePurchase = 50
    case retention3Day = 55
    case retention7Day = 60
}

func updateConversionValue(event: ConversionEvent) {
    if #available(iOS 14.0, *) {
        SKAdNetwork.updateConversionValue(event.rawValue)
    }
}

Advanced Schema with Revenue Tiers (iOS 16.1+)

@available(iOS 16.1, *)
func trackRevenueEvent(revenue: Double, eventType: String) {
    let fineValue = calculateFineValue(revenue: revenue, eventType: eventType)
    let coarseValue = calculateCoarseValue(revenue: revenue)

    SKAdNetwork.updatePostbackConversionValue(
        fineValue: fineValue,
        coarseValue: coarseValue
    ) { error in
        if let error = error {
            print("Failed to update conversion: \(error)")
        }
    }
}

@available(iOS 16.1, *)
func calculateCoarseValue(revenue: Double) -> SKAdNetwork.CoarseConversionValue {
    switch revenue {
    case 0..<10:
        return .low
    case 10..<50:
        return .medium
    default:
        return .high
    }
}

func calculateFineValue(revenue: Double, eventType: String) -> Int {
    // Encode both event type and revenue into 6 bits
    let eventBits = eventTypeBits(eventType) << 3 // 3 bits for event type
    let revenueBits = revenueTierBits(revenue)     // 3 bits for revenue
    return eventBits | revenueBits
}

func eventTypeBits(_ eventType: String) -> Int {
    switch eventType {
    case "purchase": return 0
    case "subscription": return 1
    case "add_to_cart": return 2
    case "registration": return 3
    default: return 7
    }
}

func revenueTierBits(_ revenue: Double) -> Int {
    switch revenue {
    case 0..<5: return 0
    case 5..<10: return 1
    case 10..<20: return 2
    case 20..<50: return 3
    case 50..<100: return 4
    default: return 5
    }
}

Custom Event Tracking

Level Completion (Gaming)

func trackLevelComplete(level: Int, score: Int) {
    let conversionValue = min(level, 63) // Cap at max SKAdNetwork value

    if #available(iOS 14.0, *) {
        SKAdNetwork.updateConversionValue(conversionValue)
    }

    logEvent("level_complete", parameters: [
        "level": level,
        "score": score,
        "duration": calculateLevelDuration()
    ])
}

Content View (News/Media)

func trackContentView(articleId: String, category: String) {
    // Use different conversion values for different content categories
    let conversionValue: Int
    switch category {
    case "premium":
        conversionValue = 30
    case "standard":
        conversionValue = 20
    default:
        conversionValue = 10
    }

    if #available(iOS 14.0, *) {
        SKAdNetwork.updateConversionValue(conversionValue)
    }

    logEvent("content_view", parameters: [
        "article_id": articleId,
        "category": category
    ])
}

Subscription Events

func trackSubscription(tier: String, price: Double, period: String) {
    let conversionValue: Int
    switch tier {
    case "premium":
        conversionValue = 60
    case "standard":
        conversionValue = 50
    case "basic":
        conversionValue = 40
    default:
        conversionValue = 35
    }

    if #available(iOS 14.0, *) {
        SKAdNetwork.updateConversionValue(conversionValue)
    }

    logEvent("subscribe", parameters: [
        "tier": tier,
        "price": price,
        "period": period
    ])
}

Time-Based Conversion Values

D1, D3, D7 Retention

class RetentionTracker {
    static let installDate = "install_date"

    func trackRetention() {
        guard let installDate = UserDefaults.standard.object(forKey: RetentionTracker.installDate) as? Date else {
            // First launch - store install date
            UserDefaults.standard.set(Date(), forKey: RetentionTracker.installDate)
            return
        }

        let daysSinceInstall = Calendar.current.dateComponents([.day], from: installDate, to: Date()).day ?? 0

        let conversionValue: Int
        switch daysSinceInstall {
        case 1:
            conversionValue = 55 // D1 retention
        case 3:
            conversionValue = 58 // D3 retention
        case 7:
            conversionValue = 60 // D7 retention
        default:
            return
        }

        if #available(iOS 14.0, *) {
            SKAdNetwork.updateConversionValue(conversionValue)
        }
    }
}

MMP Event Tracking

AppsFlyer Events

import AppsFlyerLib

func trackAFEvent(eventName: String, revenue: Double? = nil) {
    var eventValues: [String: Any] = [:]

    if let revenue = revenue {
        eventValues[AFEventParamRevenue] = revenue
        eventValues[AFEventParamCurrency] = "USD"
    }

    AppsFlyerLib.shared().logEvent(eventName, withValues: eventValues)
}

// Purchase event
func trackPurchaseWithAF(orderId: String, revenue: Double, items: [String]) {
    trackAFEvent(
        eventName: AFEventPurchase,
        revenue: revenue
    )
}

Adjust Events

import Adjust

func trackAdjustEvent(token: String, revenue: Double? = nil) {
    let event = ADJEvent(eventToken: token)

    if let revenue = revenue {
        event?.setRevenue(revenue, currency: "USD")
    }

    Adjust.trackEvent(event)
}

Testing & Validation

Test Mode (Development)

#if DEBUG
func testSKAdNetworkPostback() {
    // In development, conversions update faster
    if #available(iOS 14.0, *) {
        SKAdNetwork.updateConversionValue(99) // Test value
        print("Test conversion value sent")
    }
}
#endif

Logging for Debug

func logConversionEvent(event: String, value: Int) {
    print("Conversion Event: \(event), Value: \(value)")

    #if DEBUG
    // Additional debug logging
    let debugInfo = """
    Event: \(event)
    Value: \(value)
    Timestamp: \(Date())
    User: \(getCurrentUserId())
    """
    print(debugInfo)
    #endif
}

Best Practices

  • Update SKAdNetwork conversion value within 24 hours of install for optimal attribution
  • Use progressive conversion values (lower for early events, higher for valuable actions)
  • Document your conversion value schema for team reference
  • Test conversion values in TestFlight before production release
  • Balance between granularity and privacy in conversion mapping
  • Consider time-based events for retention measurement
  • Implement both SKAdNetwork and MMP tracking for comprehensive attribution
  • Monitor conversion value distribution in your analytics