| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866 |
- //
- // SettingsViewController.swift
- // VenusKitto
- //
- // Created by Neoa on 2025/8/27.
- //
- import Foundation
- import UIKit
- import StoreKit
- import AnyThinkBanner
- import AnyThinkSDK
- import AnyThinkInterstitial
- import AnyThinkNative
- // MARK: - Settings
- final class SettingsViewController: UIViewController, ATAdLoadingDelegate, ATBannerDelegate, ATInterstitialDelegate, ATNativeADDelegate {
- // 背景图
- private let bgImageView: UIImageView = {
- let iv = UIImageView(image: UIImage(named: "wode465"))
- iv.contentMode = .scaleAspectFill
- iv.clipsToBounds = true
- return iv
- }()
- private let titleLabel: UILabel = {
- let l = UILabel()
- l.text = ""
- l.font = .systemFont(ofSize: 28, weight: .semibold)
- l.textColor = UIColor(hex: "#2B2B2B")
- return l
- }()
- // 顶部用户卡片(手绘边框)
- private let profileCard = UIImageView(image: UIImage(named: "wode402"))
- private let avatarView = UIImageView()
- private let phoneLabel = UILabel()
- private let idLabel = UILabel()
- private let profileChevron = UIImageView(image: UIImage(systemName: "chevron.right"))
- // 三个功能行
- private lazy var rateRow = makeRow(title: "给个好评", action: #selector(tapRate))
- private lazy var peteRow = makeRow(title: "我的宠物", action: #selector(tapPet))
- private lazy var feedbackRow = makeRow(title: "意见反馈", action: #selector(tapFeedback))
- private lazy var aboutRow = makeRow(title: "关于我们", action: #selector(tapAbout))
-
- // MARK: - 广告相关属性
- // 广告位ID
- private let interstitialPlacementID = "b68d88cc36bca8"
- private let bannerPlacementID = "b68d88cc2ab33d"
- private let nativeRenderPlacementID = "b68d88cc1605f1"
-
- // 横幅广告容器
- private let bannerContainer: UIView = {
- let v = UIView()
- v.backgroundColor = .clear
- v.translatesAutoresizingMaskIntoConstraints = false
- return v
- }()
- private var bannerHeightConstraint: NSLayoutConstraint?
- private var bannerView: ATBannerView?
- private var bannerSize: CGSize {
- let screenWidth = UIScreen.main.bounds.width
- let aspectRatio: CGFloat = 320 / 50
- let bannerWidth = screenWidth
- let bannerHeight = bannerWidth / aspectRatio
- return CGSize(width: bannerWidth, height: bannerHeight)
- }
-
- // 原生信息流广告容器
- private let nativeAdContainer: UIView = {
- let view = UIView()
- view.backgroundColor = .clear
- view.translatesAutoresizingMaskIntoConstraints = false
- view.isUserInteractionEnabled = true
- return view
- }()
- private var nativeAdView: ATNativeADView?
- private var selfRenderView: RenderUnitView?
- private var nativeAdOffer: ATNativeAdOffer?
- private var nativeAdHeightConstraint: NSLayoutConstraint?
- // 广告展示开始时间(毫秒),key 为 placementID
- private var adStartTimes: [String: Int64] = [:]
-
- // 当前时间戳(毫秒)
- private func nowMillis() -> Int64 {
- Int64(Date().timeIntervalSince1970 * 1000)
- }
-
- // 将毫秒时间戳格式化为 "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 adCountKeyForToday() -> String {
- let df = DateFormatter()
- df.locale = Locale(identifier: "en_US_POSIX")
- df.dateFormat = "yyyy-MM-dd"
- return "adCount_" + df.string(from: Date())
- }
- private func incrementTodayAdCount() {
- let key = adCountKeyForToday()
- let current = UserDefaults.standard.integer(forKey: key)
- UserDefaults.standard.set(current + 1, forKey: key)
- }
- private func todayAdCount() -> Int {
- let key = adCountKeyForToday()
- return UserDefaults.standard.integer(forKey: key)
- }
- override func viewDidLoad() {
- super.viewDidLoad()
- view.backgroundColor = UIColor(hex: "#FFFEFC")
- setupBackground()
-
- if let backImage = UIImage(named: "AddPet385") {
- let backButton = UIBarButtonItem(image: backImage.withRenderingMode(.alwaysOriginal), style: .plain, target: self, action: #selector(tapCancel))
- navigationItem.leftBarButtonItem = backButton
- }
- NotificationCenter.default.addObserver(self,
- selector: #selector(handleAvatarUpdated(_:)),
- name: Notification.Name("UserAvatarUpdated"),
- object: nil)
- buildUI()
- fillUserInfo()
-
- // 加载广告
- loadBannerAd()
- loadNativeAd()
-
- // // 延迟加载插屏广告
- // DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) {
- // self.loadInterstitialAd()
- // }
- }
-
- deinit {
- NotificationCenter.default.removeObserver(self, name: Notification.Name("UserAvatarUpdated"), object: nil)
- }
-
- @objc private func handleAvatarUpdated(_ note: Notification) {
- if let urlStr = note.object as? String, let url = URL(string: urlStr) {
- // 直接用通知携带的新 URL 刷新头像
- loadImage(from: url) { [weak self] img in
- self?.avatarView.image = img ?? UIImage(named: "Home372")
- }
- // 同步到本地,保证下次进入可以读到
- UserDefaults.standard.set(urlStr, forKey: "memberIcon")
- } else if let urlStr = (UserDefaults.standard.string(forKey: "memberIcon")),
- let url = URL(string: urlStr) {
- // 兜底:没有携带 object 时,从本地读取刷新
- loadImage(from: url) { [weak self] img in
- self?.avatarView.image = img ?? UIImage(named: "Home372")
- }
- }
- }
- private func buildUI() {
- // 顶部标题
- view.addSubview(titleLabel)
- titleLabel.translatesAutoresizingMaskIntoConstraints = false
- NSLayoutConstraint.activate([
- titleLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 12),
- titleLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 24)
- ])
- // 用户卡片
- profileCard.contentMode = .scaleToFill
- profileCard.isUserInteractionEnabled = true
- let tapCard = UITapGestureRecognizer(target: self, action: #selector(tapProfile))
- profileCard.addGestureRecognizer(tapCard)
- view.addSubview(profileCard)
- profileCard.translatesAutoresizingMaskIntoConstraints = false
- NSLayoutConstraint.activate([
- profileCard.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 12),
- profileCard.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
- profileCard.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16),
- profileCard.heightAnchor.constraint(equalToConstant: 92)
- ])
- avatarView.backgroundColor = UIColor(hex: "#FFF4CC")
- avatarView.layer.cornerRadius = 12
- avatarView.layer.masksToBounds = true
- avatarView.contentMode = .scaleAspectFill
- phoneLabel.font = .systemFont(ofSize: 16, weight: .semibold)
- phoneLabel.textColor = UIColor(hex: "#2B2B2B")
- idLabel.font = .systemFont(ofSize: 12)
- idLabel.textColor = UIColor(hex: "#9B9B9B")
- profileChevron.tintColor = UIColor(hex: "#5B4227")
- profileChevron.setContentCompressionResistancePriority(.required, for: .horizontal)
- [avatarView, phoneLabel, idLabel, profileChevron].forEach {
- $0.translatesAutoresizingMaskIntoConstraints = false
- profileCard.addSubview($0)
- }
- NSLayoutConstraint.activate([
- avatarView.leadingAnchor.constraint(equalTo: profileCard.leadingAnchor, constant: 16),
- avatarView.centerYAnchor.constraint(equalTo: profileCard.centerYAnchor),
- avatarView.widthAnchor.constraint(equalToConstant: 56),
- avatarView.heightAnchor.constraint(equalToConstant: 56),
- profileChevron.trailingAnchor.constraint(equalTo: profileCard.trailingAnchor, constant: -12),
- profileChevron.centerYAnchor.constraint(equalTo: profileCard.centerYAnchor),
- profileChevron.widthAnchor.constraint(equalToConstant: 16),
- profileChevron.heightAnchor.constraint(equalToConstant: 16),
- phoneLabel.leadingAnchor.constraint(equalTo: avatarView.trailingAnchor, constant: 12),
- phoneLabel.trailingAnchor.constraint(lessThanOrEqualTo: profileChevron.leadingAnchor, constant: -8),
- phoneLabel.bottomAnchor.constraint(equalTo: profileCard.centerYAnchor, constant: -2),
- idLabel.leadingAnchor.constraint(equalTo: phoneLabel.leadingAnchor),
- idLabel.topAnchor.constraint(equalTo: profileCard.centerYAnchor, constant: 2),
- idLabel.trailingAnchor.constraint(lessThanOrEqualTo: profileChevron.leadingAnchor, constant: -8)
- ])
- // 添加广告容器
- view.addSubview(bannerContainer)
- view.addSubview(nativeAdContainer)
-
- // 三个功能 row
- let stack = UIStackView(arrangedSubviews: [rateRow, peteRow, feedbackRow, aboutRow])
- stack.axis = .vertical
- stack.spacing = 12
- stack.alignment = .fill
- view.addSubview(stack)
- stack.translatesAutoresizingMaskIntoConstraints = false
-
- // 设置广告容器约束
- bannerHeightConstraint = bannerContainer.heightAnchor.constraint(equalToConstant: 0)
- bannerHeightConstraint?.isActive = true
-
- nativeAdHeightConstraint = nativeAdContainer.heightAnchor.constraint(equalToConstant: 0)
- nativeAdHeightConstraint?.isActive = true
-
- NSLayoutConstraint.activate([
- // 横幅广告容器(位于用户卡片下方)
- bannerContainer.topAnchor.constraint(equalTo: profileCard.bottomAnchor, constant: 8),
- bannerContainer.leadingAnchor.constraint(equalTo: view.leadingAnchor),
- bannerContainer.trailingAnchor.constraint(equalTo: view.trailingAnchor),
-
- // 功能按钮栈(位于横幅广告下方)
- stack.topAnchor.constraint(equalTo: bannerContainer.bottomAnchor, constant: 16),
- stack.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16),
- stack.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16),
- stack.bottomAnchor.constraint(lessThanOrEqualTo: nativeAdContainer.topAnchor, constant: -16),
-
- // 原生信息流广告容器(位于页面底部)
- nativeAdContainer.leadingAnchor.constraint(equalTo: view.leadingAnchor),
- nativeAdContainer.trailingAnchor.constraint(equalTo: view.trailingAnchor),
- nativeAdContainer.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor)
- ])
- }
-
- private func setupBackground() {
- view.insertSubview(bgImageView, at: 0)
- bgImageView.translatesAutoresizingMaskIntoConstraints = false
- NSLayoutConstraint.activate([
- bgImageView.topAnchor.constraint(equalTo: view.topAnchor),
- bgImageView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
- bgImageView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
- bgImageView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
- ])
- }
-
- @objc private func tapCancel() { navigationController?.popViewController(animated: true)
- }
- private func makeRow(title: String, action: Selector) -> UIControl {
- let container = UIControl()
- container.backgroundColor = .white
- container.layer.cornerRadius = 14
- container.layer.shadowColor = UIColor.black.cgColor
- container.layer.shadowOpacity = 0.08
- container.layer.shadowRadius = 10
- container.layer.shadowOffset = CGSize(width: 0, height: 4)
- container.heightAnchor.constraint(equalToConstant: 64).isActive = true
- let label = UILabel()
- label.text = title
- label.font = .systemFont(ofSize: 16)
- label.textColor = UIColor(hex: "#2B2B2B")
- let chevron = UIImageView(image: UIImage(systemName: "chevron.right"))
- chevron.tintColor = UIColor(hex: "#5B4227")
- chevron.setContentCompressionResistancePriority(.required, for: .horizontal)
- container.addSubview(label)
- container.addSubview(chevron)
- label.translatesAutoresizingMaskIntoConstraints = false
- chevron.translatesAutoresizingMaskIntoConstraints = false
- NSLayoutConstraint.activate([
- label.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 16),
- label.centerYAnchor.constraint(equalTo: container.centerYAnchor),
- chevron.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: -16),
- chevron.centerYAnchor.constraint(equalTo: container.centerYAnchor),
- chevron.widthAnchor.constraint(equalToConstant: 16),
- chevron.heightAnchor.constraint(equalToConstant: 16)
- ])
- container.addTarget(self, action: action, for: .touchUpInside)
- return container
- }
- private func fillUserInfo() {
- // 显示手机号(打码)与 ID
- let phone = UserDefaults.standard.string(forKey: "memberPhone") ?? "登录"
- let uid = UserDefaults.standard.string(forKey: "userId") ?? "--"
- phoneLabel.text = maskedPhone(phone)
- idLabel.text = "ID:\(uid)"
- if let urlStr = UserDefaults.standard.string(forKey: "memberIcon"),
- let url = URL(string: urlStr) {
- loadImage(from: url) { [weak self] img in
- self?.avatarView.image = img ?? UIImage(named: "Home372")
- }
- } else {
- avatarView.image = UIImage(named: "Home372")
- }
- }
- private func maskedPhone(_ s: String) -> String {
- let digits = s.filter { $0.isNumber }
- guard digits.count >= 7 else { return s }
- let start = digits.prefix(3)
- let end = digits.suffix(4)
- return "\(start)****\(end)"
- }
- private func loadImage(from url: URL, completion: @escaping (UIImage?) -> Void) {
- URLSession.shared.dataTask(with: url) { data, _, _ in
- guard let data = data, let img = UIImage(data: data) else {
- DispatchQueue.main.async { completion(nil) }
- return
- }
- DispatchQueue.main.async { completion(img) }
- }.resume()
- }
- // MARK: - Actions
- @objc private func tapProfile() {
- // 检查登录状态
- let isLoggedIn = UserDefaults.standard.bool(forKey: "isLogggedIn")
- let userToken = UserDefaults.standard.string(forKey: "userToken")
-
- // 如果没有登录状态或者没有有效的token,弹出登录界面
- if !isLoggedIn || userToken?.isEmpty != false {
- showLoginViewController()
- return
- }
-
- // 已登录,跳转到个人信息页
- let userSettingsVC = UserSettingsViewController()
- userSettingsVC.hidesBottomBarWhenPushed = true
- navigationController?.pushViewController(userSettingsVC, animated: true)
- }
-
- private func showLoginViewController() {
- let loginVC = LoginViewController()
- let navController = UINavigationController(rootViewController: loginVC)
- navController.modalPresentationStyle = .fullScreen
- present(navController, animated: true)
- }
- @objc private func tapRate() {
- // 跳 App Store 评分(占位)
- SKStoreReviewController.requestReview()
- }
-
- @objc private func tapPet() {
-
- // 检查登录状态
- let isLoggedIn = UserDefaults.standard.bool(forKey: "isLogggedIn")
- let userToken = UserDefaults.standard.string(forKey: "userToken")
-
- // 如果没有登录状态或者没有有效的token,弹出登录界面
- if !isLoggedIn || userToken?.isEmpty != false {
- showLoginViewController()
- return
- }
- //进入我的宠物界面
- let vc = MyPetsViewController()
- vc.hidesBottomBarWhenPushed = true
- navigationController?.pushViewController(vc, animated: true)
- }
- @objc private func tapFeedback() {
- let vc = FeedbackViewController()
- vc.hidesBottomBarWhenPushed = true
- navigationController?.pushViewController(vc, animated: true)
- }
- @objc private func tapAbout() {
- let vc = AboutViewController()
- vc.hidesBottomBarWhenPushed = true
- navigationController?.pushViewController(vc, animated: true)
- }
-
- // MARK: - 广告方法实现
-
- // MARK: - 横幅广告方法
- private func loadBannerAd() {
- var extra: [AnyHashable: Any] = [:]
- extra[kATAdLoadingExtraBannerAdSizeKey] = NSValue(cgSize: bannerSize)
- extra[kATAdLoadingExtraMediaExtraKey] = "SettingsVC"
- ATAdManager.shared().loadAD(withPlacementID: bannerPlacementID, extra: extra, delegate: self)
- }
- private func showBannerIfReady() {
- let config = ATShowConfig(scene: nil, showCustomExt: nil)
- guard let bView = ATAdManager.shared().retrieveBannerView(forPlacementID: bannerPlacementID, config: config) else { return }
- bView.delegate = self
- bView.presentingViewController = self
- bView.translatesAutoresizingMaskIntoConstraints = false
- bannerContainer.addSubview(bView)
- NSLayoutConstraint.activate([
- bView.centerXAnchor.constraint(equalTo: bannerContainer.centerXAnchor),
- bView.topAnchor.constraint(equalTo: bannerContainer.topAnchor),
- bView.widthAnchor.constraint(equalToConstant: bannerSize.width),
- bView.heightAnchor.constraint(equalToConstant: bannerSize.height)
- ])
- bannerView = bView
- bannerHeightConstraint?.constant = bannerSize.height
- UIView.animate(withDuration: 0.25) { self.view.layoutIfNeeded() }
- }
- private func removeBanner() {
- bannerView?.destroyBanner()
- bannerView?.removeFromSuperview()
- bannerView = nil
- bannerHeightConstraint?.constant = 0
- UIView.animate(withDuration: 0.25) { self.view.layoutIfNeeded() }
- }
-
- // MARK: - 插屏广告方法
- private func loadInterstitialAd() {
- let screenWidth = UIScreen.main.bounds.width
- let size = CGSize(width: screenWidth - 30.0, height: 300.0)
- let extra: [AnyHashable: Any] = [
- kATAdLoadingExtraMediaExtraKey: "SettingsView",
- kATInterstitialExtraAdSizeKey: NSValue(cgSize: size)
- ]
- ATAdManager.shared().loadAD(withPlacementID: interstitialPlacementID, extra: extra, delegate: self)
- }
-
- private func showInterstitialAd() {
- guard ATAdManager.shared().interstitialReady(forPlacementID: interstitialPlacementID) else {
- print("插屏广告尚未加载完成")
- return
- }
- ATAdManager.shared().showInterstitial(withPlacementID: interstitialPlacementID, in: self, delegate: self)
- }
-
- // MARK: - 原生信息流广告方法
- private func loadNativeAd() {
- let extra: [AnyHashable: Any] = [
- kATAdLoadingExtraMediaExtraKey: "SettingsVC",
- kATExtraInfoNativeAdSizeKey: NSValue(cgSize: CGSize(width: UIScreen.main.bounds.width - 24, height: 170))
- ]
- ATAdManager.shared().loadAD(withPlacementID: nativeRenderPlacementID, extra: extra, delegate: self)
- }
- private func showNativeAdIfReady() {
- // 检查广告是否准备好
- guard ATAdManager.shared().nativeAdReady(forPlacementID: nativeRenderPlacementID) else {
- print("Native ad not ready")
- return
- }
-
- // 获取广告offer
- guard let offer = ATAdManager.shared().getNativeAdOffer(withPlacementID: nativeRenderPlacementID) else {
- print("Failed to get native ad offer")
- return
- }
-
- self.nativeAdOffer = offer
-
- // 创建自渲染视图
- let selfRenderView = RenderUnitView(offer: offer)
- self.selfRenderView = selfRenderView
-
- // 初始化配置
- let config = ATNativeADConfiguration()
- config.adFrame = CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 170)
- config.mediaViewFrame = CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 150)
- config.delegate = self
- config.rootViewController = self
- config.sizeToFit = true
- config.videoPlayType = .alwaysAutoPlayType
-
- // 设置logo位置
- config.logoViewFrame = CGRect(x: UIScreen.main.bounds.width - 48, y: 140, width: 36, height: 20)
-
- // 创建原生广告视图
- let nativeAdView = ATNativeADView(configuration: config, currentOffer: offer, placementID: nativeRenderPlacementID)
- self.nativeAdView = nativeAdView
-
- // 获取mediaView
- if let mediaView = nativeAdView.getMediaView() {
- selfRenderView.installMediaView(mediaView)
- }
-
- // 创建可点击组件数组
- var clickableViewArray: [UIView] = []
- clickableViewArray.append(contentsOf: [
- selfRenderView.headlineLabel,
- selfRenderView.bodyLabel,
- selfRenderView.heroImageView
- ])
-
- // 注册点击事件
- nativeAdView.registerClickableViewArray(clickableViewArray)
-
- // 绑定组件
- let info = ATNativePrepareInfo.load { prepareInfo in
- prepareInfo.textLabel = selfRenderView.bodyLabel
- prepareInfo.titleLabel = selfRenderView.headlineLabel
- prepareInfo.mainImageView = selfRenderView.heroImageView
- prepareInfo.dislikeButton = selfRenderView.closeControl
- if let mv = selfRenderView.mediaContainer {
- prepareInfo.mediaView = mv
- }
- }
- nativeAdView.prepare(with: info)
-
- // 渲染广告
- offer.renderer(with: config, selfRenderView: selfRenderView, nativeADView: nativeAdView)
-
- // 添加到容器(必须把 nativeAdView 放到视图层级中,点击与视频追踪才会生效)
- nativeAdContainer.subviews.forEach { $0.removeFromSuperview() }
- nativeAdContainer.addSubview(nativeAdView)
- nativeAdView.translatesAutoresizingMaskIntoConstraints = false
- NSLayoutConstraint.activate([
- nativeAdView.leadingAnchor.constraint(equalTo: nativeAdContainer.leadingAnchor),
- nativeAdView.trailingAnchor.constraint(equalTo: nativeAdContainer.trailingAnchor),
- nativeAdView.topAnchor.constraint(equalTo: nativeAdContainer.topAnchor),
- nativeAdView.bottomAnchor.constraint(equalTo: nativeAdContainer.bottomAnchor)
- ])
- // 自渲染视图放入 nativeAdView 内
- nativeAdView.addSubview(selfRenderView)
- selfRenderView.translatesAutoresizingMaskIntoConstraints = false
- NSLayoutConstraint.activate([
- selfRenderView.leadingAnchor.constraint(equalTo: nativeAdView.leadingAnchor),
- selfRenderView.trailingAnchor.constraint(equalTo: nativeAdView.trailingAnchor),
- selfRenderView.topAnchor.constraint(equalTo: nativeAdView.topAnchor),
- selfRenderView.bottomAnchor.constraint(equalTo: nativeAdView.bottomAnchor)
- ])
-
- // 设置容器高度约束
- nativeAdHeightConstraint?.constant = 170
- UIView.animate(withDuration: 0.25) { self.view.layoutIfNeeded() }
-
- print("Native self-render ad displayed successfully")
- }
-
- private func removeNativeAd() {
- // 清理自渲染视图
- selfRenderView?.teardown()
- selfRenderView?.removeFromSuperview()
- selfRenderView = nil
-
- // 清理原生广告视图
- nativeAdView?.removeFromSuperview()
- nativeAdView?.destroyNative()
- nativeAdView = nil
-
- // 清理offer
- nativeAdOffer = nil
-
- nativeAdHeightConstraint?.constant = 0
- UIView.animate(withDuration: 0.25) { self.view.layoutIfNeeded() }
- }
-
- // MARK: - ATAdLoadingDelegate
- func didFinishLoadingAD(withPlacementID placementID: String) {
- if placementID == bannerPlacementID {
- if ATAdManager.shared().bannerAdReady(forPlacementID: placementID) {
- showBannerIfReady()
- }
- print("Banner Ad Loaded: \(placementID)")
- } else if placementID == interstitialPlacementID {
- if ATAdManager.shared().interstitialReady(forPlacementID: placementID) {
- showInterstitialAd()
- }
- print("Interstitial Ad Loaded: \(placementID)")
- } else if placementID == nativeRenderPlacementID {
- if ATAdManager.shared().nativeAdReady(forPlacementID: placementID) {
- showNativeAdIfReady()
- }
- print("Native Ad Loaded: \(placementID)")
- }
- }
- func didFailToLoadAD(withPlacementID placementID: String, error: Error) {
- print("Ad load failed(\(placementID)): \(error)")
- }
- func didRevenue(forPlacementID placementID: String, extra: [AnyHashable : Any]!) {
- print("Ad revenue: placement=\(placementID), extra=\(extra ?? [:])")
- uploadAdRevenue(placementID: placementID, extra: extra ?? [:])
- }
- // MARK: - ATBannerDelegate
- func bannerView(_ bannerView: ATBannerView, didTapCloseButtonWithPlacementID placementID: String, extra: [AnyHashable : Any]!) {
- removeBanner()
- }
- func bannerView(_ bannerView: ATBannerView, didShowAdWithPlacementID placementID: String, extra: [AnyHashable : Any]!) {
- print("Banner did show: \(placementID)")
- adStartTimes[placementID] = nowMillis()
- incrementTodayAdCount()
- }
- func bannerView(_ bannerView: ATBannerView, didClickWithPlacementID placementID: String, extra: [AnyHashable : Any]!) {
- print("Banner did click: \(placementID)")
- }
- func bannerView(_ bannerView: ATBannerView, didAutoRefreshWithPlacement placementID: String, extra: [AnyHashable : Any]!) {
- print("Banner auto refresh: \(placementID)")
- }
- func bannerView(_ bannerView: ATBannerView, failedToAutoRefreshWithPlacementID placementID: String, error: Error) {
- print("Banner auto refresh failed: \(placementID) error=\(error)")
- }
- func bannerView(_ bannerView: ATBannerView, didDeepLinkOrJumpForPlacementID placementID: String, extra: [AnyHashable : Any]!, result success: Bool) {
- print("Banner deeplink/jump: \(placementID) success=\(success)")
- }
-
- // MARK: - ATInterstitialDelegate
- func interstitialDidShow(forPlacementID placementID: String, extra: [AnyHashable : Any]) {
- print("插屏广告展示完成:\(placementID) \(extra)")
- adStartTimes[placementID] = nowMillis()
- incrementTodayAdCount()
- }
-
- func interstitialDidClick(forPlacementID placementID: String, extra: [AnyHashable : Any]) {
- print("插屏广告点击:\(placementID)")
- }
-
- func interstitialDidClose(forPlacementID placementID: String, extra: [AnyHashable : Any]) {
- print("插屏广告关闭:\(placementID) \(extra)")
- }
-
- // MARK: - ATNativeADDelegate
- func didShowNativeAd(in adView: ATNativeADView, placementID: String, extra: [AnyHashable: Any]?) {
- print("Native ad shown for \(placementID)")
- adStartTimes[placementID] = nowMillis()
- incrementTodayAdCount()
- }
- func didTapCloseButton(in adView: ATNativeADView, placementID: String, extra: [AnyHashable: Any]?) {
- print("Native ad closed for \(placementID)")
- removeNativeAd()
- }
- func didClickNativeAd(in adView: ATNativeADView, placementID: String, extra: [AnyHashable: Any]?) {
- print("Native ad clicked for \(placementID)")
- }
-
- // MARK: - 广告收益追踪
- private func uploadAdRevenue(placementID: String, extra: [AnyHashable: Any]) {
- // 计算 begin / finish(毫秒)并格式化为 "yyyy-MM-dd HH:mm:ss"
- let beginMs = adStartTimes[placementID] ?? nowMillis()
- let finishMs = nowMillis()
- print("testaa \(beginMs) \(String(describing: adStartTimes[placementID])) \(finishMs) \(placementID)")
- adStartTimes.removeValue(forKey: placementID) // 避免复用旧开始时间
- 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
- 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
- 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()
- }
- }
|