// // SPRewardedAdManager.swift // ThimraTV // // Created by 长沙佳儿 on 2025/7/10. // import UIKit @objc protocol SPRewardedAdManagerDelegate: NSObjectProtocol { ///发放奖励 @objc optional func rewardedAdManager(manager: SPRewardedAdManager, userDidEarnReward adInfo: SPAdInfo) ///广告加载失败 @objc optional func rewardedAdManager(manager: SPRewardedAdManager, didLoadFail error: Error) ///广告加载成功 @objc optional func rewardedAdManagerDidLoadFinish(manager: SPRewardedAdManager) ///广告展示失败 @objc optional func rewardedAdManager(manager: SPRewardedAdManager, didDisplayFail error: Error) ///广告被展示 @objc optional func rewardedAdManagerDidShow(manager: SPRewardedAdManager) ///广告被关闭 @objc optional func rewardedAdManager(manager: SPRewardedAdManager, didDismiss adInfo: SPAdInfo) } class SPRewardedAdManager: NSObject { static let manager = SPRewardedAdManager() weak var delegate: SPRewardedAdManagerDelegate? private(set) var isLoadingRewardedAd = false ///重试等待时长 private var retryAttempt = 0.0 ///重试次数 private var retryCount = 0 ///最大重试次数 private let retryMaxCount = 1 ///广告激活状态 private(set) var isEnable = true private(set) var adInfo: SPAdInfo? private(set) var isShowLoading = false private(set) var isShowToast = false private var adsDate: Date? ///统计场景 var statScene: SPStatAdModel.AdScene? var videoInfo: SPVideoInfoModel? deinit { NotificationCenter.default.removeObserver(self) } override init() { super.init() NotificationCenter.default.addObserver(self, selector: #selector(didEnterBackgroundNotification), name: UIApplication.didEnterBackgroundNotification, object: nil) } func isAdAvailable() -> Bool { guard let adInfo = adInfo else { return false } if adInfo.platform_key == .google { return admob_isReady() } else if adInfo.platform_key == .applovin { return appLovin_isReady() } return false } ///加载并展示激励广告 func loadAndShowRewardedAd(isShowLoading: Bool = false, isShowToast: Bool = true) { guard isEnable else { let text = "movia_no_ads_tip".localized if isShowToast { self.showToast(text: text) } self.isShowToast = false self.isShowLoading = false let error = NSError(domain: text.localized, code: -1) loadFailHandler(isStat: false, error: error) return } self.isShowLoading = isShowLoading self.isShowToast = isShowToast if self.isShowLoading { SPHUD.show() } guard !isLoadingRewardedAd else { return } if !isAdAvailable() { self.isLoadingRewardedAd = true } self.requestAdInfo { [weak self] adInfo in guard let self = self else { return } if adInfo.platform_key == .google { self.admob_loadAndShowRewardedAd(adInfo: adInfo) } else if adInfo.platform_key == .applovin { self.appLovin_loadAndShowRewardedAd(adInfo: adInfo) } else { self.isLoadingRewardedAd = false self.adInfo = nil } } } ///预加载一个广告 func preloadRewardedAd() { guard isEnable else { return } guard !isLoadingRewardedAd else { return } isShowLoading = false isShowToast = false self.isLoadingRewardedAd = true self.requestAdInfo { [weak self] adInfo in guard let self = self else { return } if adInfo.platform_key == .google { self.admob_loadRewardedAd(adInfo: adInfo) } else if adInfo.platform_key == .applovin { self.appLovin_loadRewardedAd(adInfo: adInfo) } else { self.isLoadingRewardedAd = false self.adInfo = nil } } } ///重试加载广告 private func retryLoadAd() { guard isEnable else { return } guard retryCount < retryMaxCount else { retryCount = 0 retryAttempt = 0 return } retryCount += 1 retryAttempt += 1 let delaySec = pow(2.0, min(6.0, retryAttempt)) DispatchQueue.main.asyncAfter(deadline: .now() + delaySec) { [weak self] in self?.preloadRewardedAd() } } private func clean() { self.statScene = nil self.videoInfo = nil } } //MARK: -------------- 广告内部调用(事件回调) -------------- extension SPRewardedAdManager { ///奖励发放回调 func userDidEarnRewardHandler() { guard let adInfo = adInfo else { return } self.delegate?.rewardedAdManager?(manager: self, userDidEarnReward: adInfo) self.requestStatAd(type: "reward", errorMsg: nil) } ///广告加载失败 func loadFailHandler(isStat: Bool = true, error: Error) { if isShowLoading { SPHUD.dismiss() } if isShowToast { self.showToast() } self.isLoadingRewardedAd = false self.delegate?.rewardedAdManager?(manager: self, didLoadFail: error) if isStat { self.requestStatAd(type: "load_failed", errorMsg: error.localizedDescription) } self.clean() if self.adInfo?.platform_key != .google {//谷歌广告兜底 self.isLoadingRewardedAd = true let adInfo = SPAdInfo() adInfo.platform_key = .google adInfo.ads_id = SPAdManager.manager.admob_rewardedAdUnitID self.adInfo = adInfo self.admob_loadRewardedAd(adInfo: adInfo) } else { self.isEnable = false self.retryLoadAd() } } ///广告加载成功 func loadFinishHandler() { self.retryAttempt = 0 self.retryCount = 0 self.isLoadingRewardedAd = false self.delegate?.rewardedAdManagerDidLoadFinish?(manager: self) } ///广告展示失败 func displayFailHandler(error: Error) { if isShowLoading { SPHUD.dismiss() } if isShowToast { self.showToast() } self.delegate?.rewardedAdManager?(manager: self, didDisplayFail: error) self.requestStatAd(type: "show_failed", errorMsg: error.localizedDescription) self.clean() } ///广告被展示 func didShowHandler() { self.adsDate = Date() if isShowLoading { SPHUD.dismiss() } self.delegate?.rewardedAdManagerDidShow?(manager: self) self.requestStatAd(type: "start", errorMsg: nil) } ///广告被关闭 func didDismissHandler() { let adInfo = self.adInfo var seconds = 0 if let adsDate = self.adsDate { seconds = Int(Date().timeIntervalSince(adsDate)) } self.requestStatAd(type: "close", seconds: seconds, errorMsg: nil) { [weak self] in guard let self = self else { return } if let adInfo = adInfo { self.delegate?.rewardedAdManager?(manager: self, didDismiss: adInfo) } } self.clean() self.preloadRewardedAd() } ///广告被点击 func didClickHandler() { var seconds = 0 if let adsDate = self.adsDate { seconds = Int(Date().timeIntervalSince(adsDate)) } self.requestStatAd(type: "click", seconds: seconds, errorMsg: nil) } } extension SPRewardedAdManager { private func requestStatAd(type: String, seconds: Int = 0, errorMsg: String?, completer: (() -> Void)? = nil) { guard let adInfo = adInfo else { return } let model = SPStatAdModel() model.type = type model.ads_id = adInfo.ads_id model.view_seconds = seconds model.ad_platform_key = adInfo.platform_key model.error_msg = errorMsg model.scene = self.statScene model.short_play_id = self.videoInfo?.short_play_id model.short_play_video_id = self.videoInfo?.short_play_video_id SPStatAPI.requestStatAd(model: model, completer: completer) } @objc private func didEnterBackgroundNotification() { if self.statScene == nil { return } self.requestStatAd(type: "Interrupt", seconds: 0, errorMsg: nil) } func showToast(text: String? = "movia_no_ads_tip".localized) { SPToast.show(text: text) } private func requestAdInfo(completer: ((_ adInfo: SPAdInfo) -> Void)?) { if let adInfo = self.adInfo { completer?(adInfo) return } SPAdAPI.requestShowAdInfo { [weak self] adInfo in guard let self = self else { return } if let adInfo = adInfo { self.adInfo = adInfo } else { self.adInfo = SPAdInfo() self.adInfo?.platform_key = .google self.adInfo?.ads_id = SPAdManager.manager.admob_rewardedAdUnitID } completer?(self.adInfo!) } } }