# iOS SDK

The Metica Ads SDK for iOS provides a simple, efficient way to integrate Metica's advertising functionality into your iOS applications. The SDK supports interstitial, rewarded, banner, and MREC ad formats with built-in A/B testing capabilities through Smart Floors.

## Requirements

* iOS 15.0 or higher
* Swift 5.0+
* Xcode 16.2+
* AppLovinSDK dependency (version 13.0.0 or higher)
* Valid Metica API credentials (API key, App ID)

{% hint style="warning" %}
**CRITICAL REQUIREMENT**

You must initialize the Metica SDK before invoking any ad methods. The Metica SDK handles AppLovin SDK initialization internally, so you should NOT initialize AppLovin SDK separately.
{% endhint %}

## Callback Interfaces

### MeticaInitCallback

Callback interface for SDK initialization events.

```swift
public protocol MeticaInitCallback {
    func onInit(initResponse: MeticaInitResponse)
}
```

| Method                  | Description                              |
| ----------------------- | ---------------------------------------- |
| `onInit(initResponse:)` | Called when the SDK has been initialized |

### MeticaAdsLoadCallback

Callback interface for ad loading events.

```swift
public protocol MeticaAdsLoadCallback {
    func onAdLoadSuccess(meticaAd: MeticaAd)
    func onAdLoadFailed(meticaAdError: MeticaAdError)
}
```

| Method                           | Description                                    |
| -------------------------------- | ---------------------------------------------- |
| `onAdLoadSuccess(meticaAd:)`     | Called when an ad has been successfully loaded |
| `onAdLoadFailed(meticaAdError:)` | Called when an ad load has failed              |

### MeticaAdsShowCallback

Callback interface for ad display events.

```swift
public protocol MeticaAdsShowCallback {
    func onAdShowSuccess(meticaAd: MeticaAd)
    func onAdShowFailed(meticaAd: MeticaAd, meticaAdError: MeticaAdError)
    func onAdHidden(meticaAd: MeticaAd)
    func onAdClicked(meticaAd: MeticaAd)
    func onAdRevenuePaid(meticaAd: MeticaAd)
    func onAdRewarded(meticaAd: MeticaAd)
}
```

| Method                                    | Description                                        |
| ----------------------------------------- | -------------------------------------------------- |
| `onAdShowSuccess(meticaAd:)`              | Called when an ad has been shown successfully      |
| `onAdShowFailed(meticaAd:meticaAdError:)` | Called when an ad failed to show                   |
| `onAdHidden(meticaAd:)`                   | Called when an ad has been dismissed               |
| `onAdClicked(meticaAd:)`                  | Called when the user clicks an ad                  |
| `onAdRevenuePaid(meticaAd:)`              | Called when revenue has been attributed            |
| `onAdRewarded(meticaAd:)`                 | Called when a reward is earned (rewarded ads only) |

### MeticaAdsBannerCallback

Callback interface for banner/MREC ad events. Extends `MeticaAdsLoadCallback`.

```swift
public protocol MeticaAdsBannerCallback: MeticaAdsLoadCallback {
    func onAdClicked(meticaAd: MeticaAd)
    func onAdRevenuePaid(meticaAd: MeticaAd)
}
```

***

## Privacy Settings

Set privacy settings **before** calling `MeticaSdk.initialize()`:

```swift
// Set user consent for personalized ads
MeticaSdk.privacySettings.setHasUserConsent(isConsent: true)

// Set "Do Not Sell" flag for CCPA compliance
MeticaSdk.privacySettings.setDoNotSell(isDoNotSell: false)
```

***

## Ad Formats

| Format       | Enum Value  | Description                              |
| ------------ | ----------- | ---------------------------------------- |
| Interstitial | `.INTER`    | Full-screen ads that cover the interface |
| Rewarded     | `.REWARDED` | Video ads that reward users for watching |
| Banner       | `.BANNER`   | Adaptive banner ads (320x50)             |
| MREC         | `.MREC`     | Medium rectangle ads (300x250)           |

***

## Code Examples

### SDK Initialization

```swift
import MeticaSDK

class AppViewModel: ObservableObject, MeticaInitCallback {
    @Published var isInitialized = false
    @Published var userGroup = ""

    @MainActor
    func initializeSDK() {
        // Enable logging for development
        MeticaLogger.enableLogs = true

        // Set privacy settings BEFORE initialization
        MeticaSdk.privacySettings.setHasUserConsent(isConsent: true)
        MeticaSdk.privacySettings.setDoNotSell(isDoNotSell: false)

        // Create configuration
        let initConfig = MeticaInitConfig(
            apiKey: "YOUR_API_KEY",
            appId: "YOUR_APP_ID",
            userId: "" // Leave empty for auto-generated stable ID
        )

        let mediationInfo = MeticaMediationInfo(
            mediationType: .max,
            key: "YOUR_APPLOVIN_SDK_KEY"
        )

        // Initialize the SDK
        MeticaSdk.initialize(
            initConfig: initConfig,
            mediationInfo: mediationInfo,
            callback: self
        )
    }

    // MARK: - MeticaInitCallback

    func onInit(initResponse: MeticaInitResponse) {
        DispatchQueue.main.async {
            self.isInitialized = true
            self.userGroup = initResponse.smartFloors.userGroup == .trial ? "trial" : "holdout"

            if initResponse.smartFloors.isSuccess {
                print("Metica SDK initialized successfully")
            } else {
                print("Metica SDK initialized in fallback mode")
            }
        }
    }
}
```

