Menu

Ad Detector - iOS Integration Instructions

1. Introduction

MaterialMonitor is an ad creative monitoring SDK. It captures ad screenshots (including video-frame composition) plus metadata during ad display, and uploads them automatically to the creative audit server for compliance review and quality monitoring.

Core capabilities

Capability Description
Screenshot capture Supports splash / Banner / interstitial / rewarded video / native ads
Video composition capture Automatically detects AVPlayerLayer and composes video frames plus UIKit overlays
Persistent queue Sandbox persistence + cold-start recovery + retries + network awareness
Automatic upload Built-in signing (HMAC-MD5) + multipart upload + rate-limit retry strategy
Privacy protection Automatically masks password fields / firstResponder; supports custom masked regions
Policy awareness Supports dynamic enable/disable by placementId / networkFirmId / style

Installation Instructions

CocoaPods Install

  1. Ensure that the minimum iOS version for the project is 13.0.

  2. Add the dependency to the Podfile:

    ruby Copy
    platform :ios, '13.0'
    
    target 'YourTarget' do
      pod 'TPNMaterialMonitorSDK', '~> 1.0.0'
    end
  3. Perform the installation in the directory where the Podfile is located:

    bash Copy
    pod install --repo-update
  4. Then open the project using the generated .xcworkspace. The SDK module name is MaterialMonitorSDK, used in Swift as follows:

    swift Copy
    import MaterialMonitorSDK

    Using automatically generated Swift header files in Objective-C:

    objc Copy
    #import <MaterialMonitorSDK/MaterialMonitorSDK-Swift.h>

Framework Download method

TPNMaterialMonitorSDK.podspec downloads the framework archive through s.source. Download link:

text Copy
http://info.appsmartsite.com/Material_monitor/nochina/1.0.0/MaterialMonitorSDK-1.0.0.zip

After decompression, the compressed file contains the following directory structure:

text Copy
MaterialMonitorSDK-1.0.0/
  MaterialMonitorSDK.xcframework/

2. Initialization and Shutdown

2.1 Initialization (required)

When to call: recommended after the ATT authorization callback returns (so IDFA is available). Call it only once during the entire app lifecycle.

Swift

swift Copy
import MaterialMonitorSDK
import AppTrackingTransparency
import AdSupport

class AppDelegate: UIResponder, UIApplicationDelegate {

    // Recommended: run the ATT authorization flow in applicationDidBecomeActive
    func applicationDidBecomeActive(_ application: UIApplication) {
        if #available(iOS 14, *) {
            ATTrackingManager.requestTrackingAuthorization { _ in
                self.initMaterialMonitor()
            }
        } else {
            initMaterialMonitor()
        }
    }

    private func initMaterialMonitor() {
        let appId      = "your_app_id"
        let appKey     = "your_app_key"
        let idfa       = ASIdentifierManager.shared().advertisingIdentifier.uuidString
        let sdkVersion = "your_host_sdk_version"                   // host aggregator SDK version

        // MARK: - MaterialMonitor SDK initialization (full parameters)
        let config = MaterialMonitorConfig.Builder()
            // -- upload service URL -----------------------------------------
            .baseUploadUrl("https://your-upload-host.com")
            // -- authentication ---------------------------------------------
            .appId(appId)
            .appKey(appKey)
            // -- device identifiers ----------------------------------------
            .idfa(idfa)                 // pass ATTrackingManager.advertisingIdentifier.uuidString after ATT
            .idfv(nil)                  // when nil, SDK reads UIDevice.current.identifierForVendor automatically
            .sdkVersion(sdkVersion)     // host aggregator SDK version, written into meta device_info.sdk_version
            .caid(nil)                  // CAID (China Advertising ID, optional)
            // -- network timeout (ms) --------------------------------------
            .connectTimeoutMs(15_000)   // TCP connect timeout, default 15s, minimum 3s
            .readTimeoutMs(30_000)      // server response timeout, default 30s, minimum 3s
            // -- retry policy ----------------------------------------------
            .maxUploadAttempts(3)              // first send + up to 2 retries, minimum 1
            .retryDelayMs([5_000, 15_000])     // retry delays (Swift only)
            // -- debugging (keep false in production) ----------------------
            .debugPerfLogging(false)
            .build()

        MaterialMonitor.initialize(config: config)
    }
}

