Skip to content

ToBid iOS 接入指南

本文档指导开发者在 iOS 端将优聚智汇 SDK 以**自定义广告平台(Custom Adapter)**形式接入 ToBid(Sigmob WindMill)聚合平台。

阅读前置

请先阅读 ToBid 自定义接入概述,了解反向集成模式、工作原理、后台配置与常见问题。本文仅介绍 iOS 端的依赖引入、Build Settings 配置、SDK 初始化与广告加载。

版本号说明

组件当前版本说明
UjuRevToBidAdapter5.7.4优聚 ToBid 反向聚合适配器,版本号与 ToBid SDK 版本对齐
UjuAdCore~> 3.4.2优聚核心 SDK,由 UjuRevToBidAdapter 强依赖
UjuAdExt~> 3.4.2优聚智汇预算扩展包,扩展广告填充来源,必选(优聚 Adx 永远存在,初始化顺序第一顺位)
ToBid-iOS(WindMillSDK)5.7.4由集成方自行引入,版本必须与 UjuRevToBidAdapter 一致

版本对齐要求

UjuRevToBidAdapter 的版本号即对应适配的 ToBid SDK 版本号。集成时必须保证:

  • pod 'UjuRevToBidAdapter', '5.7.4'
  • pod 'ToBid-iOS', '5.7.4'

两者版本号必须完全一致,否则可能因 ToBid SDK 协议变更导致适配器失效。

开发环境要求

项目要求
iOS 部署目标iOS 13.0 及以上
Swift 版本Swift 5.9 及以上
Xcode 版本Xcode 15 及以上(推荐 Xcode 26+)
架构arm64(真机)/ arm64 + x86_64(模拟器)
依赖工具Xcode / CocoaPods

SDK 组件关系

ToBid iOS 反向集成场景下,集成方 App 与 SDK 的依赖关系如下:

┌──────────────────────────────────────────────┐
│            集成方 App                        │
├──────────────────────────────────────────────┤
│  ToBid-iOS / WindMillSDK(集成方自行引入)   │
│       ↓ 运行时通过 ObjC 反射发现适配器       │
│  UjuRevToBidAdapter.xcframework(必选)      │
│       ↓ 编译期依赖                           │
│  UjuAdExt.xcframework(必选)               │
│  └── 内部 DSP 适配器(Vendor 已打包)        │
│       ↓ 依赖                                 │
│  UjuAdCore.xcframework(必选)              │
│  ├── 优聚 Adx 竞价引擎                      │
│  ├── 配置服务 / 埋点上报                     │
│  └── 广告加载 / 渲染 / 展示                  │
│  (gRPC-Swift / SwiftProtobuf / SwiftNIO     │
│   已静态链接,无需引入)                     │
└──────────────────────────────────────────────┘

不支持 Banner

UjuRevToBidAdapter 当前支持 4 种广告类型:开屏 / 激励视频 / 插屏 / 原生。Banner 适配器待 UjuAdCore Banner 能力就绪后提供。

集成方式

提供两种集成方式,UjuRevToBidAdapter 为静态库类型(.a + Headers),编译期依赖 UjuAdCore 与 WindMillSDK 的 Headers,运行期由集成方链接完整的 ToBid SDK。

方式适用场景推荐度
CocoaPods使用 CocoaPods 管理依赖的项目★★★
XCFramework不使用 CocoaPods 的项目★★

方式一:CocoaPods 集成(推荐)

步骤 1:添加私有 spec repo

优聚智汇 SDK 通过私有 CocoaPods 源分发,首次接入需添加 spec repo:

bash
pod repo add UjuAdSpecs https://gitee.com/ujuad/iossdk.git

步骤 2:创建 Podfile

在项目根目录创建 Podfile

ruby
source 'https://gitee.com/ujuad/iossdk.git'
source 'https://cdn.cocoapods.org/'

platform :ios, '13.0'
use_frameworks!

target 'YourApp' do
  # 优聚核心 SDK(必选,UjuRevToBidAdapter 强依赖)
  pod 'UjuAdCore', '~> 3.4.2'

  # 优聚智汇预算扩展包(必选,扩展广告填充来源,优聚 Adx 永远存在)
  pod 'UjuAdExt', '~> 3.4.2'

  # 优聚 ToBid 反向适配器(版本对齐 ToBid SDK)
  pod 'UjuRevToBidAdapter', '5.7.4'

  # ToBid 聚合 SDK(集成方自行引入,版本必须与 UjuRevToBidAdapter 一致)
  pod 'ToBid-iOS', '5.7.4'
end

source 说明

  • source 'https://gitee.com/ujuad/iossdk.git' 必须在 Podfile 顶部声明,用于拉取优聚私有 podspec
  • source 'https://cdn.cocoapods.org/' 用于拉取 ToBid-iOS 等公共依赖
  • UjuRevToBidAdapterToBid-iOS 版本号必须完全一致

