| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425 |
- //
- // SplashAdViewController.swift
- // VenusKitto
- //
- // Created by Neoa on 2025/1/27.
- //
- import UIKit
- import AnyThinkSplash
- import AnyThinkSDK
- class SplashAdViewController: UIViewController, ATSplashDelegate {
-
- // MARK: - Properties
- private let splashPlacementID = "b68d88cc420248" // 开屏广告placement ID
- private var splashAd: ATSplash?
- private var isAdLoaded = false
- private var useFootLogoView = false // 是否使用底部Logo视图
-
- // 广告展示开始时间(毫秒)
- private var adStartTime: Int64 = 0
-
- // 回调闭包,用于广告结束后跳转到主界面
- var onSplashAdFinished: (() -> Void)?
-
- // MARK: - Initialization
- convenience init(useFootLogoView: Bool = false) {
- self.init()
- self.useFootLogoView = useFootLogoView
- }
-
- // MARK: - UI Components
- private let backgroundImageView: UIImageView = {
- let imageView = UIImageView()
- imageView.image = UIImage(named: "catlogos")
- imageView.contentMode = .scaleAspectFit
- imageView.translatesAutoresizingMaskIntoConstraints = false
- return imageView
- }()
-
- private let appNameLabel: UILabel = {
- let label = UILabel()
- label.text = "VenusKitto"
- label.font = UIFont.systemFont(ofSize: 24, weight: .bold)
- label.textColor = UIColor(hex: "#2B2B2B")
- label.textAlignment = .center
- label.translatesAutoresizingMaskIntoConstraints = false
- return label
- }()
-
- // MARK: - Lifecycle
- override func viewDidLoad() {
- super.viewDidLoad()
- view.backgroundColor = UIColor(hex: "#FFFEFC")
- loadSplashAd()
- }
-
- override func viewDidAppear(_ animated: Bool) {
- super.viewDidAppear(animated)
- }
- // MARK: - Actions
-
- // MARK: - Splash Ad Methods
- private func loadSplashAd() {
- var loadConfigDict: [AnyHashable: Any] = [:]
-
- // 开屏超时时间
- loadConfigDict[kATSplashExtraTolerateTimeoutKey] = NSNumber(value: 5)
- // 自定义load参数
- loadConfigDict[kATAdLoadingExtraMediaExtraKey] = "media_val_SplashVC"
-
- // 可选接入,如果使用了腾讯优量汇(GDT),可添加以下配置
- // AdLoadConfigTool.splash_loadExtraConfigAppend_Tencent(&loadConfigDict)
-
- ATAdManager.shared().loadAD(withPlacementID: splashPlacementID, extra: loadConfigDict, delegate: self, containerView: nil)
- }
-
-
- private func showSplashAd() {
- // 场景统计功能,可选接入
- ATAdManager.shared().entrySplashScenario(withPlacementID: splashPlacementID, scene: "")
-
- // 检查是否有就绪
- guard ATAdManager.shared().splashReady(forPlacementID: splashPlacementID) else {
- loadSplashAd()
- return
- }
-
- // 展示配置
- let config = ATShowConfig(scene: "", showCustomExt: "testShowCustomExt")
-
- // 开屏相关参数配置
- let configDict: [AnyHashable: Any] = [:]
-
- // 展示广告,在App原window中展示
- ATAdManager.shared().showSplash(withPlacementID: splashPlacementID, config: config, window: view.window!, in: self, extra: configDict, delegate: self)
-
- // 记录广告展示时间
- adStartTime = nowMillis()
- incrementTodayAdCount()
- }
-
- private func finishSplashAd() {
- // 调用回调,跳转到主界面
- onSplashAdFinished?()
- }
-
- // MARK: - Helper Methods
- private func nowMillis() -> Int64 {
- Int64(Date().timeIntervalSince1970 * 1000)
- }
-
- private func incrementTodayAdCount() {
- let df = DateFormatter()
- df.locale = Locale(identifier: "en_US_POSIX")
- df.dateFormat = "yyyy-MM-dd"
- let key = "adCount_" + df.string(from: Date())
- let current = UserDefaults.standard.integer(forKey: key)
- UserDefaults.standard.set(current + 1, forKey: key)
- }
- }
- // MARK: - ATAdLoadingDelegate
- extension SplashAdViewController: ATAdLoadingDelegate {
- /// 开屏广告加载完成
- func didFinishLoadingSplashAD(withPlacementID placementID: String, isTimeout: Bool) {
- print("开屏加载成功:\(placementID)----是否超时:\(isTimeout)")
- isAdLoaded = true
- if !isTimeout {
- // 没有超时,展示开屏广告
- showSplashAd()
- } else {
- // 加载成功,但超时了
- finishSplashAd()
- }
- }
-
- /// 广告位加载失败
- func didFailToLoadAD(withPlacementID placementID: String, error: Error) {
- print("didFailToLoadADWithPlacementID:\(placementID) error:\(error)")
- // 所有广告源加载失败了,进入首页
- finishSplashAd()
- }
-
- /// 开屏广告加载超时
- func didTimeoutLoadingSplashAD(withPlacementID placementID: String) {
- print("didTimeoutLoadingSplashADWithPlacementID:\(placementID)")
- // 超时了,首页加载完成后进入首页
- finishSplashAd()
- }
-
- /// 获得展示收益
- func didRevenue(forPlacementID placementID: String, extra: [AnyHashable : Any]!) {
- print("didRevenueForPlacementID:\(placementID) with extra: \(extra ?? [:])")
- uploadAdRevenue(placementID: placementID, extra: extra ?? [:])
- }
-
- /// 加载成功且加载流程完毕
- func didFinishLoadingAD(withPlacementID placementID: String) {
- print("didFinishLoadingADWithPlacementID:\(placementID)")
- }
- }
- // MARK: - ATSplashDelegate
- extension SplashAdViewController {
- /// 开屏广告已展示
- func splashDidShow(forPlacementID placementID: String, extra: [AnyHashable : Any]!) {
- print("splashDidShowForPlacementID:\(placementID)")
- // 展示广告后可以隐藏,避免遮挡
- // 这里可以隐藏loading view
- }
-
- /// 开屏广告已关闭
- func splashDidClose(forPlacementID placementID: String, extra: [AnyHashable : Any]!) {
- print("splashDidCloseForPlacementID:\(placementID) extra:\(extra ?? [:])")
- // 进入首页
- finishSplashAd()
- }
-
- /// 开屏广告已点击
- func splashDidClick(forPlacementID placementID: String, extra: [AnyHashable : Any]!) {
- print("splashDidClickForPlacementID:\(placementID)")
- }
-
- /// 开屏广告展示失败
- func splashDidShowFailed(forPlacementID placementID: String, error: Error, extra: [AnyHashable : Any]!) {
- print("splashDidShowFailedForPlacementID:\(placementID) error:\(error)")
- // 没有展示成功,也进入首页,注意控制重复跳转
- finishSplashAd()
- }
-
- /// 开屏广告已打开或跳转深链接页面
- func splashDeepLinkOrJump(forPlacementID placementID: String, extra: [AnyHashable : Any]!, result success: Bool) {
- print("splashDeepLinkOrJumpForPlacementID:placementID:\(placementID)")
- }
-
- /// 开屏广告详情页已关闭
- func splashDetailDidClosed(forPlacementID placementID: String, extra: [AnyHashable : Any]!) {
- print("splashDetailDidClosedForPlacementID:\(placementID)")
- // 可在此获取关闭原因:dismiss_type
- // 热启动预加载(可选)
- // loadSplashAd()
- }
-
- /// 开屏广告关闭计时
- func splashCountdownTime(_ countdown: Int, forPlacementID placementID: String, extra: [AnyHashable : Any]!) {
- print("splashCountdownTime:\(countdown) forPlacementID:\(placementID)")
- }
-
- /// 开屏广告zoomout view已点击,仅Pangle 腾讯优量汇 V+支持
- func splashZoomOutViewDidClick(forPlacementID placementID: String, extra: [AnyHashable : Any]!) {
- print("splashZoomOutViewDidClickForPlacementID:\(placementID)")
- }
-
- /// 开屏广告zoomout view已关闭,仅Pangle 腾讯优量汇 V+支持
- func splashZoomOutViewDidClose(forPlacementID placementID: String, extra: [AnyHashable : Any]!) {
- print("splashZoomOutViewDidCloseForPlacementID:\(placementID)")
- }
- }
- // MARK: - Ad Revenue Upload
- extension SplashAdViewController {
- private func uploadAdRevenue(placementID: String, extra: [AnyHashable: Any]) {
- // 计算 begin / finish(毫秒)并格式化为 "yyyy-MM-dd HH:mm:ss"
- let beginMs = adStartTime
- let finishMs = nowMillis()
- let beginTime = formatMillisToString(beginMs)
- let finishTime = formatMillisToString(finishMs)
-
- // 将 AnyHashable-key 的字典转为 String-key,且递归转为 JSON 可序列化类型
- func jsonSafe(_ value: Any) -> Any {
- if value is String || value is NSNumber || value is NSNull { return value }
- if let d = value as? [String: Any] { return d.mapValues { jsonSafe($0) } }
- if let d = value as? [AnyHashable: Any] {
- var nd: [String: Any] = [:]
- d.forEach { nd[String(describing: $0.key)] = jsonSafe($0.value) }
- return nd
- }
- if let arr = value as? [Any] { return arr.map { jsonSafe($0) } }
- if let v = value as? NSValue {
- // 处理常见 CoreGraphics 结构体;其余转描述串
- let type = String(cString: v.objCType)
- #if canImport(CoreGraphics)
- if type == "{CGSize=dd}" {
- let s = v.cgSizeValue
- return ["width": s.width, "height": s.height]
- } else if type == "{CGPoint=dd}" {
- let p = v.cgPointValue
- return ["x": p.x, "y": p.y]
- } else if type == "{CGRect={CGPoint=dd}{CGSize=dd}}" {
- let r = v.cgRectValue
- return ["x": r.origin.x, "y": r.origin.y, "width": r.size.width, "height": r.size.height]
- }
- #endif
- return v.description
- }
- if let date = value as? Date { return ISO8601DateFormatter().string(from: date) }
- if let url = value as? URL { return url.absoluteString }
- return String(describing: value)
- }
- // 注意:这里用 jsonSafe 处理每个值
- var extraDict: [String: Any] = [:]
- extra.forEach { (k, v) in
- extraDict[String(describing: k)] = jsonSafe(v)
- }
- // 便捷取值工具
- func str(_ key: String) -> String {
- if let v = extraDict[key] { return String(describing: v) }
- return ""
- }
- func intVal(_ key: String) -> Int {
- if let v = extraDict[key] as? Int { return v }
- if let s = extraDict[key] as? String, let i = Int(s) { return i }
- if let d = extraDict[key] as? Double { return Int(d) }
- return 0
- }
- func dbl(_ key: String) -> Double {
- if let v = extraDict[key] as? Double { return v }
- if let s = extraDict[key] as? String, let d = Double(s) { return d }
- if let i = extraDict[key] as? Int { return Double(i) }
- return 0
- }
- // —— 字段映射(对齐安卓) ——
- // adSourceId
- let adSourceId = intVal("adsource_id")
- // networkFormId / networkName / networkPlacementId
- let networkFormId = intVal("network_firm_id")
- let networkName = str("network_name")
- let networkPlacementId = str("network_placement_id")
- // placementId(iOS 回传里一般是 adunit_id;若无,用回调给的 placementID)
- let placementIdValue = str("adunit_id").isEmpty ? placementID : str("adunit_id")
- // recordId(有的 key 叫 requestId,也可能是 req_id,兜底)
- let recordId = str("requestId").isEmpty ? str("req_id") : str("requestId")
- // revenue(publisher_revenue)
- let revenue = dbl("publisher_revenue")
- // userId / nickName
- // let userId = UserDefaults.standard.string(forKey: "roleID") ?? ""
- let nickName = UserDefaults.standard.string(forKey: "memberName") ?? ""
- // ecpm:优先取 adsource_price;若没有,用 revenue*1000 估算一个(单位同 revenue)
- var ecpmStr = str("adsource_price")
- if ecpmStr.isEmpty {
- let ecpm = revenue * 1000.0
- ecpmStr = String(format: "%.6f", ecpm)
- }
- // adSourceIndex / adSourceType(优先 adsource_bid_type: 0=非竞价, 1=竞价)
- let adSourceIndex = intVal("adsource_index")
- // adSourceType 依据 adunit_format 映射:Native->0, RewardedVideo->1, Banner->2, Interstitial->3, Splash->4
- let adUnitFormatRaw = str("adunit_format")
- let adUnitFormat = adUnitFormatRaw.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
- let adSourceType: Int
- switch adUnitFormat {
- case "native":
- adSourceType = 0
- case "rewardedvideo", "rewarded_video", "rewarded":
- adSourceType = 1
- case "banner":
- adSourceType = 2
- case "interstitial":
- adSourceType = 3
- case "splash":
- adSourceType = 4
- default:
- // 兜底逻辑:保持原有取值(若 SDK 提供 adsource_bid_type 则沿用)
- adSourceType = intVal("adsource_bid_type")
- }
- // resultJson:把 SDK 回传完整塞进去,便于排查
- let resultJsonData = try? JSONSerialization.data(withJSONObject: extraDict, options: [])
- let resultJson = resultJsonData.flatMap { String(data: $0, encoding: .utf8) } ?? "{}"
-
- // appId:iOS 若无可用 App ID,可用 Bundle Identifier 兜底
- let appId = kTakuAppID
- let adCount = todayAdCount()
-
- // 组装请求体(与安卓 map 字段完全一致)
- let body: [String: Any] = [
- "adSourceId": adSourceId,
- "beginTime": beginTime,
- "finishTime": finishTime,
- "networkFormId": networkFormId,
- "networkName": networkName,
- "networkPlacementId": networkPlacementId,
- "nickName": nickName,
- "placementId": placementIdValue,
- "recordId": recordId,
- "revenue": revenue,
- "userId": "",
- "ecpm": ecpmStr,
- "adSourceIndex": adSourceIndex,
- "adSourceType": adSourceType,
- "resultJson": resultJson,
- "appId": appId,
- "adCount": adCount,
- "iosId": UIDevice.current.identifierForVendor?.uuidString ?? "", //iOS 设备id
- "begintimestamp": beginMs,
- "finishtimestamp": finishMs
- ]
-
- // 发起上报
- let urlString = "\(apiBaseURL)/ad/saveRecord"
- guard let url = URL(string: urlString) else {
- print("[AD-Upload] URL 无效: \(urlString)")
- return
- }
- var request = URLRequest(url: url)
- request.httpMethod = "POST"
- request.setValue("application/json", forHTTPHeaderField: "Content-Type")
- request.httpBody = try? JSONSerialization.data(withJSONObject: body, options: [])
- if let body = request.httpBody,
- let bodyString = String(data: body, encoding: .utf8) {
- print("[AD-Upload] Request URL: \(url)")
- print("[AD-Upload] Request Headers: \(request.allHTTPHeaderFields ?? [:])")
- print("[AD-Upload] Request Body: \(bodyString)")
- }
-
- URLSession.shared.dataTask(with: request) { data, response, error in
- if let error = error {
- print("[AD-Upload] 网络错误: \(error.localizedDescription)")
- return
- }
- if let http = response as? HTTPURLResponse {
- print("[AD-Upload] 响应状态码: \(http.statusCode)")
- }
- if let data = data, let s = String(data: data, encoding: .utf8) {
- print("[AD-Upload] 响应: \(s)")
- }
- }.resume()
- }
-
- // 将毫秒时间戳格式化为 "yyyy-MM-dd HH:mm:ss"
- private func formatMillisToString(_ ms: Int64) -> String {
- let date = Date(timeIntervalSince1970: TimeInterval(ms) / 1000.0)
- let df = DateFormatter()
- df.locale = Locale(identifier: "en_US_POSIX")
- df.timeZone = TimeZone.current
- df.dateFormat = "yyyy-MM-dd HH:mm:ss"
- return df.string(from: date)
- }
-
- // ===== 当天广告观看次数(持久化到 UserDefaults)=====
- private func todayAdCount() -> Int {
- let df = DateFormatter()
- df.locale = Locale(identifier: "en_US_POSIX")
- df.dateFormat = "yyyy-MM-dd"
- let key = "adCount_" + df.string(from: Date())
- return UserDefaults.standard.integer(forKey: key)
- }
- }
|