Objective-C

objc Copy
#import <MaterialMonitorSDK/MaterialMonitorSDK-Swift.h>
#import <AppTrackingTransparency/AppTrackingTransparency.h>
#import <AdSupport/AdSupport.h>

- (void)applicationDidBecomeActive:(UIApplication *)application {
    if (@available(iOS 14, *)) {
        [ATTrackingManager requestTrackingAuthorizationWithCompletionHandler:^(ATTrackingManagerAuthorizationStatus status) {
            [self initMaterialMonitor];
        }];
    } else {
        [self initMaterialMonitor];
    }
}

- (void)initMaterialMonitor {
    NSString *appId      = @"your_app_id";
    NSString *appKey     = @"your_app_key";
    NSString *idfa       = [[[ASIdentifierManager sharedManager] advertisingIdentifier] UUIDString];
    NSString *sdkVersion = @"6.5.0";

    MMConfigBuilder *builder = [[MMConfigBuilder alloc] init];
    // -- upload service URL --
    [builder baseUploadUrl:@"https://your-upload-host.com"];
    // -- authentication --
    [builder appId:appId];
    [builder appKey:appKey];
    // -- device identifiers --
    [builder idfa:idfa];
    [builder idfv:nil];
    [builder sdkVersion:sdkVersion];
    [builder caid:nil];
    // -- network timeout (ms) --
    [builder connectTimeoutMs:15000];
    [builder readTimeoutMs:30000];
    // -- retry policy (ObjC does not support custom retryDelayMs, use default [5000, 15000]) --
    [builder maxUploadAttempts:3];
    // -- debugging (keep NO in production) --
    [builder debugPerfLogging:NO];

    MaterialMonitorConfig *config = [builder build];
    [MaterialMonitor initializeWithConfig:config];
}

Tip 1: MaterialMonitor is a singleton (MaterialMonitor.shared). After initialization, call all instance methods through MaterialMonitor.shared.xxx().

Tip 2: Every parameter has a reasonable default. Only appId and appKey are strictly required for the SDK to work; override the others as needed.

Tip 3: ObjC does not support retryDelayMs(_:) (Swift array bridging limitation). ObjC projects use the built-in default [5000, 15000].

2.2 Reinitialization

You may call initialize(config:) repeatedly at runtime. The SDK will automatically tear down the old scheduler (cancel timers, interrupt in-flight requests, clear callbacks), then rebuild with the new config. Cold-start scanning will run again, while existing tasks on disk are preserved.

2.3 Shutting down the SDK

swift Copy
MaterialMonitor.shutdown()
objc Copy
[MaterialMonitor shutdown];

Behavior after shutdown:

Item Behavior
Capture / upload methods Return MMErrorCodeUninitialized immediately
In-flight upload requests Hard-cancelled (URLSession invalidateAndCancel)
Retry timers, network listeners Released immediately
Business callback registry Cleared (already-issued callbacks are delivered normally; later callbacks are silently dropped)
Records persisted on disk Kept and continue uploading on the next cold-start scan after initialize
Config Cleared

Idempotent: calling shutdown() multiple times is safe.
Restartable: calling initialize again after shutdown restores full functionality.


3. MaterialMonitorConfig

Build it with the Builder pattern. All config items and defaults are listed below:

Field Type Default Description
baseUploadUrl String "" Upload root domain (for example "https://api.example.com"). The SDK automatically appends /v1/creative/upload
appId String "" App ID, written to multipart field app_id
appKey String "" HMAC-MD5 signing key. Never print, cache, or upload in plaintext
idfa String? nil IDFA after ATT authorization. The SDK passes it through directly and does not filter all-zero values
idfv String? nil IDFV. If nil, the SDK reads UIDevice.current.identifierForVendor automatically
sdkVersion String? nil Host aggregator SDK version, written into meta device_info.sdk_version
caid String? nil CAID (China Advertising ID)
connectTimeoutMs Int 15000 TCP connect timeout (ms), minimum 3000
readTimeoutMs Int 30000 Server response timeout (ms), minimum 3000
maxUploadAttempts Int 3 Max upload attempts (initial send + retries), minimum 1
retryDelayMs [Int] [5000, 15000] Retry delays (ms). Swift only; ObjC uses the default value
maxCacheSizeMb Int 100 Upper bound for the sandbox material_monitor/ directory (MB), minimum 10
debugPerfLogging Bool false Enable verbose debug logs. Must be off in production