步骤 3:安装依赖

bash
pod install
open YourApp.xcworkspace

podspec 自动配置 Build Settings

CocoaPods 集成模式下,podspec 会自动配置 SWIFT_INCLUDE_PATHSOTHER_LDFLAGS(含 -force_load-ObjC)、ENABLE_DEBUG_DYLIB 等 Build Settings,集成方无需手动设置。

步骤 4:验证集成

swift
import UjuAdCore
import UjuRevToBidAdapter   // 适配器内部类型,通常无需直接 import
import WindMillSDK     // ToBid SDK,由集成方引入

print("UjuAdCore 版本: \(UjuAdCore.shared.getVersion())")
// 输出: UjuAdCore 版本: 3.4.2

编译运行,确认无 no such module / undefined symbol 错误即集成成功。

方式二:XCFramework 集成

步骤 1:获取 xcframework

从优聚智汇 Gitee Release 下载以下四个 zip 包:

包名下载地址
UjuAdCore-3.4.2.xcframework.zipGitee Release UjuAdCore-3.4.2
UjuAdExt-3.4.2.xcframework.zipGitee Release UjuAdExt-3.4.2
UjuRevToBidAdapter-5.7.4.xcframework.zipGitee Release UjuRevToBidAdapter-5.7.4
ToBid-iOS(WindMillSDK)ToBid 官方 或 CocoaPods 获取

解压 UjuRevToBidAdapter-5.7.4.xcframework.zip 后产物结构如下:

UjuRevToBidAdapter.xcframework/
├── Info.plist                                # xcframework 元信息
├── ios-arm64/                                # 真机 slice
│   ├── libUjuRevToBidAdapter-iphoneos.a             # 静态库
│   └── Headers/                               # UjuRevToBidAdapter.swiftmodule
├── ios-arm64_x86_64-simulator/               # 模拟器 slice
│   ├── libUjuRevToBidAdapter-simulator.a
│   └── Headers/
│       └── UjuRevToBidAdapter.swiftmodule/
└── UjuRevToBidAdapter.version.json                 # 版本元信息

步骤 2:添加到 Xcode 项目

  1. UjuAdCore.xcframeworkUjuAdExt.xcframework(含其内部 Vendor/ 目录下的第三方 DSP xcframework,按 zip 解压后的目录结构原样保留)、UjuRevToBidAdapter.xcframework、ToBid SDK 的 4 个 xcframework(WindMillSDK.xcframework / WindFoundation.xcframework / WindSDK.xcframework / NexaX.xcframework)拖入项目 Frameworks/ 目录
  2. 在 Xcode 中选择项目 → TargetGeneralFrameworks, Libraries, and Embedded Content,确认所有 xcframework 的 Embed 选项为 Do Not Embed(均为静态库)

步骤 3:配置 Build Settings

在 Target → Build Settings追加以下配置(在 UjuAdCore 已有配置的基础上追加 UjuAdExt 与 UjuRevToBidAdapter):

text
// SWIFT_INCLUDE_PATHS 追加 UjuAdExt + UjuRevToBidAdapter 的 Headers 目录
SWIFT_INCLUDE_PATHS[sdk=iphoneos*] = $(inherited) \
  $(SRCROOT)/Frameworks/UjuAdCore.xcframework/ios-arm64/Headers \
  $(SRCROOT)/Frameworks/UjuAdExt.xcframework/ios-arm64/Headers \
  $(SRCROOT)/Frameworks/UjuRevToBidAdapter.xcframework/ios-arm64/Headers
SWIFT_INCLUDE_PATHS[sdk=iphonesimulator*] = $(inherited) \
  $(SRCROOT)/Frameworks/UjuAdCore.xcframework/ios-arm64_x86_64-simulator/Headers \
  $(SRCROOT)/Frameworks/UjuAdExt.xcframework/ios-arm64_x86_64-simulator/Headers \
  $(SRCROOT)/Frameworks/UjuRevToBidAdapter.xcframework/ios-arm64_x86_64-simulator/Headers

// OTHER_LDFLAGS 追加 UjuAdExt + UjuRevToBidAdapter 的 -force_load,以及 -ObjC
OTHER_LDFLAGS[sdk=iphoneos*] = $(inherited) \
  -force_load $(SRCROOT)/Frameworks/UjuAdCore.xcframework/ios-arm64/libUjuAdCore-iphoneos.a \
  -force_load $(SRCROOT)/Frameworks/UjuAdExt.xcframework/ios-arm64/libUjuAdExt-iphoneos.a \
  -force_load $(SRCROOT)/Frameworks/UjuRevToBidAdapter.xcframework/ios-arm64/libUjuRevToBidAdapter-iphoneos.a \
  -ObjC