### Interstitial Ads

```swift
class InterstitialAdManager: MeticaAdsLoadCallback, MeticaAdsShowCallback {
    let adUnitId = "YOUR_INTERSTITIAL_AD_UNIT_ID"
    private var retryAttempt: Int = 0
    private let maxRetryDelay: Double = 64.0

    func loadAd() {
        MeticaSdk.Ads.loadInterstitial(adUnitId: adUnitId, callback: self)
    }

    func showAd() {
        if MeticaSdk.Ads.isInterstitialReady(adUnitId: adUnitId) {
            MeticaSdk.Ads.showInterstitial(adUnitId: adUnitId, callback: self)
        } else {
            print("Interstitial not ready")
        }
    }

    // MARK: - MeticaAdsLoadCallback

    func onAdLoadSuccess(meticaAd: MeticaAd) {
        // Reset retry state on success
        retryAttempt = 0
        print("Interstitial loaded: \(meticaAd.adUnitId)")
    }

    func onAdLoadFailed(meticaAdError: MeticaAdError) {
        print("Interstitial load failed: \(meticaAdError.message)")
        
        // Exponential backoff retry (2s, 4s, 8s, ... capped at 64s)
        retryAttempt += 1
        let delaySeconds = min(pow(2.0, Double(retryAttempt)), maxRetryDelay)

        DispatchQueue.main.asyncAfter(deadline: .now() + delaySeconds) { [weak self] in
            guard let self = self else { return }
            print("Retrying interstitial load in \(delaySeconds)s")
            self.loadAd()
        }
    }

    // MARK: - MeticaAdsShowCallback

    func onAdShowSuccess(meticaAd: MeticaAd) {
        print("Interstitial shown")
    }

    func onAdShowFailed(meticaAd: MeticaAd, meticaAdError: MeticaAdError) {
        print("Interstitial show failed: \(meticaAdError.message)")
    }

    func onAdHidden(meticaAd: MeticaAd) {
        print("Interstitial hidden")
        self.loadAd()
    }

    func onAdClicked(meticaAd: MeticaAd) {
        print("Interstitial clicked")
    }

    func onAdRevenuePaid(meticaAd: MeticaAd) {
        print("Interstitial revenue: \(meticaAd.revenue)")
    }

    func onAdRewarded(meticaAd: MeticaAd) {
        // Not called for interstitial ads
    }
}
```

### Rewarded Ads

```swift
class RewardedAdManager: MeticaAdsLoadCallback, MeticaAdsShowCallback {
    let adUnitId = "YOUR_REWARDED_AD_UNIT_ID"
    private var retryAttempt: Int = 0
    private let maxRetryDelay: Double = 64.0

    func loadAd() {
        MeticaSdk.Ads.loadRewarded(adUnitId: adUnitId, callback: self)
    }

    func showAd() {
        if MeticaSdk.Ads.isRewardedReady(adUnitId: adUnitId) {
            MeticaSdk.Ads.showRewarded(adUnitId: adUnitId, callback: self)
        } else {
            print("Rewarded ad not ready")
        }
    }

    // MARK: - MeticaAdsLoadCallback

    func onAdLoadSuccess(meticaAd: MeticaAd) {
        retryAttempt = 0
        print("Rewarded ad loaded")
    }

    func onAdLoadFailed(meticaAdError: MeticaAdError) {
        print("Rewarded ad load failed: \(meticaAdError.message)")
        
        // Exponential backoff retry (2s, 4s, 8s, ... capped at 64s)
        retryAttempt += 1
        let delaySeconds = min(pow(2.0, Double(retryAttempt)), maxRetryDelay)

        DispatchQueue.main.asyncAfter(deadline: .now() + delaySeconds) { [weak self] in
            guard let self = self else { return }
            print("Retrying rewarded load in \(delaySeconds)s")
            self.loadAd()
        }
    }

    // MARK: - MeticaAdsShowCallback

    func onAdShowSuccess(meticaAd: MeticaAd) {
        print("Rewarded ad shown")
    }

    func onAdShowFailed(meticaAd: MeticaAd, meticaAdError: MeticaAdError) {
        print("Rewarded ad show failed: \(meticaAdError.message)")
    }

    func onAdHidden(meticaAd: MeticaAd) {
        print("Rewarded ad hidden")
        self.loadAd()
    }

    func onAdClicked(meticaAd: MeticaAd) {
        print("Rewarded ad clicked")
    }

    func onAdRevenuePaid(meticaAd: MeticaAd) {
        print("Rewarded ad revenue: \(meticaAd.revenue)")
    }

    func onAdRewarded(meticaAd: MeticaAd) {
        print("User earned reward!")
        // Grant reward to user here
    }
}
```