Full configuration example (Swift)

swift Copy
let config = MaterialMonitorConfig.Builder()
    .baseUploadUrl("https://your-upload-host.com")
    .appId(appId)
    .appKey(appKey)
    .idfa(idfa)                          // pass in after ATT authorization
    .idfv(nil)                           // SDK reads automatically when nil
    .sdkVersion(sdkVersion)
    .connectTimeoutMs(15_000)
    .readTimeoutMs(30_000)
    .maxUploadAttempts(3)
    .retryDelayMs([5_000, 15_000])
    .debugPerfLogging(false)
    .build()

4. Capture APIs

The SDK provides two capture APIs. Choose based on whether the ad has an accessible container view:

Ad form Applicable type Recommended API
With container Banner / native ads collectFrameFromAdContainer
Without container Splash / interstitial / rewarded video collectFrameFromAdPage

All capture methods must be called on the main thread. Screenshot capture and upload callbacks are both dispatched on the main thread.

4.1 Ads with a container (Banner / native ads)

swift Copy
public func collectFrameFromAdContainer(
    _ view: UIView,
    adInfo: MMAdInfo,
    timing: AdTimingInput,
    options: FrameOptions,
    callback: FrameCallback
)

Parameters:

Parameter Type Description
view UIView Ad container view
adInfo MMAdInfo Ad information (see 7.4)
timing AdTimingInput Ad timestamps (see 7.1)
options FrameOptions Screenshot options (see 7.2)
callback FrameCallback Capture + auto-upload callback (weak reference, see 6)

Swift example (Banner):

swift Copy
func onBannerAdShow(_ bannerView: UIView, extra: [String: Any]) {
    MaterialMonitor.shared.collectFrameFromAdContainer(
        bannerView,
        adInfo: buildMMAdInfo(from: extra),
        timing: buildTiming(extra),
        options: FrameOptions(delayMs: 200),  // Banner can use a shorter delay
        callback: self
    )
}

4.2 Ads without a container (Splash / interstitial / rewarded video)

swift Copy
public func collectFrameFromAdPage(
    adInfo: MMAdInfo,
    timing: AdTimingInput,
    options: FrameOptions,
    callback: FrameCallback
)

The SDK will automatically locate the topmost presentedViewController view; if none exists, it captures the keyWindow.

Swift example (Splash):

swift Copy
func splashAdDidShow() {
    MaterialMonitor.shared.collectFrameFromAdPage(
        adInfo: adInfo,
        timing: timing,
        options: FrameOptions(delayMs: 3000),  // give splash video enough render time, in ms
        callback: self
    )
}

5. Manual Upload and Queue Management

5.1 Manual enqueue for upload

Re-enqueue an existing FrameRecord (for example when the business side wants delayed upload or cross-process recovery).

swift Copy
public func reportFrameRecord(
    _ record: FrameRecord,
    adInfo: MMAdInfo,
    callback: ReportCallback?
)

Notes:

  • record must be one previously obtained from collectFrame* and must contain a valid localFilePath
  • Triggers both show_id and upload_task_id deduplication
  • Callback is optional and only cares about upload results

5.2 Trigger pending uploads immediately

swift Copy
MaterialMonitor.shared.flushPendingUploads()

Usually you do not need to call this manually. The SDK triggers it automatically after successful capture, network recovery, or when retries become due.

5.3 Query records

swift Copy
// Swift
let records = MaterialMonitor.shared.getRecords(style: .banner)

// ObjC (by adType integer)
NSArray<FrameRecord *> *records = [[MaterialMonitor shared] getRecordsForAdType:2];

Returns all records in non-dead states (pending / uploading / retryWaiting).

5.4 Delete a record