OTHER_LDFLAGS[sdk=iphonesimulator*] = $(inherited) \
  -force_load $(SRCROOT)/Frameworks/UjuAdCore.xcframework/ios-arm64_x86_64-simulator/libUjuAdCore-simulator.a \
  -force_load $(SRCROOT)/Frameworks/UjuAdExt.xcframework/ios-arm64_x86_64-simulator/libUjuAdExt-simulator.a \
  -force_load $(SRCROOT)/Frameworks/UjuRevToBidAdapter.xcframework/ios-arm64_x86_64-simulator/libUjuRevToBidAdapter-simulator.a \
  -ObjC

// 其他必填项(与 UjuAdCore 一致)
SWIFT_ENABLE_EXPLICIT_MODULES = NO
ENABLE_DEBUG_DYLIB = NO

为什么需要 -force_load 和 -ObjC?

  • -force_load:UjuRevToBidAdapter 与 UjuAdExt 均为 Swift 静态库,UjuAdExt 内部还包含 OC 静态库(Vendor 目录下的第三方 DSP SDK)。@objc(Custom*Adapter) 标注的 OC 类符号在默认链接模式下会被 dead-strip,导致 ToBid SDK 通过 NSClassFromString("CustomConfigAdapter") 找不到适配器;UjuAdExt 内部 DSP SDK 的 OC 类符号也会被 dead-strip 导致竞价时 _OBJC_CLASS_$_*** 未定义。-force_load 强制保留全部 .o,是必需的。
  • -ObjC:ToBid SDK(WindMillSDK)与 UjuAdExt 内部 DSP SDK 均为 OC 静态库,含 Category 扩展,必须加 -ObjC 才能正确加载 OC Category。

CocoaPods 集成模式下,podspec 已自动配置上述标志。

为什么需要 SWIFT_INCLUDE_PATHS?

UjuRevToBidAdapter 与 UjuAdExt 静态库(.a + Headers)不会自动将 Headers 目录加入 swiftmodule 搜索路径,必须显式指定,否则编译报 no such module 'UjuRevToBidAdapter'no such module 'UjuAdExt'

UjuAdExt Vendor 目录下的第三方 DSP SDK

UjuAdExt zip 内 Vendor/ 目录下的第三方 DSP SDK 为静态库(.framework 内含 .a),其 OC 类符号在默认链接模式下会被 dead-strip。若 zip 中已显式列出每个 DSP 的 .xcframework 路径,还需逐个 -force_load 每个 framework 内的二进制,缺一会导致对应 DSP 的 OC 类符号找不到。具体路径以解压后的目录结构为准。CocoaPods 集成模式下由 podspec 的 vendored_frameworks 自动处理。

步骤 4:验证集成

swift
import UjuAdCore

print("UjuAdCore 版本: \(UjuAdCore.shared.getVersion())")
// 输出: UjuAdCore 版本: 3.4.2

集成验证清单:

  • [ ] UjuAdCore.xcframework 已添加到 Frameworks
  • [ ] UjuAdExt.xcframework(含 Vendor/ 下所有第三方 DSP xcframework)已添加到 Frameworks
  • [ ] UjuRevToBidAdapter.xcframework 已添加到 Frameworks
  • [ ] ToBid SDK 的 4 个 xcframework(WindMillSDK / WindFoundation / WindSDK / NexaX)已添加到 Frameworks
  • [ ] 所有 xcframework 的 Embed 选项为 Do Not Embed
  • [ ] SWIFT_INCLUDE_PATHS 已配置 UjuAdCore + UjuAdExt + UjuRevToBidAdapter 的 Headers
  • [ ] OTHER_LDFLAGS 已配置 -force_load(三个 .a + Vendor 下 DSP 的 .a)+ -ObjC
  • [ ] SWIFT_ENABLE_EXPLICIT_MODULES 设为 NO
  • [ ] ENABLE_DEBUG_DYLIB 设为 NO
  • [ ] import UjuAdCoreimport WindMillSDK 编译通过

ToBid 后台配置

1. 添加自定义广告网络

在 ToBid 后台添加自定义广告网络,配置 ADN 级别的 CustomInfo(JSON 格式):

json
{
    "appId": "YOUR_UJU_APP_ID",
    "appKey": "YOUR_UJU_APP_KEY",
    "channel": "tobid1"
}
字段类型必填说明
appIdString优聚智汇后台分配的应用 ID
appKeyString优聚智汇后台分配的 RSA 公钥,原样传入
channelString渠道标识,用于数据统计与分账

appKey 是 RSA 公钥