### Banner/MREC Ads

```swift
import SwiftUI
import UIKit
import MeticaSDK

// SwiftUI wrapper for banner ads
struct BannerAdView: UIViewRepresentable {
    let adUnitId: String
    let adFormat: MeticaAdFormatType
    let callback: MeticaAdsBannerCallback

    func makeUIView(context: Context) -> UIView {
        let container = BannerContainerView()
        container.loadAd(adUnitId: adUnitId, format: adFormat, callback: callback)
        return container
    }

    func updateUIView(_ uiView: UIView, context: Context) {}

    static func dismantleUIView(_ uiView: UIView, coordinator: ()) {
        (uiView as? BannerContainerView)?.cleanup()
    }
}

class BannerContainerView: UIView {
    private var currentAdView: MeticaAdView?

    func loadAd(adUnitId: String, format: MeticaAdFormatType, callback: MeticaAdsBannerCallback) {
        Task { @MainActor in
            let adView = await MeticaSdk.Ads.createBannerOrMrecAdView(
                adUnitId: adUnitId,
                adFormat: format
            )
            currentAdView = adView

            if let uiView = adView as? UIView {
                addSubview(uiView)
                uiView.translatesAutoresizingMaskIntoConstraints = false

                // Add constraints based on format
                if format == .BANNER {
                    NSLayoutConstraint.activate([
                        uiView.leadingAnchor.constraint(equalTo: leadingAnchor),
                        uiView.trailingAnchor.constraint(equalTo: trailingAnchor),
                        uiView.topAnchor.constraint(equalTo: topAnchor),
                        uiView.heightAnchor.constraint(equalToConstant: 50)
                    ])
                } else if format == .MREC {
                    NSLayoutConstraint.activate([
                        uiView.centerXAnchor.constraint(equalTo: centerXAnchor),
                        uiView.centerYAnchor.constraint(equalTo: centerYAnchor),
                        uiView.widthAnchor.constraint(equalToConstant: 300),
                        uiView.heightAnchor.constraint(equalToConstant: 250)
                    ])
                }
            }

            adView.setListener(callback: callback)
            adView.load()
        }
    }

    func cleanup() {
        currentAdView?.destroy()
        currentAdView = nil
    }
}

// Banner callback implementation
class BannerAdCallback: MeticaAdsBannerCallback {
    func onAdLoadSuccess(meticaAd: MeticaAd) {
        print("Banner loaded")
    }

    func onAdLoadFailed(meticaAdError: MeticaAdError) {
        print("Banner load failed: \(meticaAdError.message)")
    }

    func onAdClicked(meticaAd: MeticaAd) {
        print("Banner clicked")
    }

    func onAdRevenuePaid(meticaAd: MeticaAd) {
        print("Banner revenue: \(meticaAd.revenue)")
    }
}
```

***

## Best Practices

1. **Initialize on Main Thread**: Always call `MeticaSdk.initialize()` from the main thread using `@MainActor`.
2. **Set Privacy Settings First**: Configure privacy settings before calling `initialize()` to ensure proper propagation to ad networks.
3. **Enable Debug Logging**: During development, enable logging for easier debugging:

   ```swift
   MeticaLogger.enableLogs = true
   ```
4. **Check Ad Readiness**: Always verify ads are ready before showing:

   ```swift
   if MeticaSdk.Ads.isInterstitialReady(adUnitId: adUnitId) {
       MeticaSdk.Ads.showInterstitial(adUnitId: adUnitId, callback: self)
   }
   ```
5. **Clean Up Banner/MREC Views**: Call `destroy()` on banner and MREC ad views when they're no longer needed to free resources.
6. **Handle All Callbacks**: Implement both success and failure callbacks to handle all scenarios gracefully.
7. **Thread Safety**: All SDK methods are thread-safe. Callbacks are delivered on the main thread for UI updates.

***

## Smart Floors / User Groups

The SDK uses Smart Floors for A/B testing optimization. After initialization, users are assigned to one of two groups:

| User Group | isSuccess | Description                                                 |
| ---------- | --------- | ----------------------------------------------------------- |
| `trial`    | `true`    | Metica optimization is enabled for this user                |
| `holdout`  | `true`    | User is in the control group (no optimization)              |
| `holdout`  | `false`   | Initialization failed; user defaults to holdout as fallback |

Access the user's group after initialization:

```swift
func onInit(initResponse: MeticaInitResponse) {
    let userGroup = initResponse.smartFloors.userGroup // .trial or .holdout
    let isSuccess = initResponse.smartFloors.isSuccess

    if userGroup == .trial && isSuccess {
        print("User has Metica optimization enabled")
    } else if userGroup == .holdout && isSuccess {
        print("User is in control group")
    } else {
        print("Initialization failed, running in fallback mode")
    }
}
```


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.metica.com/api/ios-sdk.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
