| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324 |
- //
- // QuestionViewController.swift
- // RoderickRalph
- //
- // Created by Neoa on 2025/8/19.
- //
- import Foundation
- import UIKit
- import AnyThinkBanner
- import AnyThinkSDK
- import AnyThinkInterstitial
- import AnyThinkNative
- import AnyThinkRewardedVideo
- class QuizStageController: UIViewController, ATAdLoadingDelegate, ATBannerDelegate, ATInterstitialDelegate, ATNativeADDelegate, ATRewardedVideoDelegate, EnergyPanelAgent, OverlayPanelAgent{
-
- // MARK: - UI Components
-
- // 实际使用的广告placement ID(从接口获取或使用默认值)
- private var bannerPlacementID: String = "b68d88cc2ab33d"
- private var interstitialPlacementID: String = "b68d88cc36bca8"
- private var nativeRenderPlacementID: String = "b68db6c2fdfcac"
- private var rewardPlacementID: String = "b68d88cc20bb14"
- private var nativeAdView: ATNativeADView?
- private var selfRenderView: RenderUnitView?
- private var nativeAdOffer: ATNativeAdOffer?
- private var nativeAdContainer: UIView = {
- let view = UIView()
- view.translatesAutoresizingMaskIntoConstraints = false
- view.backgroundColor = .clear
- view.isUserInteractionEnabled = true
- return view
- }()
- private var bannerView: ATBannerView?
- private var bannerContainer: UIView = {
- let container = UIView()
- container.translatesAutoresizingMaskIntoConstraints = false
- container.backgroundColor = .clear
- container.isUserInteractionEnabled = true
- container.clipsToBounds = false // 确保子视图不会被裁剪
- return container
- }()
- private var bannerHeightConstraint: NSLayoutConstraint?
- 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)
- }
- // MARK: - Channel API (moved to LoginQuestionViewController)
- private let getDefaultConfig = "/wx/getByDitchId"
- private let getQuestionListPath = "/question/list"
- private let addPowerPath = "/wx/addPower"
- private let answerQuestionPath = "/question/answerQuestion"
- private let adSaveRecordPath = "/ad/saveRecord"
- // MARK: - Get Power Countdown
- private var powerCountdownOverlay: UIView?
- private var powerCountdownLabel: UILabel?
- private var powerCountdownTimer: Timer?
- private var powerRemainingSeconds: Int = 60
- private var didStartPowerCountdown = false
- // Remote-configured values with sensible defaults
- private var staminaWaitSec: Int = 60
- private var interIntervalSec: Int = 30
- private var interstitialTimer: Timer?
- private var nativeIntervalSec: Int = 30
- private var nativeTimer: Timer?
- private var isAllowNativeAutoRefresh: Bool = true
-
- // 1:不可叠加体力;0:可叠加体力
- private var canStackPower: Int = 0
-
- private var accountInfo: LoginUserInfo?
- private var selectedChannelRef: Channel?
- // Reward flow gating for cooldown
- private var rewardPowerOK: Bool = false
- private var rewardVideoClosed: Bool = false
- private var pendingToastText: String?
-
- // MARK: - Question Data State
- private struct QAItem { let id: String; let content: String }
- private struct QAQuestion { let id: String; let content: String; let correctItemId: String; let items: [QAItem] }
- private var questions: [QAQuestion] = []
- private var currentQuestionIndex: Int = 0
- private var option1ItemId: String?
- private var option2ItemId: String?
- private var questionStartAt: Date?
- private var lastQuestionIdFromLogin: String?
- private var isAnswering: Bool = false // 防止重复答题标志
-
- private let userInfoBGView: UIView = {
- let view = UIImageView()
- view.contentMode = .scaleToFill
- view.image = UIImage(named: "res_ogoxcw01")
- view.translatesAutoresizingMaskIntoConstraints = false
- return view
- }()
-
- private let iconImageView: UIImageView = {
- let iv = UIImageView()
- iv.image = UIImage(named: "catlogos")
- iv.contentMode = .scaleAspectFit
- iv.layer.cornerRadius = 29
- iv.clipsToBounds = true
- iv.translatesAutoresizingMaskIntoConstraints = false
- return iv
- }()
-
- private let userInfoLabel: UILabel = {
- let label = UILabel()
- label.text = ""
- label.font = UIFont.boldSystemFont(ofSize: 10)
- label.textColor = .white
-
- // 添加橘色的文字描边
- let strokeTextAttributes: [NSAttributedString.Key: Any] = [
- .strokeColor: UIColor.orange, // 设置描边颜色
- .strokeWidth: -2.0, // 设置描边宽度,负值为描边,正值为文字内部留白
- .foregroundColor: UIColor.white // 设置文字颜色
- ]
- let attributedString = NSAttributedString(string: label.text ?? "", attributes: strokeTextAttributes)
- label.attributedText = attributedString
-
- label.translatesAutoresizingMaskIntoConstraints = false
- return label
- }()
-
- private let musiceView: UIImageView = {
- let iv = UIImageView()
- iv.image = UIImage(named: "res_fb2rjej5")
- iv.contentMode = .scaleToFill
- // iv.layer.cornerRadius = 29
- // iv.clipsToBounds = true
- iv.translatesAutoresizingMaskIntoConstraints = false
- return iv
- }()
-
- private let gainPowerBtn: UIButton = {
- let button = UIButton(type: .system)
-
- button.setBackgroundImage(UIImage(named: "res_4sw46mis"), for: .normal)
- button.setTitle("获取体力", for: .normal)
- button.setTitleColor(.white, for: .normal)
- button.contentHorizontalAlignment = .left
- button.contentEdgeInsets = UIEdgeInsets(top: 0, left: 15, bottom: 0, right: 0) // Adjust left padding
-
- button.translatesAutoresizingMaskIntoConstraints = false
- return button
- }()
-
- private let todayCountLabel: UILabel = {
- let label = UILabel()
- label.text = "今日答题: 0题"
- label.font = UIFont.systemFont(ofSize: 13)
- label.textColor = .white
- label.translatesAutoresizingMaskIntoConstraints = false
- return label
- }()
-
- private let historyCountLabel: UILabel = {
- let label = UILabel()
- label.text = "历史答题: 0题"
- label.font = UIFont.systemFont(ofSize: 13)
- label.textColor = .white
- label.translatesAutoresizingMaskIntoConstraints = false
- return label
- }()
- private let verdictBGView: UIImageView = {
- let iv = UIImageView()
- iv.image = UIImage(named: "res_27q0oxly")
- iv.contentMode = .scaleToFill
- iv.translatesAutoresizingMaskIntoConstraints = false
- return iv
- }()
- private let verdictLabel: UILabel = {
- let label = UILabel()
- label.text = "请选择正确答案"
- label.font = UIFont.systemFont(ofSize: 24, weight: .bold)
- label.textAlignment = .center
- label.textColor = .white
- label.translatesAutoresizingMaskIntoConstraints = false
- return label
- }()
- private let boardBGView: UIImageView = {
- let iv = UIImageView()
- iv.image = UIImage(named: "res_b0fsh9ql")
- iv.contentMode = .scaleToFill
- iv.translatesAutoresizingMaskIntoConstraints = false
- return iv
- }()
-
- private let promptLabel: UILabel = {
- let label = UILabel()
- label.text = "《二泉映月》是首用什么乐器独奏的曲子"
- label.font = UIFont.systemFont(ofSize: 18, weight: .bold)
- label.textColor = .black
- label.textAlignment = .center
- label.numberOfLines = 0
- label.translatesAutoresizingMaskIntoConstraints = false
- return label
- }()
-
- private let levelBadge: UIButton = {
- let button = UIButton(type: .system)
-
- button.setBackgroundImage(UIImage(named: "res_fm0lbz7w"), for: .normal)
- button.setTitle("关卡:1", for: .normal)
- button.setTitleColor(.white, for: .normal)
- button.titleLabel?.font = UIFont.systemFont(ofSize: 20)
- button.contentHorizontalAlignment = .center
- button.contentEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) // Adjust left padding
- button.isUserInteractionEnabled = false
- button.translatesAutoresizingMaskIntoConstraints = false
- return button
- }()
-
- private let staminaBadge: UIButton = {
- let button = UIButton(type: .system)
-
- button.setBackgroundImage(UIImage(named: "res_fm0lbz7w"), for: .normal)
- button.setTitle("体力:0", for: .normal)
- button.setTitleColor(.white, for: .normal)
- button.titleLabel?.font = UIFont.systemFont(ofSize: 20)
- button.contentHorizontalAlignment = .center
- button.contentEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
- button.isUserInteractionEnabled = false
- button.translatesAutoresizingMaskIntoConstraints = false
- return button
- }()
-
- private let choiceBtnA: UIButton = {
- let button = UIButton(type: .system)
- button.setBackgroundImage(UIImage(named: "res_o67j6d3t"), for: .normal)
- button.setTitle("二胡", for: .normal)
- button.setTitleColor(.white, for: .normal)
- button.titleLabel?.font = UIFont.boldSystemFont(ofSize: 24)
- button.translatesAutoresizingMaskIntoConstraints = false
- button.addTarget(self, action: #selector(optionButtonTapped(_:)), for: .touchUpInside)
- return button
- }()
-
- private let choiceBtnB: UIButton = {
- let button = UIButton(type: .system)
- button.setBackgroundImage(UIImage(named: "res_o67j6d3t"), for: .normal)
- button.setTitle("钢琴", for: .normal)
- button.setTitleColor(.white, for: .normal)
- button.titleLabel?.font = UIFont.boldSystemFont(ofSize: 24)
- button.translatesAutoresizingMaskIntoConstraints = false
- button.addTarget(self, action: #selector(optionButtonTapped(_:)), for: .touchUpInside)
- return button
- }()
-
- private let markA: UIImageView = {
- let iv = UIImageView()
- iv.image = UIImage(named: "res_cwqxdp40")
- iv.contentMode = .scaleAspectFill
- iv.isHidden = true
- iv.translatesAutoresizingMaskIntoConstraints = false
- return iv
- }()
-
- private let markB: UIImageView = {
- let iv = UIImageView()
- iv.image = UIImage(named: "res_4h4cvu8h")
- iv.contentMode = .scaleToFill
- iv.isHidden = true
- iv.translatesAutoresizingMaskIntoConstraints = false
- return iv
- }()
-
- // MARK: - Public Methods for Data Transfer from LoginQuestionViewController
-
- func setLoginUserInfo(_ userInfo: LoginUserInfo) {
- self.accountInfo = userInfo
-
- // 如果界面已经加载,立即更新UI
- if isViewLoaded {
- updateUIWithUserInfo(userInfo)
- }
- }
-
- func setSelectedChannel(_ channel: Channel) {
- print("SSSS\(channel.id)")
- self.selectedChannelRef = channel
-
- }
-
- private func updateUIWithUserInfo(_ userInfo: LoginUserInfo) {
- // 更新右侧统计
- todayCountLabel.text = "今日答题: \(userInfo.todayAnswerCount)题"
- historyCountLabel.text = "历史答题: \(userInfo.historyAnswerCount)题"
- userInfoLabel.text = userInfo.userId
- setIconImage(with: userInfo.headImgURL)
-
- // 更新体力显示
- let savedPower = UserDefaults.standard.object(forKey: "power") as? Int ?? 0
- staminaBadge.setTitle("体力:\(savedPower)", for: .normal)
- applyPowerToOptionButtons(savedPower)
- }
-
- // MARK: - Lifecycle
- override func viewDidLoad() {
- super.viewDidLoad()
-
- // navigationController?.setNavigationBarHidden(true, animated: false)
- // 先不加载广告,等接口返回配置后再加载
- // 这样可以确保使用正确的placement ID
-
- // Set up gestures for both the icon and the label
- let iconTapGesture = UITapGestureRecognizer(target: self, action: #selector(handleIconTap))
- iconImageView.isUserInteractionEnabled = true
- iconImageView.addGestureRecognizer(iconTapGesture)
-
- let labelTapGesture = UITapGestureRecognizer(target: self, action: #selector(handleLabelTap))
- userInfoLabel.isUserInteractionEnabled = true
- userInfoLabel.addGestureRecognizer(labelTapGesture)
-
- gainPowerBtn.addTarget(self, action: #selector(handleGetPowerButtonTap), for: .touchUpInside)
-
- setupUI()
- setupConstraints()
- // Prefill icon with saved avatar (headImgURL) or placeholder
- if let userInfo = accountInfo {
- // 使用传入的用户信息更新UI
- updateUIWithUserInfo(userInfo)
- } else {
- // 使用本地保存的数据
- let savedHead = UserDefaults.standard.string(forKey: "headImgURL")
- setIconImage(with: savedHead)
-
- // Prefill power and gray out options if needed
- let savedPower = UserDefaults.standard.object(forKey: "power") as? Int ?? 0
- staminaBadge.setTitle("体力:\(savedPower)", for: .normal)
- applyPowerToOptionButtons(savedPower)
- }
- // Channel selection is now handled in LoginQuestionViewController
- // Fetch questions directly since login is complete
- fetchQuestionList()
-
-
- fetchDefaultConfig(channel: self.selectedChannelRef!)
-
- //刷新一次banner
- DispatchQueue.main.asyncAfter(deadline: .now() + 7) { [weak self] in
- guard let self = self else { return }
- self.removeBanner()
- self.loadBannerAd()
- }
- }
-
-
- override func viewWillAppear(_ animated: Bool) {
- super.viewWillAppear(animated)
- navigationController?.setNavigationBarHidden(true, animated: animated)
-
- // 监听应用进入前台的通知
- NotificationCenter.default.addObserver(
- self,
- selector: #selector(appDidEnterForeground),
- name: UIApplication.willEnterForegroundNotification,
- object: nil
- )
- }
- override func viewDidAppear(_ animated: Bool) {
- super.viewDidAppear(animated)
- }
-
- override func viewWillDisappear(_ animated: Bool) {
- super.viewWillDisappear(animated)
-
- // 移除通知监听
- NotificationCenter.default.removeObserver(
- self,
- name: UIApplication.willEnterForegroundNotification,
- object: nil
- )
- }
- private func loadNativeAd() {
- let extra: [AnyHashable: Any] = [
- kATAdLoadingExtraMediaExtraKey: "QuestionVC",
- kATExtraInfoNativeAdSizeKey: NSValue(cgSize: CGSize(width: UIScreen.main.bounds.width - 24, height: 170))
- ]
-
- // Load the native ad with placement ID
- 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.iconImageView,
- selfRenderView.headlineLabel,
- selfRenderView.bodyLabel,
- // selfRenderView.ctaLabel,
- selfRenderView.heroImageView
- ])
-
- // 注册点击事件
- nativeAdView.registerClickableViewArray(clickableViewArray)
-
- // 绑定组件
- let info = ATNativePrepareInfo.load { prepareInfo in
- prepareInfo.textLabel = selfRenderView.bodyLabel
- // prepareInfo.advertiserLabel = selfRenderView.advertiserLabel
- prepareInfo.titleLabel = selfRenderView.headlineLabel
- // prepareInfo.ratingLabel = selfRenderView.ratingLabel
- // prepareInfo.iconImageView = selfRenderView.iconImageView
- prepareInfo.mainImageView = selfRenderView.heroImageView
- // prepareInfo.logoImageView = selfRenderView.logoImageView
- // prepareInfo.ctaLabel = selfRenderView.ctaLabel
- 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)
- ])
-
- print("Native self-render ad displayed successfully")
- }
-
- @objc func loadRewardAd() {
- var loadConfig: [String: Any] = [:]
- // 服务端激励验证透传参数(可选)
- loadConfig[kATAdLoadingExtraMediaExtraKey] = "media_val_RewardedVC"
- loadConfig[kATAdLoadingExtraUserIDKey] = "rv_test_user_id"
- loadConfig[kATAdLoadingExtraRewardNameKey] = "reward_Name"
- loadConfig[kATAdLoadingExtraRewardAmountKey] = 1
- ATAdManager.shared().loadAD(withPlacementID: rewardPlacementID,
- extra: loadConfig,
- delegate: self)
- }
- // MARK: - Show Ad 展示广告
- @objc func showRewardAd() {
- // 就绪检查
- guard ATAdManager.shared().rewardedVideoReady(forPlacementID: rewardPlacementID) else {
- loadRewardAd()
- return
- }
- let config = ATShowConfig(scene: "", showCustomExt: "testShowCustomExt")
- // Reset reward flow state before showing a new video
- rewardPowerOK = false
- rewardVideoClosed = false
- // 注意:不清除pendingToastMessage,因为可能还有待显示的消息
- // 展示
- ATAdManager.shared().showRewardedVideo(withPlacementID: rewardPlacementID,
- config: config,
- in: self,
- delegate: self)
- }
-
- private func loadBannerAd() {
- var extra: [AnyHashable: Any] = [
- kATAdLoadingExtraMediaExtraKey: "QuestionVC"
- ]
- extra[kATAdLoadingExtraBannerAdSizeKey] = NSValue(cgSize: bannerSize)
- ATAdManager.shared().loadAD(withPlacementID: bannerPlacementID, extra: extra, delegate: self)
- }
- private func showBannerIfReady() {
- let config = ATShowConfig(scene: nil, showCustomExt: nil)
- guard let banner = ATAdManager.shared().retrieveBannerView(forPlacementID: bannerPlacementID, config: config) else { return }
-
- banner.delegate = self
- // 确保设置正确的 presentingViewController
- banner.presentingViewController = self
- banner.translatesAutoresizingMaskIntoConstraints = false
-
- bannerContainer.subviews.forEach { $0.removeFromSuperview() }
- bannerContainer.addSubview(banner)
- // Add constraints to the banner view
- NSLayoutConstraint.activate([
- banner.centerXAnchor.constraint(equalTo: bannerContainer.centerXAnchor),
- banner.topAnchor.constraint(equalTo: bannerContainer.topAnchor),
- banner.widthAnchor.constraint(equalToConstant: bannerSize.width),
- banner.heightAnchor.constraint(equalToConstant: bannerSize.height)
- ])
- // Store the banner view
- bannerView = banner
- bannerView?.isUserInteractionEnabled = true
-
- // Update the height constraint for the container
- bannerHeightConstraint?.constant = bannerSize.height
- UIView.animate(withDuration: 0.25) {
- self.view.layoutIfNeeded()
- }
-
- // 打印调试信息
- print("Banner frame: \(banner.frame)")
- print("Banner container frame: \(self.bannerContainer.frame)")
- }
- private func removeBanner() {
- bannerView?.destroyBanner()
- bannerView?.removeFromSuperview()
- bannerView = nil
- // Reset the height constraint to 0
- bannerHeightConstraint?.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)")
- }
- }
-
- // MARK: - ATRewardedVideoDelegate(激励回调)
- // 激励成功(注意:原 ObjC 选择子为 PlacemenID,保持一致以兼容 SDK)
- func rewardedVideoDidRewardSuccess(forPlacemenID placementID: String, extra: [AnyHashable: Any]) {
- print("rewardedVideoDidRewardSuccessForPlacemenID:\(placementID) extra:\(extra)")
- // 激励成功,向服务器申请体力
- addPower()
- }
- // 视频开始播放
- func rewardedVideoDidStartPlaying(forPlacementID placementID: String, extra: [AnyHashable: Any]) {
- print("rewardedVideoDidStartPlayingForPlacementID:\(placementID) extra:\(extra)")
- adStartTimes[placementID] = nowMillis()
- incrementTodayAdCount()
- }
- // 视频播放结束
- func rewardedVideoDidEndPlaying(forPlacementID placementID: String, extra: [AnyHashable: Any]) {
- print("rewardedVideoDidEndPlayingForPlacementID:\(placementID) extra:\(extra)")
- }
- // 视频播放失败(失败后预加载)
- func rewardedVideoDidFailToPlay(forPlacementID placementID: String, error: Error, extra: [AnyHashable: Any]) {
- let code = (error as NSError).code
- print("rewardedVideoDidFailToPlayForPlacementID:\(placementID) error:\(error) extra:\(extra)")
- // 预加载
- loadRewardAd()
- }
- // 广告关闭(关闭后预加载)
- @objc(rewardedVideoDidCloseForPlacementID:rewarded:extra:)
- func rewardedVideoDidClose(forPlacementID placementID: String, rewarded: Bool, extra: [AnyHashable: Any]) {
- print("rewardedVideoDidCloseForPlacementID:\(placementID), rewarded:\(rewarded) extra:\(extra)")
- // 预加载
- loadRewardAd()
-
- // 标记关闭并尝试开启倒计时
- rewardVideoClosed = true
- print("[Toast] Video closed, pending message: \(pendingToastText ?? "nil")")
- startCooldownIfRewardFlowDone()
-
- // 显示待显示的Toast消息
- showPendingToast()
- }
- // 广告点击
- @objc(rewardedVideoDidClickForPlacementID:extra:)
- func rewardedVideoDidClick(forPlacementID placementID: String, extra: [AnyHashable: Any]) {
- print("rewardedVideoDidClickForPlacementID:\(placementID) extra:\(extra)")
- }
- // Deeplink/跳转
- @objc(rewardedVideoDidDeepLinkOrJumpForPlacementID:extra:result:)
- func rewardedVideoDidDeepLinkOrJump(forPlacementID placementID: String, extra: [AnyHashable: Any], result success: Bool) {
- print("rewardedVideoDidDeepLinkOrJumpForPlacementID:\(placementID) extra:\(extra) success:\(success)")
- }
- // MARK: - "再看一个"能力(Again)回调
- func rewardedVideoAgainDidRewardSuccess(forPlacemenID placementID: String, extra: [AnyHashable: Any]) {
- print("rewardedVideoAgainDidRewardSuccessForPlacemenID:\(placementID) extra:\(extra)")
- }
- func rewardedVideoAgainDidStartPlaying(forPlacementID placementID: String, extra: [AnyHashable: Any]) {
- print("rewardedVideoAgainDidStartPlayingForPlacementID:\(placementID) extra:\(extra)")
- }
- func rewardedVideoAgainDidEndPlaying(forPlacementID placementID: String, extra: [AnyHashable: Any]) {
- print("rewardedVideoAgainDidEndPlayingForPlacementID:\(placementID) extra:\(extra)")
- }
- func rewardedVideoAgainDidFailToPlay(forPlacementID placementID: String, error: Error, extra: [AnyHashable: Any]) {
- let code = (error as NSError).code
- print("rewardedVideoAgainDidFailToPlayForPlacementID:\(placementID) error:\(error) extra:\(extra)")
- }
- func rewardedVideoAgainDidClick(forPlacementID placementID: String, extra: [AnyHashable: Any]) {
- print("rewardedVideoAgainDidClickForPlacementID:\(placementID) extra:\(extra)")
- }
-
- // MARK: - ATNativeAdDelegate
- func didShowNativeAd(in adView: ATNativeADView, placementID: String, extra: [AnyHashable: Any]?) {
- print("Native ad shown for \(placementID)")
- adStartTimes[placementID] = nowMillis()
- incrementTodayAdCount()
-
- // 根据配置决定是否开始原生广告自动刷新计时
- if isAllowNativeAutoRefresh {
- startNativeAdRefreshTimer()
- } else {
- print("[Config] Native ad auto refresh is disabled")
- }
- }
- 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)")
- }
-
- func didFailToLoadAD(withPlacementID placementID: String, error: Error) {
- print("AD load failed(\(placementID)): \(error)")
- // 失败不影响主流程,保持 banner 容器高度为 0
-
- // banner广告失败,自动重试加载与展示
- if placementID == bannerPlacementID {
- print("[Banner] Load failed for bannerPlacementID, will retry...")
- // 清理旧视图以避免残留
- removeBanner()
- // 稍作延迟再重试,避免与 SDK 内部节流/并发冲突
- DispatchQueue.main.asyncAfter(deadline: .now() + 2) { [weak self] in
- guard let self = self else { return }
- self.loadBannerAd()
- }
- }
- // 原生自渲染广告失败,自动重试加载与展示
- if placementID == nativeRenderPlacementID {
- print("[Native] Load failed for nativeRenderPlacementID, will retry...")
- // 清理旧视图以避免残留
- removeNativeAd()
- // 稍作延迟再重试,避免与 SDK 内部节流/并发冲突
- DispatchQueue.main.asyncAfter(deadline: .now() + 2) { [weak self] in
- guard let self = self else { return }
- self.loadNativeAd()
- }
- }
- }
- func didRevenue(forPlacementID placementID: String, extra: [AnyHashable : Any]!) {
- // 可选:上报收益日志到我们服务器
- print("Banner 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 Ad shown for \(placementID)")
- //展示广告的时间戳
- adStartTimes[placementID] = nowMillis()
- incrementTodayAdCount()
-
- // 在广告显示时设置正确的 presentingViewController
- bannerView.presentingViewController = self
- }
- func bannerView(_ bannerView: ATBannerView, didClickWithPlacementID placementID: String, extra: [AnyHashable: Any]!) {
- print("Banner Ad clicked for \(placementID)")
-
- }
- func bannerView(_ bannerView: ATBannerView, didAutoRefreshWithPlacement placementID: String, extra: [AnyHashable: Any]!) {
- print("Banner auto-refresh for \(placementID)")
- }
- func bannerView(_ bannerView: ATBannerView, failedToAutoRefreshWithPlacementID placementID: String, error: Error) {
- print("Banner auto-refresh failed for \(placementID): \(error.localizedDescription)")
- }
-
- // 添加 Banner 广告深度链接/跳转回调
- func bannerView(_ bannerView: ATBannerView, didDeepLinkOrJumpForPlacementID placementID: String, extra: [AnyHashable: Any]!, result success: Bool) {
- }
-
- private func removeNativeAd() {
- // 停止原生广告刷新计时器
- stopNativeAdRefreshTimer()
-
- // 清理自渲染视图
- selfRenderView?.teardown()
- selfRenderView?.removeFromSuperview()
- selfRenderView = nil
-
- // 清理原生广告视图
- nativeAdView?.removeFromSuperview()
- nativeAdView?.destroyNative()
- nativeAdView = nil
-
- // 清理offer
- nativeAdOffer = nil
- }
-
- // 加载插屏广告
- private func loadInterstitialAd() {
- // 加载插屏广告(支持半屏尺寸:如快手;可能会影响展示效果)
- let screenWidth = UIScreen.main.bounds.width
- let size = CGSize(width: screenWidth - 30.0, height: 300.0)
- let extra: [AnyHashable: Any] = [
- kATAdLoadingExtraMediaExtraKey: "ProfileView",
- // 设置半屏插屏广告大小
- 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)
- }
-
- 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)")
- }
-
- @objc private func handleIconTap() {
- showSettingsPopup()
- }
- @objc private func handleLabelTap() {
- showSettingsPopup()
- }
-
- private func showSettingsPopup() {
- let settingsPopup = OverlayPanelView()
- settingsPopup.translatesAutoresizingMaskIntoConstraints = false
- settingsPopup.delegate = self // ✅ 设置代理
- view.addSubview(settingsPopup)
-
- // 添加约束使弹窗覆盖整个屏幕
- NSLayoutConstraint.activate([
- settingsPopup.topAnchor.constraint(equalTo: view.topAnchor),
- settingsPopup.bottomAnchor.constraint(equalTo: view.bottomAnchor),
- settingsPopup.leadingAnchor.constraint(equalTo: view.leadingAnchor),
- settingsPopup.trailingAnchor.constraint(equalTo: view.trailingAnchor)
- ])
- }
-
- // MARK: - UI Setup
- private func setupUI() {
- // Set Gradient Background
- let gradientLayer = CAGradientLayer()
- gradientLayer.frame = view.bounds
- gradientLayer.colors = [UIColor(hexString: "#FF6324")!.cgColor, UIColor(hexString: "#FF9946")!.cgColor]
- gradientLayer.startPoint = CGPoint(x: 0.5, y: 0) // top center
- gradientLayer.endPoint = CGPoint(x: 0.5, y: 1) // bottom center
- view.layer.insertSublayer(gradientLayer, at: 0)
- view.addSubview(bannerContainer)
- view.addSubview(userInfoBGView)
- view.addSubview(iconImageView)
- view.addSubview(userInfoLabel)
- view.addSubview(musiceView)
-
- // New UI components for the right side
- view.addSubview(gainPowerBtn)
- view.addSubview(todayCountLabel)
- view.addSubview(historyCountLabel)
- view.addSubview(boardBGView)
- view.addSubview(verdictBGView)
- view.addSubview(verdictLabel)
- view.addSubview(promptLabel)
- view.addSubview(levelBadge)
- view.addSubview(staminaBadge)
-
- view.addSubview(choiceBtnA)
- view.addSubview(choiceBtnB)
- view.addSubview(markA)
- view.addSubview(markB)
- view.addSubview(nativeAdContainer) // Add native ad container
- }
-
- private func setupConstraints() {
-
- // Set up constraints for banner container
- bannerHeightConstraint = bannerContainer.heightAnchor.constraint(equalToConstant: 0)
- bannerHeightConstraint?.isActive = true
- NSLayoutConstraint.activate([
- // Banner container
- bannerContainer.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor,constant: 0),
- bannerContainer.leadingAnchor.constraint(equalTo: view.leadingAnchor),
- bannerContainer.trailingAnchor.constraint(equalTo: view.trailingAnchor),
-
- userInfoBGView.topAnchor.constraint(equalTo: bannerContainer.bottomAnchor,constant: 24),
- userInfoBGView.leadingAnchor.constraint(equalTo: view.leadingAnchor,constant: 24),
- userInfoBGView.widthAnchor.constraint(equalToConstant: 100),
- userInfoBGView.heightAnchor.constraint(equalToConstant: 100),
- iconImageView.centerXAnchor.constraint(equalTo: userInfoBGView.centerXAnchor),
- iconImageView.topAnchor.constraint(equalTo: userInfoBGView.topAnchor,constant: 15),
- iconImageView.widthAnchor.constraint(equalToConstant: 58),
- iconImageView.heightAnchor.constraint(equalToConstant: 58),
- // UID Label
- userInfoLabel.bottomAnchor.constraint(equalTo: userInfoBGView.bottomAnchor, constant: -5),
- userInfoLabel.centerXAnchor.constraint(equalTo: userInfoBGView.centerXAnchor),
-
- musiceView.leadingAnchor.constraint(equalTo: userInfoBGView.trailingAnchor, constant: 14),
- musiceView.centerYAnchor.constraint(equalTo: userInfoLabel.centerYAnchor),
-
- verdictBGView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
- verdictBGView.topAnchor.constraint(equalTo: musiceView.bottomAnchor, constant: 14),
- verdictBGView.widthAnchor.constraint(equalToConstant: 290),
- verdictBGView.heightAnchor.constraint(equalToConstant: 72),
- verdictLabel.centerXAnchor.constraint(equalTo: verdictBGView.centerXAnchor),
- verdictLabel.topAnchor.constraint(equalTo: verdictBGView.topAnchor, constant: 11),
- boardBGView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
- boardBGView.topAnchor.constraint(equalTo: musiceView.bottomAnchor, constant: 46),
- boardBGView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
- boardBGView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
- // Question Label
- promptLabel.centerYAnchor.constraint(equalTo: boardBGView.centerYAnchor),
- promptLabel.centerXAnchor.constraint(equalTo: boardBGView.centerXAnchor),
- promptLabel.leadingAnchor.constraint(equalTo: boardBGView.leadingAnchor,constant: 20),
- promptLabel.trailingAnchor.constraint(equalTo: boardBGView.trailingAnchor,constant: -20),
- levelBadge.bottomAnchor.constraint(equalTo: boardBGView.bottomAnchor, constant: 14),
- levelBadge.centerXAnchor.constraint(equalTo: boardBGView.centerXAnchor, constant: -67.5),
- levelBadge.heightAnchor.constraint(equalToConstant: 41),
- levelBadge.widthAnchor.constraint(equalToConstant: 111),
- staminaBadge.bottomAnchor.constraint(equalTo: boardBGView.bottomAnchor, constant: 14),
- staminaBadge.centerXAnchor.constraint(equalTo: boardBGView.centerXAnchor, constant: 67.5),
- staminaBadge.heightAnchor.constraint(equalToConstant: 41),
- staminaBadge.widthAnchor.constraint(equalToConstant: 111),
-
- // Option Button 1
- choiceBtnA.topAnchor.constraint(equalTo: boardBGView.bottomAnchor, constant: 45),
- choiceBtnA.centerXAnchor.constraint(equalTo: boardBGView.centerXAnchor),
- choiceBtnA.heightAnchor.constraint(equalToConstant: 47),
- choiceBtnA.widthAnchor.constraint(equalToConstant: 211),
- // Option Button 2
- choiceBtnB.topAnchor.constraint(equalTo: choiceBtnA.bottomAnchor, constant: 20),
- choiceBtnB.centerXAnchor.constraint(equalTo: boardBGView.centerXAnchor),
- choiceBtnB.heightAnchor.constraint(equalToConstant: 47),
- choiceBtnB.widthAnchor.constraint(equalToConstant: 211),
-
- markA.leadingAnchor.constraint(equalTo: choiceBtnA.trailingAnchor, constant: 4),
- markA.centerYAnchor.constraint(equalTo: choiceBtnA.centerYAnchor),
- markA.heightAnchor.constraint(equalToConstant: 40),
- markA.widthAnchor.constraint(equalToConstant: 40),
- markB.leadingAnchor.constraint(equalTo: choiceBtnB.trailingAnchor, constant: 4),
- markB.centerYAnchor.constraint(equalTo: choiceBtnB.centerYAnchor),
- markB.heightAnchor.constraint(equalToConstant: 40),
- markB.widthAnchor.constraint(equalToConstant: 40),
- nativeAdContainer.topAnchor.constraint(equalTo: choiceBtnB.bottomAnchor, constant: 10),
- nativeAdContainer.leadingAnchor.constraint(equalTo: view.leadingAnchor),
- nativeAdContainer.trailingAnchor.constraint(equalTo: view.trailingAnchor),
- nativeAdContainer.heightAnchor.constraint(equalToConstant: 170),
- // New UI components: Right-side Labels and Button
-
- gainPowerBtn.topAnchor.constraint(equalTo: userInfoBGView.topAnchor,constant: 10),
- gainPowerBtn.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
- gainPowerBtn.widthAnchor.constraint(equalToConstant: 110), // Adjust width to fit content
- gainPowerBtn.heightAnchor.constraint(equalToConstant: 37),
-
- todayCountLabel.topAnchor.constraint(equalTo: gainPowerBtn.bottomAnchor, constant: 10),
- todayCountLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
-
- historyCountLabel.topAnchor.constraint(equalTo: todayCountLabel.bottomAnchor, constant: 5),
- historyCountLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20)
-
-
- ])
- }
-
- // MARK: - Get Power Button Countdown Logic
- private func startGetPowerCountdownIfNeeded() {
- guard !didStartPowerCountdown else { return }
- didStartPowerCountdown = true
- startGetPowerCountdown()
- }
-
- private func startCooldownIfRewardFlowDone() {
- // Start only when server addPower succeeded AND video has been closed
- guard rewardPowerOK && rewardVideoClosed else { return }
- startGetPowerCountdownIfNeeded()
- // reset flags for next cycle
- rewardPowerOK = false
- rewardVideoClosed = false
- // 注意:不在这里清理pendingToastMessage,让Toast先显示
- }
- private func startGetPowerCountdown() {
- // Disable tapping while counting down
- gainPowerBtn.isEnabled = false
- // Build a gray overlay that blocks touches
- if powerCountdownOverlay == nil {
- let overlay = UIView()
- overlay.translatesAutoresizingMaskIntoConstraints = false
- overlay.backgroundColor = UIColor.black.withAlphaComponent(0.5)
- overlay.isUserInteractionEnabled = true // intercept touches while disabled
- overlay.layer.cornerRadius = 18.5
- overlay.clipsToBounds = true
- let label = UILabel()
- label.translatesAutoresizingMaskIntoConstraints = false
- label.font = UIFont.boldSystemFont(ofSize: 14)
- label.textColor = .white
- label.textAlignment = .center
- overlay.addSubview(label)
- NSLayoutConstraint.activate([
- label.centerXAnchor.constraint(equalTo: overlay.centerXAnchor),
- label.centerYAnchor.constraint(equalTo: overlay.centerYAnchor)
- ])
- gainPowerBtn.addSubview(overlay)
- NSLayoutConstraint.activate([
- overlay.leadingAnchor.constraint(equalTo: gainPowerBtn.leadingAnchor),
- overlay.trailingAnchor.constraint(equalTo: gainPowerBtn.trailingAnchor),
- overlay.topAnchor.constraint(equalTo: gainPowerBtn.topAnchor),
- overlay.bottomAnchor.constraint(equalTo: gainPowerBtn.bottomAnchor)
- ])
- powerCountdownOverlay = overlay
- powerCountdownLabel = label
- }
- powerRemainingSeconds = staminaWaitSec
- powerCountdownLabel?.text = "\(powerRemainingSeconds)s"
- powerCountdownTimer?.invalidate()
- powerCountdownTimer = Timer.scheduledTimer(timeInterval: 1.0,
- target: self,
- selector: #selector(handlePowerCountdownTick),
- userInfo: nil,
- repeats: true)
- if let timer = powerCountdownTimer {
- RunLoop.main.add(timer, forMode: .common)
- }
- }
- @objc private func handlePowerCountdownTick() {
- powerRemainingSeconds -= 1
- if powerRemainingSeconds > 0 {
- powerCountdownLabel?.text = "\(powerRemainingSeconds)s"
- } else {
- powerCountdownTimer?.invalidate()
- powerCountdownTimer = nil
- // Re-enable the button and remove overlay
- gainPowerBtn.isEnabled = true
- powerCountdownOverlay?.removeFromSuperview()
- powerCountdownOverlay = nil
- powerCountdownLabel = nil
- }
- }
- deinit {
- interstitialTimer?.invalidate()
- interstitialTimer = nil
- nativeTimer?.invalidate()
- nativeTimer = nil
- powerCountdownTimer?.invalidate()
- }
- // Keep a reference to the active Power popup (weak to avoid retain cycle)
- private weak var activePowerPopup: EnergyPanelView?
- // System loading overlay
- private var loadingOverlay: UIView?
- private var loadingSpinner: UIActivityIndicatorView?
- // MARK: - Button Actions
- @objc private func optionButtonTapped(_ sender: UIButton) {
- if sender == choiceBtnA || sender == choiceBtnB {
- handleAnswerSelection(selectedButton: sender)
- }
- }
-
- @objc private func handleGetPowerButtonTap() {
- //若已有体力,则提示并返回
- let currentPower = UserDefaults.standard.object(forKey: "power") as? Int ?? 0
- if canStackPower == 1 && currentPower > 0 {
- showToast(message: "已有体力请先答题")
- return
- }
- let powerPopup = EnergyPanelView()
- powerPopup.translatesAutoresizingMaskIntoConstraints = false
- powerPopup.delegate = self
- // 用最新登录信息配置;若没有,则从 UserDefaults 兜底
- if let info = accountInfo {
- powerPopup.configure(with: info)
- self.activePowerPopup = powerPopup
- } else {
- let nick = UserDefaults.standard.string(forKey: "nickname") ?? "XXXX"
- let role = UserDefaults.standard.string(forKey: "roleID") ?? "XXXX"
- let reg = UserDefaults.standard.string(forKey: "registryTimeStr") ?? "--"
- let today = UserDefaults.standard.object(forKey: "todayAnswerCount") as? Int ?? 0
- let history = UserDefaults.standard.object(forKey: "historyAnswerCount") as? Int ?? 0
- let head = UserDefaults.standard.string(forKey: "headImgURL")
- let lastLogin = UserDefaults.standard.string(forKey: "lastLoginTimeStr") ?? "--"
- let info = LoginUserInfo(
- nickName: nick,
- userId: role,
- registryTimeStr: reg,
- todayAnswerCount: today,
- historyAnswerCount: history,
- headImgURL: head,
- lastLoginTimeStr: lastLogin,
- answerLogs: UserDefaults.standard.stringArray(forKey: "answerLogs") ?? []
- )
- powerPopup.configure(with: info)
- self.activePowerPopup = powerPopup
- }
- view.addSubview(powerPopup)
- NSLayoutConstraint.activate([
- powerPopup.topAnchor.constraint(equalTo: view.topAnchor),
- powerPopup.bottomAnchor.constraint(equalTo: view.bottomAnchor),
- powerPopup.leadingAnchor.constraint(equalTo: view.leadingAnchor),
- powerPopup.trailingAnchor.constraint(equalTo: view.trailingAnchor)
- ])
- }
-
- func energyPanelDidTapAcquire(_ panel: EnergyPanelView) {
- // 先把弹窗移除(可按需要保留)
- panel.removeFromSuperview()
- self.activePowerPopup = nil
- // 展示激励广告
- showRewardAd()
- }
- // MARK: - PowerPopupViewDelegate (Close)
- func energyPanelDidDismiss(_ panel: EnergyPanelView) {
- // 关闭体力弹窗后刷新广告
- removeBanner()
- loadBannerAd()
- removeNativeAd()
- loadNativeAd()
- self.activePowerPopup = nil
- }
-
- private func showRealNamePopup() {
- let realNamePopup = IdentityGateView()
- realNamePopup.translatesAutoresizingMaskIntoConstraints = false
- view.addSubview(realNamePopup)
- NSLayoutConstraint.activate([
- realNamePopup.topAnchor.constraint(equalTo: view.topAnchor),
- realNamePopup.bottomAnchor.constraint(equalTo: view.bottomAnchor),
- realNamePopup.leadingAnchor.constraint(equalTo: view.leadingAnchor),
- realNamePopup.trailingAnchor.constraint(equalTo: view.trailingAnchor)
- ])
- }
- // MARK: - System Loading Helpers
- private func showSystemLoading() {
- guard loadingOverlay == nil else { return }
- let overlay = UIView()
- overlay.translatesAutoresizingMaskIntoConstraints = false
- overlay.backgroundColor = UIColor.clear
- overlay.isUserInteractionEnabled = true // block touches
- let spinner = UIActivityIndicatorView(style: .large)
- spinner.translatesAutoresizingMaskIntoConstraints = false
- spinner.startAnimating()
- spinner.hidesWhenStopped = true
- overlay.addSubview(spinner)
- view.addSubview(overlay)
- NSLayoutConstraint.activate([
- overlay.topAnchor.constraint(equalTo: view.topAnchor),
- overlay.bottomAnchor.constraint(equalTo: view.bottomAnchor),
- overlay.leadingAnchor.constraint(equalTo: view.leadingAnchor),
- overlay.trailingAnchor.constraint(equalTo: view.trailingAnchor),
- spinner.centerXAnchor.constraint(equalTo: overlay.centerXAnchor),
- spinner.centerYAnchor.constraint(equalTo: overlay.centerYAnchor)
- ])
- loadingOverlay = overlay
- loadingSpinner = spinner
- }
- private func hideSystemLoading() {
- loadingSpinner?.stopAnimating()
- loadingOverlay?.removeFromSuperview()
- loadingSpinner = nil
- loadingOverlay = nil
- }
- // MARK: - Default Config
- private func fetchDefaultConfig(channel:Channel) {
- guard let url = URL(string: apiBaseURL + getDefaultConfig) else {
- print("[Config] Invalid getDefaultConfig URL")
- return
- }
- var request = URLRequest(url: url)
- request.httpMethod = "POST"
- request.setValue("application/json", forHTTPHeaderField: "Content-Type")
- let body: [String: Any] = ["ditchId": channel.id]
- request.httpBody = try? JSONSerialization.data(withJSONObject: body, options: [])
- if let body = request.httpBody, let bodyString = String(data: body, encoding: .utf8) {
- print("[Config] Request URL: \(url)")
- print("[Config] Request Headers: \(request.allHTTPHeaderFields ?? [:])")
- print("[Config] Request Body: \(bodyString)")
- }
- URLSession.shared.dataTask(with: request) { [weak self] data, response, error in
- if let http = response as? HTTPURLResponse {
- print("[Config] Response Code: \(http.statusCode)")
- print("[Config] Response Headers: \(http.allHeaderFields)")
- }
- if let data = data, let s = String(data: data, encoding: .utf8) {
- print("[Config] Response Body: \(s)")
- }
- if let error = error {
- print("[Config] fetch error: \(error)")
- // 接口失败时,使用默认配置加载广告
- DispatchQueue.main.async { [weak self] in
- self?.reloadAllAdsWithNewConfig()
- self?.startInterstitialScheduleIfNeeded()
- }
- return
- }
- guard let data = data else {
- DispatchQueue.main.async { [weak self] in
- self?.reloadAllAdsWithNewConfig()
- self?.startInterstitialScheduleIfNeeded()
- }
- return
- }
- // Parse JSON: { code, message, data: { powerWaitTime, interstitialIntervalTime, takuBannerPid, takuInterstitialPid, takuNativePid, takuRewardPid, isAllowNativeAutoRefresh } }
- var newPowerWait: Int?
- var newInterstitial: Int?
- var newNativeInterval: Int?
- var newCanAccumulation: Int?
- var newBannerPid: String?
- var newInterstitialPid: String?
- var newNativePid: String?
- var newRewardPid: String?
- var newIsAllowNativeAutoRefresh: Bool?
- if let obj = try? JSONSerialization.jsonObject(with: data, options: []),
- let dict = obj as? [String: Any],
- let dataDict = dict["data"] as? [String: Any] {
- if let p = dataDict["powerWaitTime"] as? Int { newPowerWait = p }
- if let pStr = dataDict["powerWaitTime"] as? String, let p = Int(pStr) { newPowerWait = p }
- if let i = dataDict["interstitialIntervalTime"] as? Int { newInterstitial = i }
- if let iStr = dataDict["interstitialIntervalTime"] as? String, let i = Int(iStr) { newInterstitial = i }
- if let n = dataDict["flowIntervalTime"] as? Int { newNativeInterval = n }
- if let nStr = dataDict["flowIntervalTime"] as? String, let n = Int(nStr) { newNativeInterval = n }
- if let c = dataDict["canAccumulation"] as? Int { newCanAccumulation = c }
- if let cStr = dataDict["canAccumulation"] as? String, let c = Int(cStr) { newCanAccumulation = c }
- if let cBool = dataDict["canAccumulation"] as? Bool { newCanAccumulation = cBool ? 1 : 0 }
-
- // 获取广告placement ID
- if let bannerPid = dataDict["takuBannerPid"] as? String, !bannerPid.isEmpty { newBannerPid = bannerPid }
- if let interstitialPid = dataDict["takuInterstitialPid"] as? String, !interstitialPid.isEmpty { newInterstitialPid = interstitialPid }
- if let nativePid = dataDict["takuNativePid"] as? String, !nativePid.isEmpty { newNativePid = nativePid }
- if let rewardPid = dataDict["takuRewardPid"] as? String, !rewardPid.isEmpty { newRewardPid = rewardPid }
-
- // 获取原生广告自动刷新开关
- if let allowRefresh = dataDict["canAllowAutoRefresh"] as? Bool { newIsAllowNativeAutoRefresh = allowRefresh }
- if let allowRefreshStr = dataDict["canAllowAutoRefresh"] as? String {
- if allowRefreshStr.lowercased() == "true" { newIsAllowNativeAutoRefresh = true }
- else if allowRefreshStr.lowercased() == "false" { newIsAllowNativeAutoRefresh = false }
- }
- if let allowRefreshInt = dataDict["canAllowAutoRefresh"] as? Int { newIsAllowNativeAutoRefresh = allowRefreshInt != 0 }
- }
- DispatchQueue.main.async { [weak self] in
- if let p = newPowerWait, p > 0 {
- self?.staminaWaitSec = p
- print("[Config] Applied powerWaitTime = \(p)s")
- }
- self?.startGetPowerCountdownIfNeeded()
- if let i = newInterstitial, i > 0 {
- self?.interIntervalSec = i
- print("[Config] Applied interstitialIntervalTime = \(i)s")
- }
- self?.startInterstitialScheduleIfNeeded()
-
- if let n = newNativeInterval, n > 0 {
- self?.nativeIntervalSec = n
- print("[Config] Applied flowIntervalTime = \(n)s")
- }
-
- if let c = newCanAccumulation {
- self?.canStackPower = c
- print("[Config] Applied canAccumulation = \(c)")
- }
-
- // 更新广告placement ID
- if let bannerPid = newBannerPid {
- self?.bannerPlacementID = bannerPid
- print("[Config] Applied takuBannerPid = \(bannerPid)")
- }
- if let interstitialPid = newInterstitialPid {
- self?.interstitialPlacementID = interstitialPid
- print("[Config] Applied takuInterstitialPid = \(interstitialPid)")
- }
- if let nativePid = newNativePid {
- self?.nativeRenderPlacementID = nativePid
- print("[Config] Applied takuNativePid = \(nativePid)")
- }
- if let rewardPid = newRewardPid {
- self?.rewardPlacementID = rewardPid
- print("[Config] Applied takuRewardPid = \(rewardPid)")
- }
-
- // 更新原生广告自动刷新开关
- if let allowRefresh = newIsAllowNativeAutoRefresh {
- self?.isAllowNativeAutoRefresh = allowRefresh
- print("[Config] Applied canAllowAutoRefresh = \(allowRefresh)")
- }
-
- // 配置更新后,重新加载所有广告
- self?.reloadAllAdsWithNewConfig()
- }
- }.resume()
- }
- private func startInterstitialScheduleIfNeeded() {
- guard interstitialTimer == nil else { return }
- let interval = max(5, TimeInterval(interIntervalSec)) // safety floor
- interstitialTimer = Timer.scheduledTimer(timeInterval: interval, target: self, selector: #selector(handleInterstitialTick), userInfo: nil, repeats: true)
- if let t = interstitialTimer { RunLoop.main.add(t, forMode: .common) }
- print("[Config] Interstitial timer started with interval = \(interval)s")
- }
- @objc private func handleInterstitialTick() {
- // Attempt to show an interstitial; if not ready, trigger a load
- if ATAdManager.shared().interstitialReady(forPlacementID: interstitialPlacementID) {
- showInterstitialAd()
- } else {
- loadInterstitialAd()
- }
- }
-
- // MARK: - Native Ad Auto Refresh
- private func startNativeAdRefreshTimer() {
- guard nativeTimer == nil else { return }
- let interval = max(5, TimeInterval(nativeIntervalSec)) // safety floor
- nativeTimer = Timer.scheduledTimer(timeInterval: interval, target: self, selector: #selector(handleNativeAdTick), userInfo: nil, repeats: true)
- if let t = nativeTimer { RunLoop.main.add(t, forMode: .common) }
- print("[Config] Native ad timer started with interval = \(interval)s")
- }
-
- private func stopNativeAdRefreshTimer() {
- nativeTimer?.invalidate()
- nativeTimer = nil
- print("[Config] Native ad timer stopped")
- }
-
- @objc private func handleNativeAdTick() {
- // 刷新原生广告
- removeNativeAd()
- loadNativeAd()
- print("[Config] Native ad refreshed")
- }
-
- // MARK: - Ad Reload with New Config
- private func reloadAllAdsWithNewConfig() {
- print("[Config] Reloading all ads with new placement IDs")
-
- // 重新加载所有广告
- loadBannerAd()
- loadInterstitialAd()
- loadNativeAd()
- loadRewardAd()
- }
-
- // MARK: - Application Lifecycle
- @objc private func appDidEnterForeground() {
- print("[App] App did enter foreground, refreshing native ad")
- // 应用从后台进入前台时刷新原生广告
- refreshNativeAdOnAppBecomeActive()
- }
-
- private func refreshNativeAdOnAppBecomeActive() {
-
- // 移除当前原生广告
- removeNativeAd()
-
- // 重新加载原生广告
- loadNativeAd()
-
- print("[App] Native ad refreshed on app become active")
- }
- // MARK: - Toast Helper
-
- /// 延迟显示Toast消息,等待激励视频关闭后显示
- private func showToastAfterVideoClosed(message: String) {
- print("[Toast] Setting pending message: \(message), videoClosed: \(rewardVideoClosed)")
- pendingToastText = message
- // 如果视频已经关闭,立即显示
- if rewardVideoClosed {
- print("[Toast] Video already closed, showing immediately")
- showPendingToast()
- } else {
- print("[Toast] Video not closed yet, waiting for close event")
- }
- }
-
- /// 显示待显示的Toast消息
- private func showPendingToast() {
- guard let message = pendingToastText else {
- print("[Toast] No pending message to show")
- return
- }
- print("[Toast] Showing pending message: \(message)")
- pendingToastText = nil
- showToast(message: message)
- }
-
- private func showToast(message: String) {
- // Avoid stacking multiple toasts
- let tag = 9527
- if let existing = view.viewWithTag(tag) {
- existing.removeFromSuperview()
- }
- let toastLabel = UILabel()
- toastLabel.text = message
- toastLabel.font = UIFont.systemFont(ofSize: 15, weight: .medium)
- toastLabel.textColor = .white
- toastLabel.backgroundColor = UIColor.black.withAlphaComponent(0.8)
- toastLabel.textAlignment = .center
- toastLabel.alpha = 0.0
- toastLabel.layer.cornerRadius = 16
- toastLabel.clipsToBounds = true
- toastLabel.numberOfLines = 0
- toastLabel.tag = tag
- toastLabel.translatesAutoresizingMaskIntoConstraints = false
- view.addSubview(toastLabel)
- // Padding
- let horizontalPadding: CGFloat = 24
- let verticalPadding: CGFloat = 12
- let maxWidth = view.frame.width - 40
- let size = toastLabel.sizeThatFits(CGSize(width: maxWidth - horizontalPadding * 2, height: CGFloat.greatestFiniteMagnitude))
- let width = min(size.width + horizontalPadding * 2, maxWidth)
- let height = size.height + verticalPadding * 2
- // Center horizontally, bottom at ~20% above bottom
- NSLayoutConstraint.activate([
- toastLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
- toastLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor),
- toastLabel.widthAnchor.constraint(equalToConstant: width),
- toastLabel.heightAnchor.constraint(equalToConstant: height)
- ])
- UIView.animate(withDuration: 0.25, animations: {
- toastLabel.alpha = 1.0
- }) { _ in
- DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
- UIView.animate(withDuration: 0.25, animations: {
- toastLabel.alpha = 0.0
- }) { _ in
- toastLabel.removeFromSuperview()
- }
- }
- }
- }
-
- // MARK: - Image Loading (Icon)
- private static let imageCache = NSCache<NSURL, UIImage>()
- private func setIconImage(with urlString: String?) {
- guard let s = urlString, !s.isEmpty, let url = URL(string: s) else {
- self.iconImageView.image = UIImage(named: "catlogos")
- return
- }
- if let cached = QuizStageController.imageCache.object(forKey: url as NSURL) {
- self.iconImageView.image = cached
- return
- }
- URLSession.shared.dataTask(with: url) { data, _, _ in
- guard let data = data, let img = UIImage(data: data) else { return }
- QuizStageController.imageCache.setObject(img, forKey: url as NSURL)
- DispatchQueue.main.async { [weak self] in
- self?.iconImageView.image = img
- }
- }.resume()
- }
- // MARK: - Question List
- private func fetchQuestionList() {
- guard var components = URLComponents(string: apiBaseURL + getQuestionListPath) else {
- print("[Question] Invalid question list URL")
- return
- }
- // If backend needs appId, pass it; harmless if ignored
- components.queryItems = [ URLQueryItem(name: "appId", value: kTakuAppID) ]
- guard let url = components.url else { return }
- var request = URLRequest(url: url)
- request.httpMethod = "GET"
- print("[Question] Request URL: \(url)")
- URLSession.shared.dataTask(with: request) { [weak self] data, response, error in
- if let http = response as? HTTPURLResponse {
- print("[Question] Response Code: \(http.statusCode)")
- print("[Question] Response Headers: \(http.allHeaderFields)")
- }
- if let data = data, let s = String(data: data, encoding: .utf8) {
- print("[Question] Response Body: \(s)")
- }
- if let error = error {
- print("[Question] fetch error: \(error)")
- return
- }
- guard let data = data else { return }
- let parsed = self?.parseQuestionList(from: data) ?? []
- DispatchQueue.main.async { [weak self] in
- guard let self = self else { return }
- self.questions = parsed
- // 根据登录返回的 lastQuestionId(或本地缓存)定位:从其"下一关"开始
- let lastQid = self.lastQuestionIdFromLogin ?? UserDefaults.standard.string(forKey: "lastQuestionId")
- if let lq = lastQid,
- !lq.isEmpty,
- let idx = self.questions.firstIndex(where: { $0.id == lq }) {
- let nextIndex = idx + 1
- self.currentQuestionIndex = (nextIndex < self.questions.count) ? nextIndex : 0
- } else {
- // 为空或没匹配上:从第一关开始
- self.currentQuestionIndex = 0
- }
- self.renderCurrentQuestion()
- }
- }.resume()
- }
- private func parseQuestionList(from data: Data) -> [QAQuestion] {
- var result: [QAQuestion] = []
- guard let obj = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any],
- let arr = obj["data"] as? [[String: Any]] else { return result }
- for q in arr {
- let qid = (q["questionId"] as? String) ?? ""
- let content = (q["questionContent"] as? String) ?? ""
- let correct = (q["correctItem"] as? String) ?? ""
- var items: [QAItem] = []
- if let itemList = q["itemList"] as? [[String: Any]] {
- for it in itemList {
- let iid = (it["itemId"] as? String) ?? ""
- let ic = (it["itemContent"] as? String) ?? ""
- items.append(QAItem(id: iid, content: ic))
- }
- }
- if !qid.isEmpty && !content.isEmpty && !items.isEmpty {
- result.append(QAQuestion(id: qid, content: content, correctItemId: correct, items: items))
- }
- }
- return result
- }
- private func renderCurrentQuestion() {
- guard currentQuestionIndex >= 0, currentQuestionIndex < questions.count else { return }
- let q = questions[currentQuestionIndex]
- promptLabel.text = q.content
- // Reset indicators
- markA.isHidden = true
- markB.isHidden = true
- verdictLabel.text = "请选择正确答案"
- // Fill two options (use前两个)
- if q.items.indices.contains(0) {
- choiceBtnA.setTitle(q.items[0].content, for: .normal)
- option1ItemId = q.items[0].id
- } else {
- choiceBtnA.setTitle("选项A", for: .normal)
- option1ItemId = nil
- }
- if q.items.indices.contains(1) {
- choiceBtnB.setTitle(q.items[1].content, for: .normal)
- option2ItemId = q.items[1].id
- } else {
- choiceBtnB.setTitle("选项B", for: .normal)
- option2ItemId = nil
- }
- // Reset option button backgrounds to default
- choiceBtnA.setBackgroundImage(UIImage(named: "res_o67j6d3t"), for: .normal)
- choiceBtnB.setBackgroundImage(UIImage(named: "res_o67j6d3t"), for: .normal)
- // Ensure disabled/gray state persists across questions
- let currentPower = UserDefaults.standard.object(forKey: "power") as? Int ?? 0
- applyPowerToOptionButtons(currentPower)
- // 重置答题状态,允许答题
- isAnswering = false
-
- // 更新关卡显示
- levelBadge.setTitle("关卡:\(currentQuestionIndex + 1)", for: .normal)
- // 题目开始计时点
- questionStartAt = Date()
- }
- private func handleAnswerSelection(selectedButton: UIButton) {
- // 防止重复答题
- guard !isAnswering else {
- print("[答题] 正在处理中,忽略重复点击")
- return
- }
-
- guard currentQuestionIndex < questions.count else { return }
- let q = questions[currentQuestionIndex]
- // 设置答题状态,禁用按钮
- isAnswering = true
- choiceBtnA.isEnabled = false
- choiceBtnB.isEnabled = false
- let selectedId: String?
- let isFirst = (selectedButton == choiceBtnA)
- if isFirst { selectedId = option1ItemId } else { selectedId = option2ItemId }
-
- guard let sid = selectedId else {
- print("[答题] 选项ID为空")
- resetAnswerState()
- return
- }
- // 显示加载状态
- showSystemLoading()
- // verdictLabel.text = "提交中..."
- // 计算答题耗时
- let durationSec: Int = {
- if let start = questionStartAt { return max(0, Int(Date().timeIntervalSince(start))) }
- return 0
- }()
-
- // 先请求API,成功后再显示结果
- answerQuestion(questionId: q.id, itemId: sid, duration: durationSec, selectedButton: selectedButton) { [weak self] success in
- guard let self = self else { return }
-
- DispatchQueue.main.async {
- self.hideSystemLoading()
-
- if success {
- // API成功,显示答题结果
- self.showAnswerResult(question: q, selectedId: sid, selectedButton: selectedButton, isFirst: isFirst)
-
- // 1秒后进入下一题
- DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in
- self?.goToNextQuestion()
- }
- } else {
- // API失败,重置状态允许重新答题
- self.resetAnswerState()
- self.showToast(message: "提交失败,请重试")
- }
- }
- }
- }
-
- // 显示答题结果(正确/错误标记)
- private func showAnswerResult(question: QAQuestion, selectedId: String, selectedButton: UIButton, isFirst: Bool) {
- let correctImage = UIImage(named: "res_cwqxdp40")
- let wrongImage = UIImage(named: "res_4h4cvu8h")
- // 先隐藏
- markA.isHidden = true
- markB.isHidden = true
- // 正确项标绿勾
- if option1ItemId == question.correctItemId { markA.image = correctImage; markA.isHidden = false }
- if option2ItemId == question.correctItemId { markB.image = correctImage; markB.isHidden = false }
- // 如果选错,则在所选按钮旁显示红叉
- let isCorrect = (selectedId == question.correctItemId)
- if !isCorrect {
- if isFirst { markA.image = wrongImage; markA.isHidden = false }
- else { markB.image = wrongImage; markB.isHidden = false }
- verdictLabel.text = "回答错误!"
- } else {
- verdictLabel.text = "回答正确!"
- }
- // Update selected button background based on correctness
- if isCorrect {
- selectedButton.setBackgroundImage(UIImage(named: "res_rqqxjzk3"), for: .normal)
- } else {
- selectedButton.setBackgroundImage(UIImage(named: "res_zup626a7"), for: .normal)
- }
- }
-
- // 重置答题状态(用于失败时恢复)
- private func resetAnswerState() {
- isAnswering = false
-
- // 恢复按钮状态(根据体力判断)
- let currentPower = UserDefaults.standard.object(forKey: "power") as? Int ?? 0
- let enabled = currentPower > 0
- choiceBtnA.isEnabled = enabled
- choiceBtnB.isEnabled = enabled
-
- // 恢复按钮背景
- choiceBtnA.setBackgroundImage(UIImage(named: "res_o67j6d3t"), for: .normal)
- choiceBtnB.setBackgroundImage(UIImage(named: "res_o67j6d3t"), for: .normal)
-
- // 隐藏标记
- markA.isHidden = true
- markB.isHidden = true
-
- // 恢复提示文字
- verdictLabel.text = "请选择正确答案"
- }
-
- // MARK: - Answer Question API
- private func answerQuestion(questionId: String, itemId: String, duration: Int, selectedButton: UIButton, completion: @escaping (Bool) -> Void) {
- guard let url = URL(string: apiBaseURL + answerQuestionPath) else {
- print("[Answer] Invalid URL")
- completion(false)
- return
- }
- let userId = accountInfo?.userId ?? (UserDefaults.standard.string(forKey: "roleID") ?? "")
- guard !userId.isEmpty else {
- print("[Answer] Missing userId")
- completion(false)
- return
- }
- var request = URLRequest(url: url)
- request.httpMethod = "POST"
- request.setValue("application/json", forHTTPHeaderField: "Content-Type")
- request.timeoutInterval = 15 // 设置15秒超时
- let body: [String: Any] = [
- "duration": duration,
- "itemId": itemId,
- "questionId": questionId,
- "userId": userId
- ]
- request.httpBody = try? JSONSerialization.data(withJSONObject: body, options: [])
- // Logs
- if let body = request.httpBody, let bodyString = String(data: body, encoding: .utf8) {
- print("[Answer] Request URL: \(url)")
- print("[Answer] Request Headers: \(request.allHTTPHeaderFields ?? [:])")
- print("[Answer] Request Body: \(bodyString)")
- }
- URLSession.shared.dataTask(with: request) { [weak self] data, response, error in
- if let http = response as? HTTPURLResponse {
- print("[Answer] Response Code: \(http.statusCode)")
- print("[Answer] Response Headers: \(http.allHeaderFields)")
- }
- if let data = data, let s = String(data: data, encoding: .utf8) {
- print("[Answer] Response Body: \(s)")
- }
- if let error = error {
- print("[Answer] error: \(error)")
- completion(false)
- return
- }
- guard let data = data,
- let obj = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] else {
- print("[Answer] Invalid response data")
- completion(false)
- return
- }
- let code = obj["code"] as? Int ?? 0
- if code != 200 {
- let msg = (obj["message"] as? String) ?? (obj["msg"] as? String) ?? "作答上报失败"
- print("[Answer] Response code not 200: \(msg)")
- completion(false)
- return
- }
- // 请求成功后扣除体力 + 同步今日/历史答题与作答时间记录
- var returnedToday: Int?
- var returnedHistory: Int?
- var returnedLogs: [String]?
- if let dataDict = obj["data"] as? [String: Any] {
- if let v = dataDict["todayAnswerCount"] as? Int { returnedToday = v }
- else if let s = dataDict["todayAnswerCount"] as? String, let i = Int(s) { returnedToday = i }
- if let v = dataDict["historyAnswerCount"] as? Int { returnedHistory = v }
- else if let s = dataDict["historyAnswerCount"] as? String, let i = Int(s) { returnedHistory = i }
- if let arr = dataDict["answerRecordTimeList"] as? [String] { returnedLogs = arr }
- else if let anyArr = dataDict["answerRecordTimeList"] as? [Any] { returnedLogs = anyArr.compactMap { String(describing: $0) } }
- else if let arr = dataDict["answerRecordList"] as? [String] { returnedLogs = arr }
- }
- DispatchQueue.main.async { [weak self] in
- guard let self = self else {
- completion(false)
- return
- }
- // 1) 扣体力
- let currentPower = UserDefaults.standard.object(forKey: "power") as? Int ?? 0
- let newPower = max(0, currentPower - 1)
- self.staminaBadge.setTitle("体力:\(newPower)", for: .normal)
- self.applyPowerToOptionButtons(newPower)
- UserDefaults.standard.set(newPower, forKey: "power")
- if newPower == 0 { self.showToast(message: "体力不足,请先领取") }
- // 2) 更新右侧统计(若后端返回)
- if let t = returnedToday {
- self.todayCountLabel.text = "今日答题: \(t)题"
- UserDefaults.standard.set(t, forKey: "todayAnswerCount")
- }
- if let h = returnedHistory {
- self.historyCountLabel.text = "历史答题: \(h)题"
- UserDefaults.standard.set(h, forKey: "historyAnswerCount")
- }
- if let logs = returnedLogs {
- UserDefaults.standard.set(logs, forKey: "answerLogs")
- }
- // 3) 更新内存里的 loginUserInfo,用于后续弹窗展示
- if let old = self.accountInfo {
- let newInfo = LoginUserInfo(
- nickName: old.nickName,
- userId: old.userId,
- registryTimeStr: old.registryTimeStr,
- todayAnswerCount: returnedToday ?? old.todayAnswerCount,
- historyAnswerCount: returnedHistory ?? old.historyAnswerCount,
- headImgURL: old.headImgURL,
- lastLoginTimeStr: old.lastLoginTimeStr,
- answerLogs: returnedLogs ?? old.answerLogs
- )
- self.accountInfo = newInfo
- // 若弹窗正在显示,实时刷新
- self.activePowerPopup?.configure(with: newInfo)
- } else if (returnedToday != nil) || (returnedHistory != nil) || (returnedLogs != nil) {
- // 构造一个兜底的 LoginUserInfo
- let nick = UserDefaults.standard.string(forKey: "nickname") ?? "XXXX"
- let role = UserDefaults.standard.string(forKey: "roleID") ?? ""
- let reg = UserDefaults.standard.string(forKey: "registryTimeStr") ?? "--"
- let head = UserDefaults.standard.string(forKey: "headImgURL")
- let last = UserDefaults.standard.string(forKey: "lastLoginTimeStr") ?? "--"
- let t = returnedToday ?? (UserDefaults.standard.object(forKey: "todayAnswerCount") as? Int ?? 0)
- let h = returnedHistory ?? (UserDefaults.standard.object(forKey: "historyAnswerCount") as? Int ?? 0)
- let logs = returnedLogs ?? (UserDefaults.standard.stringArray(forKey: "answerLogs") ?? [])
- let info = LoginUserInfo(nickName: nick, userId: role, registryTimeStr: reg, todayAnswerCount: t, historyAnswerCount: h, headImgURL: head, lastLoginTimeStr: last, answerLogs: logs)
- self.accountInfo = info
- self.activePowerPopup?.configure(with: info)
- }
-
- // API请求成功,调用完成回调
- completion(true)
- }
- }.resume()
- }
- private func goToNextQuestion() {
- currentQuestionIndex += 1
- if currentQuestionIndex < questions.count {
- renderCurrentQuestion()
- } else {
- // 题目答完:简单重置或提示
- verdictLabel.text = "已答完全部题目"
- // 也可以重置到第一题:
- // currentQuestionIndex = 0; renderCurrentQuestion()
- }
-
- // 重置答题状态,允许下一题答题
- isAnswering = false
- }
- // MARK: - Power → Option Buttons
- private func applyPowerToOptionButtons(_ power: Int) {
- let enabled = power > 0 && !isAnswering // 体力大于0且不在答题中
- DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in
- guard let self = self else { return }
- self.choiceBtnA.isEnabled = enabled
- self.choiceBtnB.isEnabled = enabled
- // 置灰/恢复
- self.choiceBtnA.alpha = enabled ? 1.0 : 0.5
- self.choiceBtnB.alpha = enabled ? 1.0 : 0.5
- }
- }
-
- // MARK: - Add Power after Reward
- private func addPower() {
- guard var comps = URLComponents(string: apiBaseURL + addPowerPath) else {
- print("[Power] Invalid addPower URL")
- return
- }
- // 优先使用登录后返回的 userId(roleId),否则从持久化读取
- let roleId = accountInfo?.userId ?? (UserDefaults.standard.string(forKey: "roleID") ?? "")
- guard !roleId.isEmpty else {
- print("[Power] Missing userId for addPower")
- DispatchQueue.main.async { [weak self] in
- self?.showToastAfterVideoClosed(message: "缺少用户ID,无法增加体力")
- }
- return
- }
- // GET /wx/addPower?userId=xxx
- comps.queryItems = [ URLQueryItem(name: "userId", value: roleId) ]
- guard let url = comps.url else {
- print("[Power] Failed to build addPower URL with query")
- return
- }
- var request = URLRequest(url: url)
- request.httpMethod = "GET"
- // Request logs (GET,无请求体)
- print("[Power] addPower Request URL: \(url)")
- print("[Power] addPower Request Headers: \(request.allHTTPHeaderFields ?? [:])")
- URLSession.shared.dataTask(with: request) { [weak self] data, response, error in
- // Response logs
- if let http = response as? HTTPURLResponse {
- print("[Power] addPower Response Code: \(http.statusCode)")
- print("[Power] addPower Response Headers: \(http.allHeaderFields)")
- }
- if let data = data, let s = String(data: data, encoding: .utf8) {
- print("[Power] addPower Response Body: \(s)")
- }
- if let error = error {
- print("[Power] addPower error: \(error)")
- DispatchQueue.main.async { [weak self] in
- self?.showToastAfterVideoClosed(message: "网络异常,体力更新失败")
- }
- return
- }
- guard let data = data,
- let obj = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] else { return }
- let code = obj["code"] as? Int ?? 0
- if code != 200 {
- let msg = (obj["message"] as? String) ?? (obj["msg"] as? String) ?? "体力更新失败"
- DispatchQueue.main.async { [weak self] in
- self?.showToastAfterVideoClosed(message: msg)
- }
- return
- }
- var newPower = 0
- if let dataDict = obj["data"] as? [String: Any] {
- if let v = dataDict["power"] as? Int { newPower = v }
- if let s = dataDict["power"] as? String, let i = Int(s) { newPower = i }
- }
- DispatchQueue.main.async { [weak self] in
- guard let self = self else { return }
- self.staminaBadge.setTitle("体力:\(newPower)", for: .normal)
- self.applyPowerToOptionButtons(newPower)
- UserDefaults.standard.set(newPower, forKey: "power")
- // 激励成功 + 服务端加体力成功且激励视频关闭后,才启动获取体力按钮的倒计时
- self.rewardPowerOK = true
- self.didStartPowerCountdown = false
- self.startCooldownIfRewardFlowDone()
-
-
- // 延迟显示成功消息,等待激励视频关闭
- self.showToastAfterVideoClosed(message: "体力增加成功!")
- }
- }.resume()
- }
-
-
- // 广告展示开始时间(毫秒),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 // 如果后端要 UTC,就改成 TimeZone(secondsFromGMT: 0)
- 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)
- }
- // MARK: - 数据模型
- // 将 TopOn/AnyThink 收益回传转换为服务端接口字段并上报
- private func uploadAdRevenue(placementID: String, extra: [AnyHashable: Any]) {
- // 读取鉴权 token
- // guard let userInfo = UserDefaults.standard.dictionary(forKey: "userInfo"),
- // let token = userInfo["token"] as? String else {
- // print("[AD-Upload] 无 token,跳过上报")
- // return
- // }
- // 计算 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 iosId = UIDevice.current.identifierForVendor?.uuidString ?? ""
- 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": userId,
- "ecpm": ecpmStr,
- "adSourceIndex": adSourceIndex,
- "adSourceType": adSourceType,
- "resultJson": resultJson,
- "appId": appId,
- "adCount": adCount,
- "iosId": iosId,
- "begintimestamp": beginMs,
- "finishtimestamp": finishMs
- ]
- // print("testggg \(body)")
- // 发起上报
- let urlString = apiBaseURL + adSaveRecordPath
- // print("[AD-Upload] URL \(urlString)")
- 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.setValue(token, forHTTPHeaderField: "Authorization")
- 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) { [weak self] 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)")
- // 解析 JSON,并在 code == 301 时弹 toast 并延迟跳转登录
- if let obj = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
- let codeVal: Int = {
- if let i = obj["code"] as? Int { return i }
- if let str = obj["code"] as? String, let i = Int(str) { return i }
- return 0
- }()
- if codeVal == 301 {
- let message = (obj["message"] as? String) ?? (obj["msg"] as? String) ?? "软件被手机系统限制!请稍后再试!"
- DispatchQueue.main.async { [weak self] in
- self?.showToast(message: message)
- DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) { [weak self] in
- self?.switchToLoginScreen()
- }
- }
- }
- }
- }
- }.resume()
- }
- private func switchToLoginScreen() {
- // // 清除用户登录状态
- // UserDefaults.standard.set(false, forKey: "isLoggedIn")
- // UserDefaults.standard.removeObject(forKey: "userInfo")
- // 创建登录界面
- let loginVC = EntryGateController()
- // let navController = UINavigationController(rootViewController: loginVC)
-
- // 获取当前窗口
- guard let window = UIApplication.shared.windows.first(where: { $0.isKeyWindow }) else {
- return
- }
-
- // 设置根视图控制器为登录界面
- window.rootViewController = loginVC
-
- // 添加切换动画
- UIView.transition(with: window,
- duration: 0.5,
- options: .transitionCrossDissolve,
- animations: nil,
- completion: nil)
- }
-
- // MARK: - OverlayPanelAgent
- func overlayPanelDidDismiss(_ panel: OverlayPanelView) {
- // 刷新横幅广告
- removeBanner()
- loadBannerAd()
- // 刷新信息流(原生)广告
- removeNativeAd()
- loadNativeAd()
- }
- }
- // MARK: - UIColor Extension
- extension UIColor {
- convenience init?(hexString: String) {
- var hex = hexString.trimmingCharacters(in: .whitespacesAndNewlines).uppercased()
-
- if hex.hasPrefix("#") {
- hex.remove(at: hex.startIndex)
- }
-
- var rgbValue: UInt64 = 0
- Scanner(string: hex).scanHexInt64(&rgbValue)
-
- let r = CGFloat((rgbValue & 0xFF0000) >> 16) / 255.0
- let g = CGFloat((rgbValue & 0x00FF00) >> 8) / 255.0
- let b = CGFloat(rgbValue & 0x0000FF) / 255.0
-
- self.init(red: r, green: g, blue: b, alpha: 1.0)
- }
- }
|