appKey 实际是优聚智汇后台分配的 RSA 公钥(Base64 编码字符串),用于服务端通信加密与验签。请从优聚智汇开发者后台获取,原样传入,不要自行修改或截断。

2. 配置广告位

在 ToBid 后台为每个广告位配置 CustomInfo(JSON 格式):

json
{
    "placementId": "YOUR_UJU_PLACEMENT_ID"
}
字段类型必填说明
placementIdString优聚智汇后台分配的广告位 ID

3. 配置适配器类名

ToBid 后台"自定义广告平台"配置时,需填写优聚适配器的 ObjC 类名(ToBid SDK 通过 NSClassFromString 反射发现):

适配器ObjC 类名
初始化适配器CustomConfigAdapter
开屏广告适配器CustomSplashAdapter
激励视频适配器CustomRewardAdapter
插屏广告适配器CustomInterstitialAdapter
原生广告适配器CustomNativeAdapter

类名不可混淆

上述适配器类名通过 @objc(Custom*Adapter) 标注暴露给 ObjC runtime,集成方工程必须开启 -ObjC 链接标志(CocoaPods 自动配置,XCFramework 需手动添加),否则链接器会 dead-strip 这些类符号,运行时 ToBid SDK 无法发现适配器。

ToBid SDK 初始化

开发者只需初始化 ToBid SDK,优聚 SDK 的初始化由 ToBid 在启动时自动触发(通过适配器的初始化入口)。

1. 请求 ATT 授权(iOS 14+)

iOS 14+ 需在 Info.plist 添加 ATT 使用说明:

xml
<key>NSUserTrackingUsageDescription</key>
<string>我们需要您的许可来提供更精准的广告体验</string>

AppDelegate 中请求 ATT 授权,授权完成后再初始化 ToBid SDK(ATT 授权前网络请求可能被系统拒绝):

swift
import UIKit
import AppTrackingTransparency
import WindMillSDK
import UjuAdCore  // 用于解除日志 tag 过滤

@main
class AppDelegate: UIResponder, UIApplicationDelegate {

    func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {

        // 解除 UjuAdCore 日志 tag 过滤(初始化后生效,便于排查)
        UjuLogger.shared.setTagFilter(nil)

        // 请求 ATT 授权,授权完成后再初始化 ToBid SDK
        requestATTAndSetupToBid()

        return true
    }

    private func requestATTAndSetupToBid() {
        if #available(iOS 14, *) {
            let status = ATTrackingManager.trackingAuthorizationStatus
            if status == .notDetermined {
                ATTrackingManager.requestTrackingAuthorization { [weak self] _ in
                    DispatchQueue.main.async {
                        self?.setupToBidSDK()
                    }
                }
            } else {
                setupToBidSDK()
            }
        } else {
            setupToBidSDK()
        }
    }
}

2. 初始化 ToBid SDK

授权完成后调用 WindMillAds.setupSDK(appId:) 初始化 ToBid SDK:

swift
private func setupToBidSDK() {
    guard !ToBidConfig.tobidAppId.isEmpty else {
        print("ToBid appId 未配置")
        return
    }

    // 开启调试日志(调试阶段建议开启)
    WindMillAds.setDebugEnable(true)

    // 初始化 ToBid SDK,传入 ToBid 后台分配的 AppId
    WindMillAds.setupSDK(appId: ToBidConfig.tobidAppId) { success, error in
        DispatchQueue.main.async {
            if success {
                print("ToBid SDK 初始化成功,version=\(WindMillAds.sdkVersion())")
            } else {
                print("ToBid SDK 初始化失败: \(error?.localizedDescription ?? "unknown")")
            }
        }
    }
}

优聚 SDK 无需单独初始化

ToBid SDK 启动后,会自动调用 CustomConfigAdapter.initializeAdapter(configuration:) 方法,在适配器内部完成优聚 SDK 的 initialize + start 流程。开发者无需不应调用 UjuAdCore.shared.initialize() / UjuAdCore.shared.start()

初始化链路:

  1. WindMillAds.setupSDK(appId:) 启动 ToBid SDK
  2. ToBid SDK 通过 NSClassFromString("CustomConfigAdapter") 发现适配器
  3. 调用 CustomConfigAdapter.initializeAdapter(configuration:)
  4. 适配器解析 custom_info 获取优聚 appId / appKey / channel
  5. 调用 UjuAdCore.shared.initialize(...) + UjuAdCore.shared.start(listener)
  6. 初始化完成后回调 ToBid bridge 通知初始化结果

广告加载示例

以下以激励视频为例,展示通过 ToBid 标准接口加载广告的完整流程。开屏、插屏、原生广告的调用方式类似,仅广告对象类名和监听协议不同。

swift
import UIKit
import WindMillSDK

