import SwiftUI import UIKit #if canImport(GoogleMobileAds) import GoogleMobileAds struct AdBannerView: View { var adUnitId: String = AdMobConfig.resolvedBannerHomeUnitId @State private var state: BannerState = .loading var body: some View { VStack(spacing: 0) { Rectangle() .fill(Color.mealMoodTextSecondary.opacity(0.22)) .frame(height: 1) ZStack { Color.mealMoodSurface BannerBridge(adUnitId: adUnitId, state: $state) if state != .loaded { HStack(spacing: 6) { Image(systemName: "megaphone") Text(state == .loading ? "ads_loading" : "ads_unavailable") .font(.mealMoodCaption) } .foregroundColor(.mealMoodTextSecondary) } } .frame(height: 56) .clipped() } .frame(maxWidth: .infinity) .background(Color.mealMoodSurface) } } private enum BannerState { case loading case loaded case failed } private struct BannerBridge: UIViewRepresentable { let adUnitId: String @Binding var state: BannerState func makeCoordinator() -> Coordinator { Coordinator(state: $state) } func makeUIView(context: Context) -> UIView { let container = UIView() container.backgroundColor = UIColor(Color.mealMoodSurface) container.clipsToBounds = true let width = max(UIScreen.main.bounds.width, 320) let adSize = GADCurrentOrientationAnchoredAdaptiveBannerAdSizeWithWidth(width) let banner = GADBannerView(adSize: adSize) banner.adUnitID = adUnitId banner.delegate = context.coordinator banner.rootViewController = rootViewController() banner.translatesAutoresizingMaskIntoConstraints = false banner.backgroundColor = .clear context.coordinator.bannerView = banner container.addSubview(banner) NSLayoutConstraint.activate([ banner.centerXAnchor.constraint(equalTo: container.centerXAnchor), banner.centerYAnchor.constraint(equalTo: container.centerYAnchor), banner.widthAnchor.constraint(equalToConstant: adSize.size.width), banner.heightAnchor.constraint(equalToConstant: adSize.size.height) ]) context.coordinator.loadBannerIfPossible() return container } func updateUIView(_ uiView: UIView, context: Context) { context.coordinator.bannerView?.rootViewController = rootViewController() context.coordinator.loadBannerIfPossible() } private func rootViewController() -> UIViewController? { UIApplication.shared.connectedScenes .compactMap { $0 as? UIWindowScene } .flatMap(\.windows) .first(where: \.isKeyWindow)? .rootViewController } final class Coordinator: NSObject, GADBannerViewDelegate { @Binding var state: BannerState weak var bannerView: GADBannerView? private var didRequestLoad = false private var retryCount = 0 init(state: Binding) { self._state = state } func loadBannerIfPossible() { guard let bannerView, bannerView.rootViewController != nil else { return } guard !didRequestLoad else { return } didRequestLoad = true state = .loading bannerView.load(GADRequest()) } func bannerViewDidReceiveAd(_ bannerView: GADBannerView) { state = .loaded retryCount = 0 } func bannerView(_ bannerView: GADBannerView, didFailToReceiveAdWithError error: any Error) { state = .failed didRequestLoad = false if retryCount < 2 { retryCount += 1 loadBannerIfPossible() } #if DEBUG print("AdMob banner failed (\(retryCount)): \(error.localizedDescription)") #endif } } } #else struct AdBannerView: View { var adUnitId: String = AdMobConfig.resolvedBannerHomeUnitId var body: some View { VStack(spacing: 0) { Rectangle() .fill(Color.mealMoodTextSecondary.opacity(0.22)) .frame(height: 1) HStack(spacing: 6) { Image(systemName: "megaphone") Text("ads_unavailable") .font(.mealMoodCaption) } .foregroundColor(.mealMoodTextSecondary) .frame(maxWidth: .infinity, minHeight: 56) .background(Color.mealMoodSurface) } .frame(maxWidth: .infinity) .background(Color.mealMoodSurface) } } #endif