Ultraworked with [Sisyphus] https://github.com/code-yeongyu/oh-my-openagent Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
306 lines
9.2 KiB
Swift
306 lines
9.2 KiB
Swift
//
|
|
// YandexAdSupport.swift
|
|
// SodaLive
|
|
//
|
|
// Created by OpenCode on 2026/04/28.
|
|
//
|
|
|
|
import SwiftUI
|
|
import YandexMobileAds
|
|
|
|
enum YandexBannerPlacement {
|
|
case liveTab
|
|
case liveDetail
|
|
case contentDetail
|
|
case creatorCommunityAll
|
|
case seriesMainHome
|
|
case seriesMainDayOfWeek
|
|
case seriesMainByGenre
|
|
case pushNotificationList
|
|
case notificationReceiveSettings
|
|
case chatCharacterList
|
|
case chatOriginalTabTop
|
|
case chatTalkTabTop
|
|
}
|
|
|
|
enum YandexInterstitialPlacement {
|
|
case contentDetail
|
|
}
|
|
|
|
enum YandexAdUnitIdProvider {
|
|
|
|
static func banner(for placement: YandexBannerPlacement) -> String {
|
|
switch placement {
|
|
case .liveTab:
|
|
YANDEX_LIVE_TAB_BANNER_AD_UNIT_ID
|
|
case .liveDetail:
|
|
YANDEX_LIVE_DETAIL_BANNER_AD_UNIT_ID
|
|
case .contentDetail:
|
|
YANDEX_CONTENT_DETAIL_BANNER_AD_UNIT_ID
|
|
case .creatorCommunityAll:
|
|
YANDEX_CREATOR_COMMUNITY_ALL_BANNER_AD_UNIT_ID
|
|
case .seriesMainHome:
|
|
YANDEX_SERIES_MAIN_HOME_BANNER_AD_UNIT_ID
|
|
case .seriesMainDayOfWeek:
|
|
YANDEX_SERIES_MAIN_DAY_OF_WEEK_BANNER_AD_UNIT_ID
|
|
case .seriesMainByGenre:
|
|
YANDEX_SERIES_MAIN_BY_GENRE_BANNER_AD_UNIT_ID
|
|
case .pushNotificationList:
|
|
YANDEX_PUSH_NOTIFICATION_LIST_BANNER_AD_UNIT_ID
|
|
case .notificationReceiveSettings:
|
|
YANDEX_NOTIFICATION_RECEIVE_SETTINGS_BANNER_AD_UNIT_ID
|
|
case .chatCharacterList:
|
|
YANDEX_CHAT_CHARACTER_LIST_BANNER_AD_UNIT_ID
|
|
case .chatOriginalTabTop:
|
|
YANDEX_CHAT_ORIGINAL_TAB_TOP_BANNER_AD_UNIT_ID
|
|
case .chatTalkTabTop:
|
|
YANDEX_CHAT_TALK_TAB_TOP_BANNER_AD_UNIT_ID
|
|
}
|
|
}
|
|
|
|
static func interstitial(for placement: YandexInterstitialPlacement) -> String {
|
|
switch placement {
|
|
case .contentDetail:
|
|
YANDEX_CONTENT_DETAIL_INTERSTITIAL_AD_UNIT_ID
|
|
}
|
|
}
|
|
}
|
|
|
|
struct YandexInlineBannerView: View {
|
|
|
|
let placement: YandexBannerPlacement
|
|
|
|
var maxHeight: CGFloat = 90
|
|
var horizontalPadding: CGFloat = 13.3
|
|
|
|
@State private var bannerHeight: CGFloat = 0
|
|
@State private var isLoadFailed = true
|
|
|
|
var body: some View {
|
|
GeometryReader { proxy in
|
|
let width = max(proxy.size.width - (horizontalPadding * 2), 1)
|
|
let resolvedHeight = isLoadFailed ? 0 : (bannerHeight > 0 ? bannerHeight : maxHeight)
|
|
|
|
YandexInlineBannerContainer(
|
|
placement: placement,
|
|
width: width,
|
|
maxHeight: maxHeight,
|
|
bannerHeight: $bannerHeight,
|
|
isLoadFailed: $isLoadFailed
|
|
)
|
|
.frame(width: width, height: resolvedHeight)
|
|
.padding(.horizontal, horizontalPadding)
|
|
}
|
|
.frame(height: isLoadFailed ? 0 : (bannerHeight > 0 ? bannerHeight : maxHeight))
|
|
}
|
|
}
|
|
|
|
private struct YandexInlineBannerContainer: UIViewRepresentable {
|
|
|
|
let placement: YandexBannerPlacement
|
|
|
|
let width: CGFloat
|
|
let maxHeight: CGFloat
|
|
|
|
@Binding var bannerHeight: CGFloat
|
|
@Binding var isLoadFailed: Bool
|
|
|
|
func makeCoordinator() -> Coordinator {
|
|
Coordinator(parent: self)
|
|
}
|
|
|
|
func makeUIView(context: Context) -> UIView {
|
|
let view = UIView()
|
|
view.backgroundColor = .clear
|
|
return view
|
|
}
|
|
|
|
func updateUIView(_ uiView: UIView, context: Context) {
|
|
context.coordinator.parent = self
|
|
context.coordinator.configureIfNeeded(containerView: uiView)
|
|
}
|
|
|
|
final class Coordinator: NSObject, BannerAdViewDelegate {
|
|
|
|
var parent: YandexInlineBannerContainer
|
|
private weak var containerView: UIView?
|
|
private var bannerAdView: BannerAdView?
|
|
private var currentWidth: CGFloat = 0
|
|
|
|
init(parent: YandexInlineBannerContainer) {
|
|
self.parent = parent
|
|
}
|
|
|
|
func configureIfNeeded(containerView: UIView) {
|
|
self.containerView = containerView
|
|
|
|
let roundedWidth = parent.width.rounded(.down)
|
|
|
|
guard bannerAdView == nil || abs(currentWidth - roundedWidth) > 0.5 else {
|
|
return
|
|
}
|
|
|
|
currentWidth = roundedWidth
|
|
|
|
DispatchQueue.main.async {
|
|
self.parent.bannerHeight = 0
|
|
self.parent.isLoadFailed = false
|
|
}
|
|
|
|
bannerAdView?.removeFromSuperview()
|
|
|
|
let adSize = BannerAdSize.inline(width: roundedWidth, maxHeight: parent.maxHeight)
|
|
let bannerAdView = BannerAdView(adSize: adSize)
|
|
bannerAdView.translatesAutoresizingMaskIntoConstraints = false
|
|
bannerAdView.delegate = self
|
|
|
|
containerView.addSubview(bannerAdView)
|
|
|
|
NSLayoutConstraint.activate([
|
|
bannerAdView.topAnchor.constraint(equalTo: containerView.topAnchor),
|
|
bannerAdView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor),
|
|
bannerAdView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor),
|
|
bannerAdView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor)
|
|
])
|
|
|
|
self.bannerAdView = bannerAdView
|
|
bannerAdView.loadAd(with: AdRequest(adUnitID: YandexAdUnitIdProvider.banner(for: parent.placement)))
|
|
}
|
|
|
|
func bannerAdViewDidLoad(_ adView: BannerAdView) {
|
|
adView.layoutIfNeeded()
|
|
|
|
let measuredHeight = adView.bounds.height > 0 ? adView.bounds.height : parent.maxHeight
|
|
|
|
DispatchQueue.main.async {
|
|
self.parent.bannerHeight = measuredHeight
|
|
self.parent.isLoadFailed = false
|
|
}
|
|
}
|
|
|
|
func bannerAdViewDidFailLoading(_ adView: BannerAdView, error: Error) {
|
|
DispatchQueue.main.async {
|
|
self.parent.bannerHeight = 0
|
|
self.parent.isLoadFailed = true
|
|
}
|
|
}
|
|
|
|
func bannerAdViewDidClick(_ adView: BannerAdView) {
|
|
}
|
|
|
|
func bannerAdView(_ adView: BannerAdView, didTrackImpression impressionData: ImpressionData?) {
|
|
}
|
|
}
|
|
}
|
|
|
|
@MainActor
|
|
final class YandexInterstitialAdManager: NSObject {
|
|
|
|
static let shared = YandexInterstitialAdManager()
|
|
|
|
private var interstitialAd: InterstitialAd?
|
|
private var interstitialAdLoader: InterstitialAdLoader?
|
|
private var currentPlacement: YandexInterstitialPlacement?
|
|
private var pendingAction: (@MainActor () -> Void)?
|
|
private var isLoading = false
|
|
|
|
func preloadAd(for placement: YandexInterstitialPlacement) {
|
|
guard !isLoading else {
|
|
return
|
|
}
|
|
|
|
if currentPlacement == placement, interstitialAd != nil {
|
|
return
|
|
}
|
|
|
|
let loader = InterstitialAdLoader()
|
|
interstitialAdLoader = loader
|
|
interstitialAd = nil
|
|
currentPlacement = placement
|
|
isLoading = true
|
|
|
|
Task {
|
|
do {
|
|
let loadedAd = try await loader.loadAd(with: AdRequest(adUnitID: YandexAdUnitIdProvider.interstitial(for: placement)))
|
|
guard currentPlacement == placement else {
|
|
isLoading = false
|
|
return
|
|
}
|
|
|
|
interstitialAd = loadedAd
|
|
} catch {
|
|
if currentPlacement == placement {
|
|
interstitialAd = nil
|
|
}
|
|
}
|
|
|
|
if currentPlacement == placement {
|
|
isLoading = false
|
|
}
|
|
}
|
|
}
|
|
|
|
func showAdIfAvailable(for placement: YandexInterstitialPlacement, then action: @escaping @MainActor () -> Void) {
|
|
guard let presenter = presentingViewController(), let interstitialAd, currentPlacement == placement else {
|
|
action()
|
|
preloadAd(for: placement)
|
|
return
|
|
}
|
|
|
|
pendingAction = action
|
|
self.interstitialAd = nil
|
|
interstitialAd.delegate = self
|
|
interstitialAd.show(from: presenter)
|
|
}
|
|
|
|
private func completePendingAction() {
|
|
let action = pendingAction
|
|
pendingAction = nil
|
|
action?()
|
|
|
|
if let currentPlacement {
|
|
preloadAd(for: currentPlacement)
|
|
}
|
|
}
|
|
|
|
private func presentingViewController() -> UIViewController? {
|
|
guard
|
|
let rootViewController = UIApplication.shared.connectedScenes
|
|
.compactMap({ $0 as? UIWindowScene })
|
|
.flatMap({ $0.windows })
|
|
.first(where: { $0.isKeyWindow })?
|
|
.rootViewController
|
|
else {
|
|
return nil
|
|
}
|
|
|
|
var topViewController = rootViewController
|
|
|
|
while let presentedViewController = topViewController.presentedViewController {
|
|
topViewController = presentedViewController
|
|
}
|
|
|
|
return topViewController
|
|
}
|
|
}
|
|
|
|
extension YandexInterstitialAdManager: InterstitialAdDelegate {
|
|
|
|
func interstitialAdDidShow(_ interstitialAd: InterstitialAd) {
|
|
}
|
|
|
|
func interstitialAdDidDismiss(_ interstitialAd: InterstitialAd) {
|
|
completePendingAction()
|
|
}
|
|
|
|
func interstitialAdDidClick(_ interstitialAd: InterstitialAd) {
|
|
}
|
|
|
|
func interstitialAd(_ interstitialAd: InterstitialAd, didTrackImpression impressionData: ImpressionData?) {
|
|
}
|
|
|
|
func interstitialAd(_ interstitialAd: InterstitialAd, didFailToShow error: Error) {
|
|
completePendingAction()
|
|
}
|
|
}
|