class RewardAdViewController: UIViewController {

    // MARK: - 属性

    /// ToBid 激励视频广告对象
    private var rewardAd: WindMillRewardVideoAd?
    /// 日志输出文本视图
    private let logTextView = UITextView()
    /// 展示按钮
    private let showButton = UIButton(type: .system)
    /// 广告是否已加载就绪
    private var isReady = false

    // MARK: - 加载广告

    @objc private func loadAd() {
        appendLog("开始加载激励视频广告: placementId=\(ToBidConfig.tobidRewardPlacementId)")
        isReady = false

        // 1. 创建广告请求
        let request = WindMillAdRequest.request()
        request.placementId = ToBidConfig.tobidRewardPlacementId  // ToBid 后台广告位 ID

        // 2. 创建 ToBid 激励视频广告对象
        rewardAd = WindMillRewardVideoAd(request: request)

        // 3. 设置代理
        rewardAd?.delegate = self

        // 4. 加载广告
        rewardAd?.loadAdData()
    }

    // MARK: - 展示广告

    @objc private func showAd() {
        guard let ad = rewardAd, ad.isAdReady() else {
            appendLog("广告未就绪,无法展示")
            return
        }
        appendLog("展示激励视频广告")
        ad.showAd(withRootViewController: self)
    }

    // MARK: - 资源释放

    deinit {
        // 销毁广告对象,释放资源
        rewardAd = nil
    }

    private func appendLog(_ message: String) {
        DispatchQueue.main.async { [weak self] in
            self?.logTextView.text.append(message + "\n")
        }
    }
}

// MARK: - ToBid 激励视频监听回调

extension RewardAdViewController: WindMillRewardVideoAdDelegate {

    func rewardVideoAdDidLoad(_ rewardVideoAd: WindMillRewardVideoAd) {
        appendLog("✅ 广告加载成功")
        isReady = true
    }

    func rewardVideoAdDidLoad(_ rewardVideoAd: WindMillRewardVideoAd, didFailWithError error: Error) {
        appendLog("❌ 广告加载失败: \(error.localizedDescription)")
    }

    func rewardVideoAdDidVisible(_ rewardVideoAd: WindMillRewardVideoAd) {
        appendLog("✅ 广告展示成功")
    }

    func rewardVideoAdDidShowFailed(_ rewardVideoAd: WindMillRewardVideoAd, error: Error) {
        appendLog("❌ 广告展示失败: \(error.localizedDescription)")
    }

    func rewardVideoAdDidClick(_ rewardVideoAd: WindMillRewardVideoAd) {
        appendLog("✅ 广告被点击")
    }

    func rewardVideoAdDidClose(_ rewardVideoAd: WindMillRewardVideoAd) {
        appendLog("✅ 广告关闭")
        isReady = false
    }

    func rewardVideoAd(_ rewardVideoAd: WindMillRewardVideoAd, reward: WindMillRewardInfo) {
        appendLog("🎁 获得奖励: isCompletedView=\(reward.isCompeltedView)")
        // 在此发放应用内奖励
    }

    func rewardVideoAd(_ rewardVideoAd: WindMillRewardVideoAd, didPlayFinishWithError error: Error?) {
        appendLog("✅ 播放结束: \(error?.localizedDescription ?? "nil")")
    }
}

各广告类型对应类与协议

广告类型ToBid 广告类监听协议加载方法
开屏WindMillSplashAdWindMillSplashAdDelegateloadAd()
激励视频WindMillRewardVideoAdWindMillRewardVideoAdDelegateloadAdData()
插屏WindMillIntersititialAdWindMillIntersititialAdDelegateloadAdData()
原生WindMillNativeAdsManagerWindMillNativeAdsManagerDelegateloadAdData(count:)

原生广告采用模板渲染

优聚原生广告采用模板渲染模式,由 SDK 内部渲染广告视图并绑定到 ToBid 提供的容器中。开发者无需自行绘制原生广告视图,NativeAdData 的字段(title/desc/iconUrl 等)可能为空。

Objective-C 宿主 App 接入

UjuRevToBidAdapter 为 Swift 静态库,但内部适配器类均标注 @objc(Custom*Adapter) 并继承 NSObject,ToBid SDK(WindMillSDK,OC 框架)通过 NSClassFromString 反射发现并调用。Objective-C 宿主 App 可直接接入,无需 Swift 桥接

