Fix AdMob history banner and refine Felica history mapping

This commit is contained in:
2026-04-24 21:32:56 +07:00
parent 8f0b001501
commit 7882f77a27
6 changed files with 289 additions and 38 deletions

View File

@ -223,38 +223,133 @@ private struct BannerAdView: UIViewRepresentable {
func makeCoordinator() -> Coordinator { Coordinator(self) }
func makeUIView(context: Context) -> GADBannerView {
let banner = GADBannerView(adSize: adSize)
func makeUIView(context: Context) -> HistoryBannerView {
let banner = HistoryBannerView(adSize: adSize)
banner.adUnitID = adUnitID
banner.delegate = context.coordinator
banner.rootViewController = context.coordinator.findRootViewController()
banner.load(GADRequest())
banner.onDidMoveToWindow = { [weak coordinator = context.coordinator] bannerView in
coordinator?.handleBannerDidMoveToWindow(bannerView)
}
return banner
}
func updateUIView(_ uiView: GADBannerView, context: Context) {}
func updateUIView(_ uiView: HistoryBannerView, context: Context) {
context.coordinator.attachRootViewControllerIfNeeded(to: uiView)
context.coordinator.loadBannerIfNeeded(uiView, reason: "updateUIView")
}
// MARK: Coordinator
final class Coordinator: NSObject, GADBannerViewDelegate {
let parent: BannerAdView
private var retryWorkItem: DispatchWorkItem?
private var isAdLoaded = false
private var isLoadInFlight = false
private let retryDelay: TimeInterval = 20
init(_ parent: BannerAdView) { self.parent = parent }
deinit {
retryWorkItem?.cancel()
}
func bannerViewDidReceiveAd(_ bannerView: GADBannerView) {
retryWorkItem?.cancel()
isLoadInFlight = false
isAdLoaded = true
log("banner loaded", bannerView: bannerView)
DispatchQueue.main.async { self.parent.onAdLoaded?() }
}
func bannerView(_ bannerView: GADBannerView, didFailToReceiveAdWithError error: Error) {
isLoadInFlight = false
isAdLoaded = false
log("banner failed", bannerView: bannerView, error: error)
scheduleRetry(for: bannerView)
DispatchQueue.main.async { self.parent.onAdFailed?() }
}
func findRootViewController() -> UIViewController? {
UIApplication.shared.connectedScenes
.compactMap { $0 as? UIWindowScene }
.flatMap { $0.windows }
.first { $0.isKeyWindow }?
.rootViewController
func handleBannerDidMoveToWindow(_ bannerView: GADBannerView) {
attachRootViewControllerIfNeeded(to: bannerView)
loadBannerIfNeeded(bannerView, reason: "didMoveToWindow")
}
func attachRootViewControllerIfNeeded(to bannerView: GADBannerView) {
guard let rootViewController = findNearestViewController(from: bannerView) else {
log("root view controller not ready", bannerView: bannerView)
return
}
if bannerView.rootViewController !== rootViewController {
bannerView.rootViewController = rootViewController
log("attached root view controller", bannerView: bannerView)
}
}
func loadBannerIfNeeded(_ bannerView: GADBannerView, reason: String) {
guard !isAdLoaded, !isLoadInFlight else { return }
guard bannerView.window != nil else {
log("skip load: banner is not in window", bannerView: bannerView, reason: reason)
return
}
guard bannerView.rootViewController != nil else {
log("skip load: root view controller missing", bannerView: bannerView, reason: reason)
return
}
retryWorkItem?.cancel()
isLoadInFlight = true
log("loading banner", bannerView: bannerView, reason: reason)
bannerView.load(GADRequest())
}
private func scheduleRetry(for bannerView: GADBannerView) {
retryWorkItem?.cancel()
let workItem = DispatchWorkItem { [weak self, weak bannerView] in
guard let self, let bannerView else { return }
self.attachRootViewControllerIfNeeded(to: bannerView)
self.loadBannerIfNeeded(bannerView, reason: "retry")
}
retryWorkItem = workItem
log("scheduling retry in \(Int(retryDelay))s", bannerView: bannerView)
DispatchQueue.main.asyncAfter(deadline: .now() + retryDelay, execute: workItem)
}
private func log(_ message: String, bannerView: GADBannerView, reason: String? = nil, error: Error? = nil) {
var parts = ["[AdMob][History]", message]
if let reason {
parts.append("reason=\(reason)")
}
parts.append("adUnitID=\(bannerView.adUnitID ?? "-")")
parts.append("visible=\(bannerView.window != nil)")
parts.append("loaded=\(isAdLoaded)")
parts.append("inFlight=\(isLoadInFlight)")
parts.append("rootVC=\(String(describing: type(of: bannerView.rootViewController)))")
if let nsError = error as NSError? {
parts.append("errorDomain=\(nsError.domain)")
parts.append("errorCode=\(nsError.code)")
parts.append("error=\(nsError.localizedDescription)")
}
debugLog(parts.joined(separator: " | "))
}
private func findNearestViewController(from view: UIView) -> UIViewController? {
sequence(first: view.next, next: { $0?.next })
.first { $0 is UIViewController } as? UIViewController
}
}
}
private final class HistoryBannerView: GADBannerView {
var onDidMoveToWindow: ((HistoryBannerView) -> Void)?
override func didMoveToWindow() {
super.didMoveToWindow()
onDidMoveToWindow?(self)
}
}
@ -400,11 +495,19 @@ final class HistoryHostingController: UIViewController {
}
private func loadInterstitial() {
debugLog("[AdMob][HistoryInterstitial] loading interstitial | adUnitID=\(interstitialAdUnitID)")
GADInterstitialAd.load(
withAdUnitID: interstitialAdUnitID,
request: GADRequest()
) { [weak self] ad, _ in
// Store ad if loaded; ignore error fallback to direct export
) { [weak self] ad, error in
if let error = error as NSError? {
debugLog(
"[AdMob][HistoryInterstitial] failed | adUnitID=\(self?.interstitialAdUnitID ?? "-") | errorDomain=\(error.domain) | errorCode=\(error.code) | error=\(error.localizedDescription)"
)
} else {
debugLog("[AdMob][HistoryInterstitial] loaded | adUnitID=\(self?.interstitialAdUnitID ?? "-")")
}
self?.interstitial = ad
self?.interstitial?.fullScreenContentDelegate = self
}
@ -645,6 +748,7 @@ extension HistoryHostingController: GADFullScreenContentDelegate {
// Called when the interstitial is dismissed proceed with export
func adDidDismissFullScreenContent(_ ad: GADFullScreenPresentingAd) {
debugLog("[AdMob][HistoryInterstitial] dismissed")
interstitial = nil
loadInterstitial() // pre-load for the next export attempt
exportPDF()
@ -652,6 +756,10 @@ extension HistoryHostingController: GADFullScreenContentDelegate {
// Called when the interstitial fails to present fall back to direct export
func ad(_ ad: GADFullScreenPresentingAd, didFailToPresentFullScreenContentWithError error: Error) {
let nsError = error as NSError
debugLog(
"[AdMob][HistoryInterstitial] present failed | errorDomain=\(nsError.domain) | errorCode=\(nsError.code) | error=\(nsError.localizedDescription)"
)
interstitial = nil
exportPDF()
}