swift Copy
MaterialMonitor.shared.deleteRecord(record)

Behavior branches:

Current state Behavior
pending / retryWaiting Delete the file pair immediately, no callback
uploading Mark as dead; delete silently after HTTP completes
dead Delete the file pair immediately

5.5 Get current config

swift Copy
let config = MaterialMonitor.shared.getConfig()

Returns nil when not initialized or after shutdown.


6. Callback Protocols

6.1 FrameCallback (capture + upload)

swift Copy
@objc public protocol FrameCallback: AnyObject {
    func onCollectSuccess(_ record: FrameRecord)
    func onCollectFailure(errorCode: Int, errorName: String, message: String)
    func onReportSuccess(_ record: FrameRecord)
    func onReportSkip(_ record: FrameRecord, reason: String)
    func onReportFailure(errorCode: Int, errorName: String, message: String, record: FrameRecord?)
}
Callback When it fires
onCollectSuccess Screenshot success + persisted to disk + enqueued
onCollectFailure Screenshot or enqueue failure (no later upload callbacks will fire)
onReportSuccess Server returns code=0
onReportSkip show_id already exists (duplicated_show_id)
onReportFailure Terminal failure such as retries exhausted / auth failure / parameter error / quota exhausted

6.2 ReportCallback (upload only)

Used by reportFrameRecord. Signature is the same as the upload portion of FrameCallback.

6.3 Key characteristics ⚠️

  • All callbacks are dispatched on the main thread, so they can access UI directly
  • Callbacks are weakly referenced (weak); after the ViewController is released, callbacks are silently dropped and will not leak
  • During the capture flow (delay + screenshot + compression + persistence), the callback is held strongly. The VC will not be released during that time (up to about 3 seconds + processing time), so take this into account

7. Data Models

7.1 AdTimingInput

swift Copy
public init(requestTimeMs: Int64, fillTimeMs: Int64, showTimeMs: Int64)

Timestamps for each ad stage, in milliseconds. Convenience constructor:

swift Copy
let timing = AdTimingInput.sameWallClockMs(Int64(Date().timeIntervalSince1970 * 1000))

7.2 FrameOptions

swift Copy
public init(
    delayMs: Int           = 500,
    maxSizeKb: Int         = 300,
    maxLongEdge: Int       = 1080,
    keepQualityFirst: Bool = true,
    excludeViews: [UIView] = []
)
Field Default Description
delayMs 500 Delay in milliseconds for waiting until the ad creative finishes loading. For video ads, 2000+ is recommended; Banner and native ads can usually use 200 to 300
maxSizeKb 300 Target maximum JPEG size (KB)
maxLongEdge 1080 Maximum pixel length of the longest JPEG edge
keepQualityFirst true Whether to prioritize image quality (reduce quality starting from 0.7)
excludeViews [] Subviews to mask (privacy regions, such as custom password boxes)

7.3 FrameRecord

Represents one capture result. Business code usually reads it only.

Field Type Description
showId String Ad impression ID (server idempotency key)
uploadTaskId String Internal SDK UUID
placementId String Ad placement ID
style MaterialStyle Ad style (Swift)
adType Int Ad type integer (ObjC-friendly)
requestTimeMs / fillTimeMs / showTimeMs Int64 Timestamps (ms)
imageUrl String? Native creative URL (native only)
localFilePath String Absolute sandbox path
imageHash String pHash (16-char lowercase hex)
createdAt Int64 Capture timestamp (ms)

7.4 MMAdInfo

Ad information, constructed by business code in the onAdShow callback:

Field Type Required Description
requestId String Yes Request ID
showId String Yes Unique ad impression ID (idempotency key)
placementId String Yes Ad placement ID
adSourceId String Yes Ad source ID
adNetworkFirmId Int Yes Ad network ID
adType Int Yes Ad type integer: 0=Native / 1=RewardedVideo / 2=Banner / 3=Interstitial / 4=Splash
requestTimestamp Int64 Yes Request timestamp (ms)
fillTimestamp Int64 Yes Fill timestamp (ms)
showTimestamp Int64 Yes Show timestamp (ms)
creativeId String? Optional Creative ID (not returned by some ad networks)
ecpm NSNumber? Optional eCPM
sceneId String? Optional Scene ID

