SettingsViewController.swift 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866
  1. //
  2. // SettingsViewController.swift
  3. // VenusKitto
  4. //
  5. // Created by Neoa on 2025/8/27.
  6. //
  7. import Foundation
  8. import UIKit
  9. import StoreKit
  10. import AnyThinkBanner
  11. import AnyThinkSDK
  12. import AnyThinkInterstitial
  13. import AnyThinkNative
  14. // MARK: - Settings
  15. final class SettingsViewController: UIViewController, ATAdLoadingDelegate, ATBannerDelegate, ATInterstitialDelegate, ATNativeADDelegate {
  16. // 背景图
  17. private let bgImageView: UIImageView = {
  18. let iv = UIImageView(image: UIImage(named: "wode465"))
  19. iv.contentMode = .scaleAspectFill
  20. iv.clipsToBounds = true
  21. return iv
  22. }()
  23. private let titleLabel: UILabel = {
  24. let l = UILabel()
  25. l.text = ""
  26. l.font = .systemFont(ofSize: 28, weight: .semibold)
  27. l.textColor = UIColor(hex: "#2B2B2B")
  28. return l
  29. }()
  30. // 顶部用户卡片(手绘边框)
  31. private let profileCard = UIImageView(image: UIImage(named: "wode402"))
  32. private let avatarView = UIImageView()
  33. private let phoneLabel = UILabel()
  34. private let idLabel = UILabel()
  35. private let profileChevron = UIImageView(image: UIImage(systemName: "chevron.right"))
  36. // 三个功能行
  37. private lazy var rateRow = makeRow(title: "给个好评", action: #selector(tapRate))
  38. private lazy var peteRow = makeRow(title: "我的宠物", action: #selector(tapPet))
  39. private lazy var feedbackRow = makeRow(title: "意见反馈", action: #selector(tapFeedback))
  40. private lazy var aboutRow = makeRow(title: "关于我们", action: #selector(tapAbout))
  41. // MARK: - 广告相关属性
  42. // 广告位ID
  43. private let interstitialPlacementID = "b68d88cc36bca8"
  44. private let bannerPlacementID = "b68d88cc2ab33d"
  45. private let nativeRenderPlacementID = "b68d88cc1605f1"
  46. // 横幅广告容器
  47. private let bannerContainer: UIView = {
  48. let v = UIView()
  49. v.backgroundColor = .clear
  50. v.translatesAutoresizingMaskIntoConstraints = false
  51. return v
  52. }()
  53. private var bannerHeightConstraint: NSLayoutConstraint?
  54. private var bannerView: ATBannerView?
  55. private var bannerSize: CGSize {
  56. let screenWidth = UIScreen.main.bounds.width
  57. let aspectRatio: CGFloat = 320 / 50
  58. let bannerWidth = screenWidth
  59. let bannerHeight = bannerWidth / aspectRatio
  60. return CGSize(width: bannerWidth, height: bannerHeight)
  61. }
  62. // 原生信息流广告容器
  63. private let nativeAdContainer: UIView = {
  64. let view = UIView()
  65. view.backgroundColor = .clear
  66. view.translatesAutoresizingMaskIntoConstraints = false
  67. view.isUserInteractionEnabled = true
  68. return view
  69. }()
  70. private var nativeAdView: ATNativeADView?
  71. private var selfRenderView: RenderUnitView?
  72. private var nativeAdOffer: ATNativeAdOffer?
  73. private var nativeAdHeightConstraint: NSLayoutConstraint?
  74. // 广告展示开始时间(毫秒),key 为 placementID
  75. private var adStartTimes: [String: Int64] = [:]
  76. // 当前时间戳(毫秒)
  77. private func nowMillis() -> Int64 {
  78. Int64(Date().timeIntervalSince1970 * 1000)
  79. }
  80. // 将毫秒时间戳格式化为 "yyyy-MM-dd HH:mm:ss"
  81. private func formatMillisToString(_ ms: Int64) -> String {
  82. let date = Date(timeIntervalSince1970: TimeInterval(ms) / 1000.0)
  83. let df = DateFormatter()
  84. df.locale = Locale(identifier: "en_US_POSIX")
  85. df.timeZone = TimeZone.current
  86. df.dateFormat = "yyyy-MM-dd HH:mm:ss"
  87. return df.string(from: date)
  88. }
  89. // ===== 当天广告观看次数(持久化到 UserDefaults)=====
  90. private func adCountKeyForToday() -> String {
  91. let df = DateFormatter()
  92. df.locale = Locale(identifier: "en_US_POSIX")
  93. df.dateFormat = "yyyy-MM-dd"
  94. return "adCount_" + df.string(from: Date())
  95. }
  96. private func incrementTodayAdCount() {
  97. let key = adCountKeyForToday()
  98. let current = UserDefaults.standard.integer(forKey: key)
  99. UserDefaults.standard.set(current + 1, forKey: key)
  100. }
  101. private func todayAdCount() -> Int {
  102. let key = adCountKeyForToday()
  103. return UserDefaults.standard.integer(forKey: key)
  104. }
  105. override func viewDidLoad() {
  106. super.viewDidLoad()
  107. view.backgroundColor = UIColor(hex: "#FFFEFC")
  108. setupBackground()
  109. if let backImage = UIImage(named: "AddPet385") {
  110. let backButton = UIBarButtonItem(image: backImage.withRenderingMode(.alwaysOriginal), style: .plain, target: self, action: #selector(tapCancel))
  111. navigationItem.leftBarButtonItem = backButton
  112. }
  113. NotificationCenter.default.addObserver(self,
  114. selector: #selector(handleAvatarUpdated(_:)),
  115. name: Notification.Name("UserAvatarUpdated"),
  116. object: nil)
  117. buildUI()
  118. fillUserInfo()
  119. // 加载广告
  120. loadBannerAd()
  121. loadNativeAd()
  122. // // 延迟加载插屏广告
  123. // DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) {
  124. // self.loadInterstitialAd()
  125. // }
  126. }
  127. deinit {
  128. NotificationCenter.default.removeObserver(self, name: Notification.Name("UserAvatarUpdated"), object: nil)
  129. }
  130. @objc private func handleAvatarUpdated(_ note: Notification) {
  131. if let urlStr = note.object as? String, let url = URL(string: urlStr) {
  132. // 直接用通知携带的新 URL 刷新头像
  133. loadImage(from: url) { [weak self] img in
  134. self?.avatarView.image = img ?? UIImage(named: "Home372")
  135. }
  136. // 同步到本地,保证下次进入可以读到
  137. UserDefaults.standard.set(urlStr, forKey: "memberIcon")
  138. } else if let urlStr = (UserDefaults.standard.string(forKey: "memberIcon")),
  139. let url = URL(string: urlStr) {
  140. // 兜底:没有携带 object 时,从本地读取刷新
  141. loadImage(from: url) { [weak self] img in
  142. self?.avatarView.image = img ?? UIImage(named: "Home372")
  143. }
  144. }
  145. }
  146. private func buildUI() {
  147. // 顶部标题
  148. view.addSubview(titleLabel)
  149. titleLabel.translatesAutoresizingMaskIntoConstraints = false
  150. NSLayoutConstraint.activate([
  151. titleLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 12),
  152. titleLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 24)
  153. ])
  154. // 用户卡片
  155. profileCard.contentMode = .scaleToFill
  156. profileCard.isUserInteractionEnabled = true
  157. let tapCard = UITapGestureRecognizer(target: self, action: #selector(tapProfile))
  158. profileCard.addGestureRecognizer(tapCard)
  159. view.addSubview(profileCard)
  160. profileCard.translatesAutoresizingMaskIntoConstraints = false
  161. NSLayoutConstraint.activate([
  162. profileCard.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 12),
  163. profileCard.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
  164. profileCard.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16),
  165. profileCard.heightAnchor.constraint(equalToConstant: 92)
  166. ])
  167. avatarView.backgroundColor = UIColor(hex: "#FFF4CC")
  168. avatarView.layer.cornerRadius = 12
  169. avatarView.layer.masksToBounds = true
  170. avatarView.contentMode = .scaleAspectFill
  171. phoneLabel.font = .systemFont(ofSize: 16, weight: .semibold)
  172. phoneLabel.textColor = UIColor(hex: "#2B2B2B")
  173. idLabel.font = .systemFont(ofSize: 12)
  174. idLabel.textColor = UIColor(hex: "#9B9B9B")
  175. profileChevron.tintColor = UIColor(hex: "#5B4227")
  176. profileChevron.setContentCompressionResistancePriority(.required, for: .horizontal)
  177. [avatarView, phoneLabel, idLabel, profileChevron].forEach {
  178. $0.translatesAutoresizingMaskIntoConstraints = false
  179. profileCard.addSubview($0)
  180. }
  181. NSLayoutConstraint.activate([
  182. avatarView.leadingAnchor.constraint(equalTo: profileCard.leadingAnchor, constant: 16),
  183. avatarView.centerYAnchor.constraint(equalTo: profileCard.centerYAnchor),
  184. avatarView.widthAnchor.constraint(equalToConstant: 56),
  185. avatarView.heightAnchor.constraint(equalToConstant: 56),
  186. profileChevron.trailingAnchor.constraint(equalTo: profileCard.trailingAnchor, constant: -12),
  187. profileChevron.centerYAnchor.constraint(equalTo: profileCard.centerYAnchor),
  188. profileChevron.widthAnchor.constraint(equalToConstant: 16),
  189. profileChevron.heightAnchor.constraint(equalToConstant: 16),
  190. phoneLabel.leadingAnchor.constraint(equalTo: avatarView.trailingAnchor, constant: 12),
  191. phoneLabel.trailingAnchor.constraint(lessThanOrEqualTo: profileChevron.leadingAnchor, constant: -8),
  192. phoneLabel.bottomAnchor.constraint(equalTo: profileCard.centerYAnchor, constant: -2),
  193. idLabel.leadingAnchor.constraint(equalTo: phoneLabel.leadingAnchor),
  194. idLabel.topAnchor.constraint(equalTo: profileCard.centerYAnchor, constant: 2),
  195. idLabel.trailingAnchor.constraint(lessThanOrEqualTo: profileChevron.leadingAnchor, constant: -8)
  196. ])
  197. // 添加广告容器
  198. view.addSubview(bannerContainer)
  199. view.addSubview(nativeAdContainer)
  200. // 三个功能 row
  201. let stack = UIStackView(arrangedSubviews: [rateRow, peteRow, feedbackRow, aboutRow])
  202. stack.axis = .vertical
  203. stack.spacing = 12
  204. stack.alignment = .fill
  205. view.addSubview(stack)
  206. stack.translatesAutoresizingMaskIntoConstraints = false
  207. // 设置广告容器约束
  208. bannerHeightConstraint = bannerContainer.heightAnchor.constraint(equalToConstant: 0)
  209. bannerHeightConstraint?.isActive = true
  210. nativeAdHeightConstraint = nativeAdContainer.heightAnchor.constraint(equalToConstant: 0)
  211. nativeAdHeightConstraint?.isActive = true
  212. NSLayoutConstraint.activate([
  213. // 横幅广告容器(位于用户卡片下方)
  214. bannerContainer.topAnchor.constraint(equalTo: profileCard.bottomAnchor, constant: 8),
  215. bannerContainer.leadingAnchor.constraint(equalTo: view.leadingAnchor),
  216. bannerContainer.trailingAnchor.constraint(equalTo: view.trailingAnchor),
  217. // 功能按钮栈(位于横幅广告下方)
  218. stack.topAnchor.constraint(equalTo: bannerContainer.bottomAnchor, constant: 16),
  219. stack.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
  220. stack.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16),
  221. stack.bottomAnchor.constraint(lessThanOrEqualTo: nativeAdContainer.topAnchor, constant: -16),
  222. // 原生信息流广告容器(位于页面底部)
  223. nativeAdContainer.leadingAnchor.constraint(equalTo: view.leadingAnchor),
  224. nativeAdContainer.trailingAnchor.constraint(equalTo: view.trailingAnchor),
  225. nativeAdContainer.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor)
  226. ])
  227. }
  228. private func setupBackground() {
  229. view.insertSubview(bgImageView, at: 0)
  230. bgImageView.translatesAutoresizingMaskIntoConstraints = false
  231. NSLayoutConstraint.activate([
  232. bgImageView.topAnchor.constraint(equalTo: view.topAnchor),
  233. bgImageView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
  234. bgImageView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
  235. bgImageView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
  236. ])
  237. }
  238. @objc private func tapCancel() { navigationController?.popViewController(animated: true)
  239. }
  240. private func makeRow(title: String, action: Selector) -> UIControl {
  241. let container = UIControl()
  242. container.backgroundColor = .white
  243. container.layer.cornerRadius = 14
  244. container.layer.shadowColor = UIColor.black.cgColor
  245. container.layer.shadowOpacity = 0.08
  246. container.layer.shadowRadius = 10
  247. container.layer.shadowOffset = CGSize(width: 0, height: 4)
  248. container.heightAnchor.constraint(equalToConstant: 64).isActive = true
  249. let label = UILabel()
  250. label.text = title
  251. label.font = .systemFont(ofSize: 16)
  252. label.textColor = UIColor(hex: "#2B2B2B")
  253. let chevron = UIImageView(image: UIImage(systemName: "chevron.right"))
  254. chevron.tintColor = UIColor(hex: "#5B4227")
  255. chevron.setContentCompressionResistancePriority(.required, for: .horizontal)
  256. container.addSubview(label)
  257. container.addSubview(chevron)
  258. label.translatesAutoresizingMaskIntoConstraints = false
  259. chevron.translatesAutoresizingMaskIntoConstraints = false
  260. NSLayoutConstraint.activate([
  261. label.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 16),
  262. label.centerYAnchor.constraint(equalTo: container.centerYAnchor),
  263. chevron.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: -16),
  264. chevron.centerYAnchor.constraint(equalTo: container.centerYAnchor),
  265. chevron.widthAnchor.constraint(equalToConstant: 16),
  266. chevron.heightAnchor.constraint(equalToConstant: 16)
  267. ])
  268. container.addTarget(self, action: action, for: .touchUpInside)
  269. return container
  270. }
  271. private func fillUserInfo() {
  272. // 显示手机号(打码)与 ID
  273. let phone = UserDefaults.standard.string(forKey: "memberPhone") ?? "登录"
  274. let uid = UserDefaults.standard.string(forKey: "userId") ?? "--"
  275. phoneLabel.text = maskedPhone(phone)
  276. idLabel.text = "ID:\(uid)"
  277. if let urlStr = UserDefaults.standard.string(forKey: "memberIcon"),
  278. let url = URL(string: urlStr) {
  279. loadImage(from: url) { [weak self] img in
  280. self?.avatarView.image = img ?? UIImage(named: "Home372")
  281. }
  282. } else {
  283. avatarView.image = UIImage(named: "Home372")
  284. }
  285. }
  286. private func maskedPhone(_ s: String) -> String {
  287. let digits = s.filter { $0.isNumber }
  288. guard digits.count >= 7 else { return s }
  289. let start = digits.prefix(3)
  290. let end = digits.suffix(4)
  291. return "\(start)****\(end)"
  292. }
  293. private func loadImage(from url: URL, completion: @escaping (UIImage?) -> Void) {
  294. URLSession.shared.dataTask(with: url) { data, _, _ in
  295. guard let data = data, let img = UIImage(data: data) else {
  296. DispatchQueue.main.async { completion(nil) }
  297. return
  298. }
  299. DispatchQueue.main.async { completion(img) }
  300. }.resume()
  301. }
  302. // MARK: - Actions
  303. @objc private func tapProfile() {
  304. // 检查登录状态
  305. let isLoggedIn = UserDefaults.standard.bool(forKey: "isLogggedIn")
  306. let userToken = UserDefaults.standard.string(forKey: "userToken")
  307. // 如果没有登录状态或者没有有效的token,弹出登录界面
  308. if !isLoggedIn || userToken?.isEmpty != false {
  309. showLoginViewController()
  310. return
  311. }
  312. // 已登录,跳转到个人信息页
  313. let userSettingsVC = UserSettingsViewController()
  314. userSettingsVC.hidesBottomBarWhenPushed = true
  315. navigationController?.pushViewController(userSettingsVC, animated: true)
  316. }
  317. private func showLoginViewController() {
  318. let loginVC = LoginViewController()
  319. let navController = UINavigationController(rootViewController: loginVC)
  320. navController.modalPresentationStyle = .fullScreen
  321. present(navController, animated: true)
  322. }
  323. @objc private func tapRate() {
  324. // 跳 App Store 评分(占位)
  325. SKStoreReviewController.requestReview()
  326. }
  327. @objc private func tapPet() {
  328. // 检查登录状态
  329. let isLoggedIn = UserDefaults.standard.bool(forKey: "isLogggedIn")
  330. let userToken = UserDefaults.standard.string(forKey: "userToken")
  331. // 如果没有登录状态或者没有有效的token,弹出登录界面
  332. if !isLoggedIn || userToken?.isEmpty != false {
  333. showLoginViewController()
  334. return
  335. }
  336. //进入我的宠物界面
  337. let vc = MyPetsViewController()
  338. vc.hidesBottomBarWhenPushed = true
  339. navigationController?.pushViewController(vc, animated: true)
  340. }
  341. @objc private func tapFeedback() {
  342. let vc = FeedbackViewController()
  343. vc.hidesBottomBarWhenPushed = true
  344. navigationController?.pushViewController(vc, animated: true)
  345. }
  346. @objc private func tapAbout() {
  347. let vc = AboutViewController()
  348. vc.hidesBottomBarWhenPushed = true
  349. navigationController?.pushViewController(vc, animated: true)
  350. }
  351. // MARK: - 广告方法实现
  352. // MARK: - 横幅广告方法
  353. private func loadBannerAd() {
  354. var extra: [AnyHashable: Any] = [:]
  355. extra[kATAdLoadingExtraBannerAdSizeKey] = NSValue(cgSize: bannerSize)
  356. extra[kATAdLoadingExtraMediaExtraKey] = "SettingsVC"
  357. ATAdManager.shared().loadAD(withPlacementID: bannerPlacementID, extra: extra, delegate: self)
  358. }
  359. private func showBannerIfReady() {
  360. let config = ATShowConfig(scene: nil, showCustomExt: nil)
  361. guard let bView = ATAdManager.shared().retrieveBannerView(forPlacementID: bannerPlacementID, config: config) else { return }
  362. bView.delegate = self
  363. bView.presentingViewController = self
  364. bView.translatesAutoresizingMaskIntoConstraints = false
  365. bannerContainer.addSubview(bView)
  366. NSLayoutConstraint.activate([
  367. bView.centerXAnchor.constraint(equalTo: bannerContainer.centerXAnchor),
  368. bView.topAnchor.constraint(equalTo: bannerContainer.topAnchor),
  369. bView.widthAnchor.constraint(equalToConstant: bannerSize.width),
  370. bView.heightAnchor.constraint(equalToConstant: bannerSize.height)
  371. ])
  372. bannerView = bView
  373. bannerHeightConstraint?.constant = bannerSize.height
  374. UIView.animate(withDuration: 0.25) { self.view.layoutIfNeeded() }
  375. }
  376. private func removeBanner() {
  377. bannerView?.destroyBanner()
  378. bannerView?.removeFromSuperview()
  379. bannerView = nil
  380. bannerHeightConstraint?.constant = 0
  381. UIView.animate(withDuration: 0.25) { self.view.layoutIfNeeded() }
  382. }
  383. // MARK: - 插屏广告方法
  384. private func loadInterstitialAd() {
  385. let screenWidth = UIScreen.main.bounds.width
  386. let size = CGSize(width: screenWidth - 30.0, height: 300.0)
  387. let extra: [AnyHashable: Any] = [
  388. kATAdLoadingExtraMediaExtraKey: "SettingsView",
  389. kATInterstitialExtraAdSizeKey: NSValue(cgSize: size)
  390. ]
  391. ATAdManager.shared().loadAD(withPlacementID: interstitialPlacementID, extra: extra, delegate: self)
  392. }
  393. private func showInterstitialAd() {
  394. guard ATAdManager.shared().interstitialReady(forPlacementID: interstitialPlacementID) else {
  395. print("插屏广告尚未加载完成")
  396. return
  397. }
  398. ATAdManager.shared().showInterstitial(withPlacementID: interstitialPlacementID, in: self, delegate: self)
  399. }
  400. // MARK: - 原生信息流广告方法
  401. private func loadNativeAd() {
  402. let extra: [AnyHashable: Any] = [
  403. kATAdLoadingExtraMediaExtraKey: "SettingsVC",
  404. kATExtraInfoNativeAdSizeKey: NSValue(cgSize: CGSize(width: UIScreen.main.bounds.width - 24, height: 170))
  405. ]
  406. ATAdManager.shared().loadAD(withPlacementID: nativeRenderPlacementID, extra: extra, delegate: self)
  407. }
  408. private func showNativeAdIfReady() {
  409. // 检查广告是否准备好
  410. guard ATAdManager.shared().nativeAdReady(forPlacementID: nativeRenderPlacementID) else {
  411. print("Native ad not ready")
  412. return
  413. }
  414. // 获取广告offer
  415. guard let offer = ATAdManager.shared().getNativeAdOffer(withPlacementID: nativeRenderPlacementID) else {
  416. print("Failed to get native ad offer")
  417. return
  418. }
  419. self.nativeAdOffer = offer
  420. // 创建自渲染视图
  421. let selfRenderView = RenderUnitView(offer: offer)
  422. self.selfRenderView = selfRenderView
  423. // 初始化配置
  424. let config = ATNativeADConfiguration()
  425. config.adFrame = CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 170)
  426. config.mediaViewFrame = CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 150)
  427. config.delegate = self
  428. config.rootViewController = self
  429. config.sizeToFit = true
  430. config.videoPlayType = .alwaysAutoPlayType
  431. // 设置logo位置
  432. config.logoViewFrame = CGRect(x: UIScreen.main.bounds.width - 48, y: 140, width: 36, height: 20)
  433. // 创建原生广告视图
  434. let nativeAdView = ATNativeADView(configuration: config, currentOffer: offer, placementID: nativeRenderPlacementID)
  435. self.nativeAdView = nativeAdView
  436. // 获取mediaView
  437. if let mediaView = nativeAdView.getMediaView() {
  438. selfRenderView.installMediaView(mediaView)
  439. }
  440. // 创建可点击组件数组
  441. var clickableViewArray: [UIView] = []
  442. clickableViewArray.append(contentsOf: [
  443. selfRenderView.headlineLabel,
  444. selfRenderView.bodyLabel,
  445. selfRenderView.heroImageView
  446. ])
  447. // 注册点击事件
  448. nativeAdView.registerClickableViewArray(clickableViewArray)
  449. // 绑定组件
  450. let info = ATNativePrepareInfo.load { prepareInfo in
  451. prepareInfo.textLabel = selfRenderView.bodyLabel
  452. prepareInfo.titleLabel = selfRenderView.headlineLabel
  453. prepareInfo.mainImageView = selfRenderView.heroImageView
  454. prepareInfo.dislikeButton = selfRenderView.closeControl
  455. if let mv = selfRenderView.mediaContainer {
  456. prepareInfo.mediaView = mv
  457. }
  458. }
  459. nativeAdView.prepare(with: info)
  460. // 渲染广告
  461. offer.renderer(with: config, selfRenderView: selfRenderView, nativeADView: nativeAdView)
  462. // 添加到容器(必须把 nativeAdView 放到视图层级中,点击与视频追踪才会生效)
  463. nativeAdContainer.subviews.forEach { $0.removeFromSuperview() }
  464. nativeAdContainer.addSubview(nativeAdView)
  465. nativeAdView.translatesAutoresizingMaskIntoConstraints = false
  466. NSLayoutConstraint.activate([
  467. nativeAdView.leadingAnchor.constraint(equalTo: nativeAdContainer.leadingAnchor),
  468. nativeAdView.trailingAnchor.constraint(equalTo: nativeAdContainer.trailingAnchor),
  469. nativeAdView.topAnchor.constraint(equalTo: nativeAdContainer.topAnchor),
  470. nativeAdView.bottomAnchor.constraint(equalTo: nativeAdContainer.bottomAnchor)
  471. ])
  472. // 自渲染视图放入 nativeAdView 内
  473. nativeAdView.addSubview(selfRenderView)
  474. selfRenderView.translatesAutoresizingMaskIntoConstraints = false
  475. NSLayoutConstraint.activate([
  476. selfRenderView.leadingAnchor.constraint(equalTo: nativeAdView.leadingAnchor),
  477. selfRenderView.trailingAnchor.constraint(equalTo: nativeAdView.trailingAnchor),
  478. selfRenderView.topAnchor.constraint(equalTo: nativeAdView.topAnchor),
  479. selfRenderView.bottomAnchor.constraint(equalTo: nativeAdView.bottomAnchor)
  480. ])
  481. // 设置容器高度约束
  482. nativeAdHeightConstraint?.constant = 170
  483. UIView.animate(withDuration: 0.25) { self.view.layoutIfNeeded() }
  484. print("Native self-render ad displayed successfully")
  485. }
  486. private func removeNativeAd() {
  487. // 清理自渲染视图
  488. selfRenderView?.teardown()
  489. selfRenderView?.removeFromSuperview()
  490. selfRenderView = nil
  491. // 清理原生广告视图
  492. nativeAdView?.removeFromSuperview()
  493. nativeAdView?.destroyNative()
  494. nativeAdView = nil
  495. // 清理offer
  496. nativeAdOffer = nil
  497. nativeAdHeightConstraint?.constant = 0
  498. UIView.animate(withDuration: 0.25) { self.view.layoutIfNeeded() }
  499. }
  500. // MARK: - ATAdLoadingDelegate
  501. func didFinishLoadingAD(withPlacementID placementID: String) {
  502. if placementID == bannerPlacementID {
  503. if ATAdManager.shared().bannerAdReady(forPlacementID: placementID) {
  504. showBannerIfReady()
  505. }
  506. print("Banner Ad Loaded: \(placementID)")
  507. } else if placementID == interstitialPlacementID {
  508. if ATAdManager.shared().interstitialReady(forPlacementID: placementID) {
  509. showInterstitialAd()
  510. }
  511. print("Interstitial Ad Loaded: \(placementID)")
  512. } else if placementID == nativeRenderPlacementID {
  513. if ATAdManager.shared().nativeAdReady(forPlacementID: placementID) {
  514. showNativeAdIfReady()
  515. }
  516. print("Native Ad Loaded: \(placementID)")
  517. }
  518. }
  519. func didFailToLoadAD(withPlacementID placementID: String, error: Error) {
  520. print("Ad load failed(\(placementID)): \(error)")
  521. }
  522. func didRevenue(forPlacementID placementID: String, extra: [AnyHashable : Any]!) {
  523. print("Ad revenue: placement=\(placementID), extra=\(extra ?? [:])")
  524. uploadAdRevenue(placementID: placementID, extra: extra ?? [:])
  525. }
  526. // MARK: - ATBannerDelegate
  527. func bannerView(_ bannerView: ATBannerView, didTapCloseButtonWithPlacementID placementID: String, extra: [AnyHashable : Any]!) {
  528. removeBanner()
  529. }
  530. func bannerView(_ bannerView: ATBannerView, didShowAdWithPlacementID placementID: String, extra: [AnyHashable : Any]!) {
  531. print("Banner did show: \(placementID)")
  532. adStartTimes[placementID] = nowMillis()
  533. incrementTodayAdCount()
  534. }
  535. func bannerView(_ bannerView: ATBannerView, didClickWithPlacementID placementID: String, extra: [AnyHashable : Any]!) {
  536. print("Banner did click: \(placementID)")
  537. }
  538. func bannerView(_ bannerView: ATBannerView, didAutoRefreshWithPlacement placementID: String, extra: [AnyHashable : Any]!) {
  539. print("Banner auto refresh: \(placementID)")
  540. }
  541. func bannerView(_ bannerView: ATBannerView, failedToAutoRefreshWithPlacementID placementID: String, error: Error) {
  542. print("Banner auto refresh failed: \(placementID) error=\(error)")
  543. }
  544. func bannerView(_ bannerView: ATBannerView, didDeepLinkOrJumpForPlacementID placementID: String, extra: [AnyHashable : Any]!, result success: Bool) {
  545. print("Banner deeplink/jump: \(placementID) success=\(success)")
  546. }
  547. // MARK: - ATInterstitialDelegate
  548. func interstitialDidShow(forPlacementID placementID: String, extra: [AnyHashable : Any]) {
  549. print("插屏广告展示完成:\(placementID) \(extra)")
  550. adStartTimes[placementID] = nowMillis()
  551. incrementTodayAdCount()
  552. }
  553. func interstitialDidClick(forPlacementID placementID: String, extra: [AnyHashable : Any]) {
  554. print("插屏广告点击:\(placementID)")
  555. }
  556. func interstitialDidClose(forPlacementID placementID: String, extra: [AnyHashable : Any]) {
  557. print("插屏广告关闭:\(placementID) \(extra)")
  558. }
  559. // MARK: - ATNativeADDelegate
  560. func didShowNativeAd(in adView: ATNativeADView, placementID: String, extra: [AnyHashable: Any]?) {
  561. print("Native ad shown for \(placementID)")
  562. adStartTimes[placementID] = nowMillis()
  563. incrementTodayAdCount()
  564. }
  565. func didTapCloseButton(in adView: ATNativeADView, placementID: String, extra: [AnyHashable: Any]?) {
  566. print("Native ad closed for \(placementID)")
  567. removeNativeAd()
  568. }
  569. func didClickNativeAd(in adView: ATNativeADView, placementID: String, extra: [AnyHashable: Any]?) {
  570. print("Native ad clicked for \(placementID)")
  571. }
  572. // MARK: - 广告收益追踪
  573. private func uploadAdRevenue(placementID: String, extra: [AnyHashable: Any]) {
  574. // 计算 begin / finish(毫秒)并格式化为 "yyyy-MM-dd HH:mm:ss"
  575. let beginMs = adStartTimes[placementID] ?? nowMillis()
  576. let finishMs = nowMillis()
  577. print("testaa \(beginMs) \(String(describing: adStartTimes[placementID])) \(finishMs) \(placementID)")
  578. adStartTimes.removeValue(forKey: placementID) // 避免复用旧开始时间
  579. let beginTime = formatMillisToString(beginMs)
  580. let finishTime = formatMillisToString(finishMs)
  581. // 将 AnyHashable-key 的字典转为 String-key,且递归转为 JSON 可序列化类型
  582. func jsonSafe(_ value: Any) -> Any {
  583. if value is String || value is NSNumber || value is NSNull { return value }
  584. if let d = value as? [String: Any] { return d.mapValues { jsonSafe($0) } }
  585. if let d = value as? [AnyHashable: Any] {
  586. var nd: [String: Any] = [:]
  587. d.forEach { nd[String(describing: $0.key)] = jsonSafe($0.value) }
  588. return nd
  589. }
  590. if let arr = value as? [Any] { return arr.map { jsonSafe($0) } }
  591. if let v = value as? NSValue {
  592. // 处理常见 CoreGraphics 结构体;其余转描述串
  593. let type = String(cString: v.objCType)
  594. #if canImport(CoreGraphics)
  595. if type == "{CGSize=dd}" {
  596. let s = v.cgSizeValue
  597. return ["width": s.width, "height": s.height]
  598. } else if type == "{CGPoint=dd}" {
  599. let p = v.cgPointValue
  600. return ["x": p.x, "y": p.y]
  601. } else if type == "{CGRect={CGPoint=dd}{CGSize=dd}}" {
  602. let r = v.cgRectValue
  603. return ["x": r.origin.x, "y": r.origin.y, "width": r.size.width, "height": r.size.height]
  604. }
  605. #endif
  606. return v.description
  607. }
  608. if let date = value as? Date { return ISO8601DateFormatter().string(from: date) }
  609. if let url = value as? URL { return url.absoluteString }
  610. return String(describing: value)
  611. }
  612. // 注意:这里用 jsonSafe 处理每个值
  613. var extraDict: [String: Any] = [:]
  614. extra.forEach { (k, v) in
  615. extraDict[String(describing: k)] = jsonSafe(v)
  616. }
  617. // 便捷取值工具
  618. func str(_ key: String) -> String {
  619. if let v = extraDict[key] { return String(describing: v) }
  620. return ""
  621. }
  622. func intVal(_ key: String) -> Int {
  623. if let v = extraDict[key] as? Int { return v }
  624. if let s = extraDict[key] as? String, let i = Int(s) { return i }
  625. if let d = extraDict[key] as? Double { return Int(d) }
  626. return 0
  627. }
  628. func dbl(_ key: String) -> Double {
  629. if let v = extraDict[key] as? Double { return v }
  630. if let s = extraDict[key] as? String, let d = Double(s) { return d }
  631. if let i = extraDict[key] as? Int { return Double(i) }
  632. return 0
  633. }
  634. // —— 字段映射(对齐安卓) ——
  635. // adSourceId
  636. let adSourceId = intVal("adsource_id")
  637. // networkFormId / networkName / networkPlacementId
  638. let networkFormId = intVal("network_firm_id")
  639. let networkName = str("network_name")
  640. let networkPlacementId = str("network_placement_id")
  641. // placementId(iOS 回传里一般是 adunit_id;若无,用回调给的 placementID)
  642. let placementIdValue = str("adunit_id").isEmpty ? placementID : str("adunit_id")
  643. // recordId(有的 key 叫 requestId,也可能是 req_id,兜底)
  644. let recordId = str("requestId").isEmpty ? str("req_id") : str("requestId")
  645. // revenue(publisher_revenue)
  646. let revenue = dbl("publisher_revenue")
  647. // userId / nickName
  648. // let userId = UserDefaults.standard.string(forKey: "roleID") ?? ""
  649. let nickName = UserDefaults.standard.string(forKey: "memberName") ?? ""
  650. // ecpm:优先取 adsource_price;若没有,用 revenue*1000 估算一个(单位同 revenue)
  651. var ecpmStr = str("adsource_price")
  652. if ecpmStr.isEmpty {
  653. let ecpm = revenue * 1000.0
  654. ecpmStr = String(format: "%.6f", ecpm)
  655. }
  656. // adSourceIndex / adSourceType(优先 adsource_bid_type: 0=非竞价, 1=竞价)
  657. let adSourceIndex = intVal("adsource_index")
  658. // adSourceType 依据 adunit_format 映射:Native->0, RewardedVideo->1, Banner->2, Interstitial->3
  659. let adUnitFormatRaw = str("adunit_format")
  660. let adUnitFormat = adUnitFormatRaw.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
  661. let adSourceType: Int
  662. switch adUnitFormat {
  663. case "native":
  664. adSourceType = 0
  665. case "rewardedvideo", "rewarded_video", "rewarded":
  666. adSourceType = 1
  667. case "banner":
  668. adSourceType = 2
  669. case "interstitial":
  670. adSourceType = 3
  671. default:
  672. // 兜底逻辑:保持原有取值(若 SDK 提供 adsource_bid_type 则沿用)
  673. adSourceType = intVal("adsource_bid_type")
  674. }
  675. // resultJson:把 SDK 回传完整塞进去,便于排查
  676. let resultJsonData = try? JSONSerialization.data(withJSONObject: extraDict, options: [])
  677. let resultJson = resultJsonData.flatMap { String(data: $0, encoding: .utf8) } ?? "{}"
  678. // appId:iOS 若无可用 App ID,可用 Bundle Identifier 兜底
  679. let appId = kTakuAppID
  680. let adCount = todayAdCount()
  681. // 组装请求体(与安卓 map 字段完全一致)
  682. let body: [String: Any] = [
  683. "adSourceId": adSourceId,
  684. "beginTime": beginTime,
  685. "finishTime": finishTime,
  686. "networkFormId": networkFormId,
  687. "networkName": networkName,
  688. "networkPlacementId": networkPlacementId,
  689. "nickName": nickName,
  690. "placementId": placementIdValue,
  691. "recordId": recordId,
  692. "revenue": revenue,
  693. "userId": "",
  694. "ecpm": ecpmStr,
  695. "adSourceIndex": adSourceIndex,
  696. "adSourceType": adSourceType,
  697. "resultJson": resultJson,
  698. "appId": appId,
  699. "adCount": adCount,
  700. "iosId": UIDevice.current.identifierForVendor?.uuidString ?? "", //iOS 设备id
  701. "begintimestamp": beginMs,
  702. "finishtimestamp": finishMs
  703. ]
  704. // 发起上报
  705. let urlString = "\(apiBaseURL)/ad/saveRecord"
  706. guard let url = URL(string: urlString) else {
  707. print("[AD-Upload] URL 无效: \(urlString)")
  708. return
  709. }
  710. var request = URLRequest(url: url)
  711. request.httpMethod = "POST"
  712. request.setValue("application/json", forHTTPHeaderField: "Content-Type")
  713. request.httpBody = try? JSONSerialization.data(withJSONObject: body, options: [])
  714. if let body = request.httpBody,
  715. let bodyString = String(data: body, encoding: .utf8) {
  716. print("[AD-Upload] Request URL: \(url)")
  717. print("[AD-Upload] Request Headers: \(request.allHTTPHeaderFields ?? [:])")
  718. print("[AD-Upload] Request Body: \(bodyString)")
  719. }
  720. URLSession.shared.dataTask(with: request) { data, response, error in
  721. if let error = error {
  722. print("[AD-Upload] 网络错误: \(error.localizedDescription)")
  723. return
  724. }
  725. if let http = response as? HTTPURLResponse {
  726. print("[AD-Upload] 响应状态码: \(http.statusCode)")
  727. }
  728. if let data = data, let s = String(data: data, encoding: .utf8) {
  729. print("[AD-Upload] 响应: \(s)")
  730. }
  731. }.resume()
  732. }
  733. }