// // 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 } 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 } } 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 = false 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() } }