7.5 MaterialStyle

Enum value adType rawValue
.native 0 "native"
.rewarded 1 "rewarded"
.banner 2 "banner"
.interstitial 3 "interstitial"
.splash 4 "splash"

8. Error Codes

MMErrorCode enum (MMErrorCodeXxx prefix in ObjC):

Capture errors (1xxx)

Numeric value errorName Meaning Retryable
1001 VIEW_INVALID View is nil or not attached to a window No
1002 SCENE_INVALID keyWindow not found or top VC already dismissed No
1003 COLLECT_FAILED Screenshot failed (for example the view was released) No
1004 COMPRESS_FAILED JPEG compression failed No

Upload errors (2xxx)

Numeric value errorName Meaning Retryable
2001 PARAMS_INVALID Required parameter missing or invalid No
2002 NETWORK_ERROR Network request failed / timed out Yes
2003 AUTH_FAILED Authentication failed (check appId / appKey) No
2004 RETRY_EXHAUSTED Retry attempts exhausted No
2005 UNINITIALIZED SDK not initialized or already shut down No
2006 NOT_READY Upload config incomplete (missing appId / appKey) No
2007 STRATEGY_DISABLED Current placement disabled by policy No

Q1: Must capture be called on the main thread?

Yes. The SDK internally asserts Thread.isMainThread; calling it from a background thread triggers an assert (crash in DEBUG builds, undefined behavior in Release).

Q2: How should delayMs be set?

Ad type Recommended delay
Banner 200 ms
Interstitial 500 ms
Rewarded video 1000 ms
Splash 2000~3000 ms (give video splash enough render time)
Native 200~500 ms (business rendering is usually faster)

Q3: When is record.localFilePath available in the callback?

After onCollectSuccess, the sandbox file already exists. After onReportSuccess / onReportFailure, the file may already have been cleaned up. Do not read localFilePath in upload callbacks.

Q4: Memory usage and disk space?

  • Memory: peak per capture is roughly the screenshot raw size (4 x width x height bytes, about 8 MB for 1080p) plus a temporary JPEG buffer. Released after compression completes
  • Disk: default upper bound is 100 MB (maxCacheSizeMb). When exceeded, cold-start scanning deletes the oldest dead records by createdAt

Q5: Why are video ad screenshots black / missing the video frame?

This is limited by iOS system behavior:

  • ✅ Videos rendered by AVPlayerLayer - already supported by the SDK
  • AVSampleBufferDisplayLayer - cannot be captured frame by frame
  • CAMetalLayer / CAEAGLLayer - cannot be captured frame by frame
  • DRM-protected content - the system blocks frame capture

For the last three cases, frame capture from within the app process is not possible on iOS. If you need video creative monitoring, it is recommended to have the server fetch and analyze the creative URL directly from the ad network.

Q6: Should debugPerfLogging be enabled?

Enable it only during integration debugging. When enabled, it prints a lot of debug logs via print:

  • Performance overhead: about 5 ms extra per screenshot / upload
  • Privacy risk: logs contain fields such as app_id, sign, and meta JSON
  • Must be disabled in production

Q7: Will uploads continue after the app goes to the background?

Yes, until the system suspends the app. The SDK uses a normal URLSession and does not support background transfer. If the app is killed, any pending tasks will resume on the next cold start.

Q8: How do I integrate this into an ObjC project?

The SDK is fully annotated with @objc. You need to:

  1. Import in Bridging-Header.h: #import <MaterialMonitorSDK/MaterialMonitorSDK-Swift.h>
  2. ObjC Builder class name: MMConfigBuilder
  3. Conform to the protocol: @interface MyVC : UIViewController <FrameCallback>

Appendix A: Full Integration Example

A.1 AppDelegate initialization

swift Copy
import MaterialMonitorSDK
import AppTrackingTransparency
import AdSupport

@main
class AppDelegate: UIResponder, UIApplicationDelegate {