为什么 OC 宿主 App 无需 Swift 桥接?

  • UjuRevToBidAdapter 的 Custom*Adapter 类通过 @objc 标注暴露给 ObjC runtime,ToBid SDK 通过 runtime 发现并调用
  • 宿主 App 不直接调用 UjuRevToBidAdapter 内部的 Swift 方法,只与 ToBid SDK 的 OC API 交互
  • UjuAdCore 的初始化、日志配置均由 CustomConfigAdapter.initializeAdapter(configuration:) 内部自动完成(含 UjuLogger.shared.setTagFilter(nil) 解除日志过滤)
  • UjuAdCore 未生成 -Swift.h 桥接头(构建时 SWIFT_INSTALL_OBJC_HEADER=NO),但这不影响 OC 宿主 App 接入,因为宿主 App 无需直接调用 UjuAdCore 的 Swift API

OC AppDelegate 示例

objc
// AppDelegate.h
#import <UIKit/UIKit.h>

@interface AppDelegate : UIResponder <UIApplicationDelegate>
@end
objc
// AppDelegate.m
#import "AppDelegate.h"
#import <AppTrackingTransparency/AppTrackingTransparency.h>
#import <WindMillSDK/WindMillSDK.h>

// ToBid 后台分配的 AppId(替换为自有 ToBid appId)
static NSString *const kToBidAppId = @"YOUR_TOBID_APP_ID";

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application
        didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

    // 请求 ATT 授权,授权完成后再初始化 ToBid SDK
    // - iOS 14+: ATT 授权前网络请求可能被系统拒绝(Denied over Wi-Fi interface)
    // - 授权完成后网络权限就绪,ToBid SDK config 请求才能成功
    [self requestATTAndSetupToBid];

    return YES;
}

#pragma mark - ATT + ToBid SDK Setup

- (void)requestATTAndSetupToBid {
    if (@available(iOS 14, *)) {
        ATTrackingManagerAuthorizationStatus status = [ATTrackingManager trackingAuthorizationStatus];
        if (status == ATTrackingManagerAuthorizationStatusNotDetermined) {
            // 未授权,请求授权
            [ATTrackingManager requestTrackingAuthorizationWithCompletionHandler:^(ATTrackingManagerAuthorizationStatus status) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    [self setupToBidSDK];
                });
            }];
        } else {
            // 已授权或已拒绝,直接初始化
            [self setupToBidSDK];
        }
    } else {
        // iOS 13 及以下,无需 ATT 授权,直接初始化
        [self setupToBidSDK];
    }
}

- (void)setupToBidSDK {
    if (kToBidAppId.length == 0) {
        NSLog(@"ToBid appId 未配置,跳过 ToBid SDK 初始化");
        return;
    }

    // 开启调试日志(调试阶段建议开启)
    [WindMillAds setDebugEnable:YES];

    // 初始化 ToBid SDK,传入 ToBid 后台分配的 AppId
    // 优聚 SDK 的初始化由 CustomConfigAdapter 内部自动触发,无需手动调用
    [WindMillAds setupSDKWithAppId:kToBidAppId callback:^(BOOL success, NSError *error) {
        dispatch_async(dispatch_get_main_queue(), ^{
            if (success) {
                NSLog(@"ToBid SDK 初始化成功, version=%@", [WindMillAds sdkVersion]);
            } else {
                NSLog(@"ToBid SDK 初始化失败: %@", error.localizedDescription);
            }
        });
    }];
}

@end

OC 广告加载示例(激励视频)

objc
// RewardAdViewController.m
#import "RewardAdViewController.h"
#import <WindMillSDK/WindMillSDK.h>

@interface RewardAdViewController () <WindMillRewardVideoAdDelegate>
@property (nonatomic, strong) WindMillRewardVideoAd *rewardAd;
@property (nonatomic, assign) BOOL isReady;
@end

@implementation RewardAdViewController

- (void)loadAd {
    self.isReady = NO;

    // 1. 创建广告请求
    WindMillAdRequest *request = [WindMillAdRequest request];
    request.placementId = @"YOUR_TOBID_REWARD_PLACEMENT_ID";

    // 2. 创建 ToBid 激励视频广告对象
    self.rewardAd = [[WindMillRewardVideoAd alloc] initWithRequest:request];

    // 3. 设置代理
    self.rewardAd.delegate = self;

    // 4. 加载广告
    [self.rewardAd loadAdData];
}

- (void)showAd {
    if (!self.rewardAd || ![self.rewardAd isAdReady]) {
        NSLog(@"广告未就绪,无法展示");
        return;
    }
    [self.rewardAd showAdWithRootViewController:self];
}

#pragma mark - WindMillRewardVideoAdDelegate

- (void)rewardVideoAdDidLoad:(WindMillRewardVideoAd *)rewardVideoAd {
    NSLog(@"✅ 广告加载成功");
    self.isReady = YES;
}

- (void)rewardVideoAdDidLoad:(WindMillRewardVideoAd *)rewardVideoAd didFailWithError:(NSError *)error {
    NSLog(@"❌ 广告加载失败: %@", error.localizedDescription);
}

