Fix AdMob history banner and refine Felica history mapping
This commit is contained in:
@ -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()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user