    func application(_ application: UIApplication,
                     didFinishLaunchingWithOptions launchOptions: [...]?) -> Bool {
        // Initialize after ATT authorization (recommended)
        if #available(iOS 14, *) {
            ATTrackingManager.requestTrackingAuthorization { status in
                DispatchQueue.main.async {
                    self.setupMaterialMonitor()
                }
            }
        } else {
            setupMaterialMonitor()
        }
        return true
    }

    private func setupMaterialMonitor() {
        let idfa = ASIdentifierManager.shared().advertisingIdentifier.uuidString

        let config = MaterialMonitorConfig.Builder()
            .baseUploadUrl("https://your-upload-host.com")
            .appId("your_app_id")
            .appKey("your_app_key")
            .idfa(idfa)
            .sdkVersion("6.5.0")
            .debugPerfLogging(false)
            .build()

        MaterialMonitor.initialize(config: config)
    }
}
swift Copy
extension MyViewController: FrameCallback {

    func onBannerAdShow(_ bannerView: UIView, extra: [String: Any]) {
        let adInfo = buildMMAdInfo(from: extra)

        MaterialMonitor.shared.collectFrameFromAdContainer(
            bannerView,
            adInfo: adInfo,
            timing: adInfo.timingInput,
            options: FrameOptions(delayMs: 200),
            callback: self
        )
    }

    // MARK: FrameCallback
    func onCollectSuccess(_ record: FrameRecord) {
        print("Success: captured taskId=\(record.uploadTaskId)")
    }
    func onCollectFailure(errorCode: Int, errorName: String, message: String) {
        print("Failure: [\(errorName)] \(message)")
    }
    func onReportSuccess(_ record: FrameRecord) {
        print("Success: reported showId=\(record.showId)")
    }
    func onReportSkip(_ record: FrameRecord, reason: String) {
        print("Skipped: \(reason)")
    }
    func onReportFailure(errorCode: Int, errorName: String,
                        message: String, record: FrameRecord?) {
        print("Failure: [\(errorName)] \(message)")
    }
}
objc Copy
#import <MaterialMonitorSDK/MaterialMonitorSDK-Swift.h>

@interface MyViewController () <FrameCallback>
@end

@implementation MyViewController

- (void)onBannerAdShow:(UIView *)bannerView extra:(NSDictionary *)extra {
    MMAdInfo *adInfo = [self buildMMAdInfoFromExtra:extra];
    AdTimingInput *timing = [[AdTimingInput alloc]
        initWithRequestTimeMs:adInfo.requestTimestamp
                   fillTimeMs:adInfo.fillTimestamp
                   showTimeMs:adInfo.showTimestamp];
    FrameOptions *options = [[FrameOptions alloc] init];

    [[MaterialMonitor shared] collectFrameFromAdContainer:bannerView
                                                   adInfo:adInfo
                                                   timing:timing
                                                  options:options
                                                 callback:self];
}

#pragma mark - FrameCallback
- (void)onCollectSuccess:(FrameRecord *)record {
    NSLog(@"Success: %@", record.uploadTaskId);
}
- (void)onCollectFailureWithErrorCode:(NSInteger)code
                            errorName:(NSString *)name
                              message:(NSString *)msg {
    NSLog(@"Failure: [%@] %@", name, msg);
}
- (void)onReportSuccess:(FrameRecord *)record { /* ... */ }
- (void)onReportSkip:(FrameRecord *)record reason:(NSString *)reason { /* ... */ }
- (void)onReportFailureWithErrorCode:(NSInteger)code
                           errorName:(NSString *)name
                             message:(NSString *)msg
                              record:(FrameRecord *)record { /* ... */ }
@end

A.4 Native ads integration

Native ads (business-side self-rendered) use the same collectFrameFromAdContainer as Banner. Just pass the rendered container view:

swift Copy
func nativeAdDidShow(_ nativeView: UIView) {
    MaterialMonitor.shared.collectFrameFromAdContainer(
        nativeView,
        adInfo: adInfo,
        timing: timing,
        options: FrameOptions(delayMs: 300),   // native creative usually renders a bit slower
        callback: self
    )
}

Previous
Ad Detector - Android Integration Instructions
Next
Sub-account Management
Last modified: 2026-08-13Powered by