- (void)rewardVideoAdDidVisible:(WindMillRewardVideoAd *)rewardVideoAd {
    NSLog(@"✅ 广告展示成功");
}

- (void)rewardVideoAdDidClick:(WindMillRewardVideoAd *)rewardVideoAd {
    NSLog(@"✅ 广告被点击");
}

- (void)rewardVideoAdDidClose:(WindMillRewardVideoAd *)rewardVideoAd {
    NSLog(@"✅ 广告关闭");
    self.isReady = NO;
}

- (void)rewardVideoAd:(WindMillRewardVideoAd *)rewardVideoAd reward:(WindMillRewardInfo *)reward {
    NSLog(@"🎁 获得奖励: isCompletedView=%d", reward.isCompeltedView);
    // 在此发放应用内奖励
}

@end

OC 宿主 App 集成注意事项

事项说明
集成方式与 Swift 宿主 App 完全一致(CocoaPods / XCFramework)
Build Settings与 Swift 宿主 App 完全一致(-force_load + -ObjC + SWIFT_INCLUDE_PATHS
Swift 运行时依赖Xcode 自动链接 Swift 运行时库,OC 宿主 App 无需手动配置
UjuAdCore 调用无需不应直接调用 UjuAdCore 的 Swift API,初始化由适配器自动完成
日志调试适配器初始化 UjuAdCore 时已自动调用 UjuLogger.shared.setTagFilter(nil) 解除日志过滤
ToBid SDK API全部为 OC 接口,OC 宿主 App 直接调用即可

仍需配置 SWIFT_INCLUDE_PATHS

即使宿主 App 是纯 OC,编译 UjuRevToBidAdapter 静态库时仍需配置 SWIFT_INCLUDE_PATHS,因为 UjuRevToBidAdapter 的 swiftmodule 需要在编译期被解析。CocoaPods 集成模式下 podspec 已自动配置。

宿主 App 引入注意事项

引入 UjuRevToBidAdapter 时,宿主 App 的依赖关系如下:

依赖类型内容是否需要集成方引入
优聚核心 SDKUjuAdCore ~> 3.4.2✅ 必选,UjuRevToBidAdapter 强依赖
优聚预算扩展包UjuAdExt ~> 3.4.2(含 Vendor 下第三方 DSP SDK)✅ 必选,扩展广告填充来源
ToBid 适配器UjuRevToBidAdapter 5.7.4✅ 必选
ToBid 聚合 SDKToBid-iOS 5.7.4(WindMillSDK + WindFoundation + WindSDK + NexaX)✅ 必选,集成方自行引入
第三方库gRPC-Swift / SwiftProtobuf / SwiftNIO❌ 已静态链接进 UjuAdCore,禁止重复引入
系统框架UIKit / Foundation / AVFoundation / CoreMedia / AdSupport / AppTrackingTransparency / StoreKit / CoreLocation✅ Xcode 自动链接
系统库libz.tbd / libresolv.tbd✅ Xcode 自动链接

Info.plist 必要配置

引入 UjuRevToBidAdapter 后,需在 Info.plist 中补充以下配置:

配置项原因
NSUserTrackingUsageDescriptioniOS 14+ 获取 IDFA 需用户授权(ToBid SDK + 优聚 SDK 均需)
NSAppTransportSecurity.NSAllowsArbitraryLoadsToBid SDK 与 UjuAdExt 内部 DSP SDK 均使用 HTTP 明文请求广告,iOS 默认强制 HTTPS 会报 -1022
NSMotionUsageDescriptionUjuAdExt 内部 DSP SDK 读取 CoreMotion 设备信息
LSApplicationQueriesSchemesUjuAdExt 内部 DSP SDK 广告跳转目标 App 的 URL Scheme 白名单(完整列表见 iOS 准备工作 - UjuAdExt 必要配置
SKAdNetworkItemsUjuAdExt 内部 DSP SDK 广告归因所需的 SKAdNetwork ID 列表(完整列表见 iOS 准备工作 - UjuAdExt 必要配置

依赖关系总览

宿主 App

  ├── ToBid-iOS(必选,集成方自行引入)
  │     ├── WindMillSDK.xcframework
  │     ├── WindFoundation.xcframework
  │     ├── WindSDK.xcframework
  │     └── NexaX.xcframework
  │       ↓ 运行时通过 ObjC 反射发现适配器
  ├── UjuRevToBidAdapter 5.7.4(必选)
  │     └── Custom*Adapter(@objc 暴露给 ToBid SDK)
  │           ↓ 编译期依赖
  ├── UjuAdExt ~> 3.4.2(必选)
  │     └── 内部 DSP 适配器(Vendor 下第三方 SDK 已打包,集成方无需单独获取)
  │           ↓ 依赖
  ├── UjuAdCore ~> 3.4.2(必选)
  │     ├── 优聚 Adx 竞价引擎
  │     ├── 配置服务 / 埋点上报
  │     └── 广告加载 / 渲染 / 展示
  │     (gRPC-Swift / SwiftProtobuf / SwiftNIO 已静态链接,无需引入)

  └── Info.plist 配置
        ├── NSUserTrackingUsageDescription
        ├── NSAppTransportSecurity.NSAllowsArbitraryLoads
        ├── NSMotionUsageDescription
        ├── LSApplicationQueriesSchemes
        └── SKAdNetworkItems

适配器架构说明

优聚 ToBid 适配器以独立 XCFramework(UjuRevToBidAdapter.xcframework)形式提供,实现 WindMillSDK 的 AWMCustom*Adapter 协议:

  • 初始化适配器CustomConfigAdapter):实现 ToBid 的 AWMCustomConfigAdapter 协议,在 ToBid 启动时解析后台 custom_info(appId/appKey/channel),完成优聚 SDK 的 initialize + start 流程,并通过 bridge 回调通知 ToBid 初始化结果
  • 广告适配器CustomSplashAdapter / CustomRewardAdapter / CustomInterstitialAdapter / CustomNativeAdapter):每种广告类型对应一个独立的适配器类,实现 ToBid 的 AWMCustom*Adapter 协议,负责广告的加载、展示、销毁与事件回调转发
  • 初始化等待机制:适配器内部维护初始化状态追踪(CustomSdkInitHelper),各广告适配器在 loadAd 入口会等待优聚 SDK 初始化完成后再执行实际加载,避免 "no strategy" 错误
  • 配置解析:统一解析 ToBid 后台下发的 custom_info JSON 字符串,提取 appId/appKey/channel(ADN 级别)与 placementId(广告位级别)
  • Bridge 隔离:适配器通过 @objc 标注暴露 OC 类名供 ToBid SDK 反射发现,bridge 回调由 ToBid SDK 在运行时传入

内部实现无需关注

适配器内部的所有类、方法与状态机均为 SDK 内部实现细节,开发者无需直接调用。开发者只需通过 ToBid 标准接口(WindMillAds / WindMillRewardVideoAd 等)使用广告即可。

常见问题

Q: 集成方需要单独初始化优聚 SDK 吗?

A: 不需要。ToBid SDK 启动后会自动触发优聚 SDK 的 initialize + start 流程。开发者只需初始化 ToBid SDK 即可。

Q: 广告加载时报 "no strategy" 或 "UjuAdCore init failed" 错误怎么办?

A: 该错误表示优聚 SDK 尚未初始化完成就调用了 loadAd。请检查:

  • ToBid 后台 ADN 级别 custom_info 中的 appId / appKey 是否正确
  • 优聚后台是否已创建对应的应用和广告位
  • 是否遗漏了 ATT 授权(iOS 14+ 必须先授权再初始化 ToBid SDK)
  • 网络连接是否正常

Q: ToBid SDK 调用适配器报 "class not found" 怎么办?

A: 表示链接器 dead-strip 了适配器 OC 类符号。请检查:

  • OTHER_LDFLAGS 是否已配置 -force_load 指向 libUjuRevToBidAdapter-iphoneos.a / libUjuRevToBidAdapter-simulator.a
  • OTHER_LDFLAGS 是否已配置 -ObjC(CocoaPods 自动配置,XCFramework 需手动添加)
  • 验证方法:在 AppDelegate 中调用 NSClassFromString("CustomConfigAdapter"),应返回非 nil

Q: UjuRevToBidAdapter 与 ToBid-iOS 的版本号必须一致吗?

A: 是的UjuRevToBidAdapter 的版本号即对应适配的 ToBid SDK 版本号。集成时必须保证:

  • pod 'UjuRevToBidAdapter', '5.7.4'
  • pod 'ToBid-iOS', '5.7.4'

两者版本号必须完全一致,否则可能因 ToBid SDK 协议变更导致适配器失效。

Q: 激励视频的奖励如何发放?

A: 优聚 SDK 的激励视频奖励回调由适配器内部处理并转发给 ToBid。开发者应在 ToBid 的激励视频代理回调(rewardVideoAd(_:reward:))中根据 reward.isCompeltedView 发放奖励,无需直接处理优聚 SDK 的监听器。

Q: 原生广告为什么是模板渲染模式?

A: 优聚原生广告采用模板渲染模式,由 SDK 内部渲染广告视图。适配器通过 ToBid 的 WindMillNativeAdData 协议将模板视图暴露给 ToBid 容器。NativeAdData 的字段(title/desc/iconUrl 等)可能为空,开发者无需自行渲染。

相关链接