🐥

Google AdMobのSwiftUI実装例をシンプルに書き換える

2023/08/12に公開

はじめに

Google AdMobを使ったモバイル広告のSwiftUI実装例は最初読んだ際にかなり混乱させられました。

https://developers.google.com/admob/ios/swiftui?hl=ja

かなり改善点がある気がするのでそれをここに書いておきます(上記の例は2023.8.12のもの)。

前提としては上記のコードはアダプティブな広告のため、横幅を動的に取得するようになっています。

何が混乱するのか

  • structのSwiftUI.ViewであるUIViewControllerRepresentableがclassであるGADBannerViewを保持している
    • GADBannerViewが実際に広告リクエストを行うが、ViewControllerからサイズ変更の値を間接的にやりとする必要が出てきてしまう
      • ViewControllerCoordinatorを介してBannerViewControllerWidthDelegateでやりとりしている
        • parentという名前で直接親を参照してフルにアクセスしてしまっている
          • そもそも型どうしのやり取りで子に自身を渡すのはかなりアンチパターンだと私は考えているので必要がなければ避けたい
            • 子が親の直接的な型に依存してしまっている

  • structのSwiftUI.ViewであるUIViewControllerRepresentableがclassであるGADBannerViewを保持しないようにする
    • ViewControllerGADBannerView()を保持する
      • 利点
        • 参照型が参照型を保持する
        • adUnitIDViewControllerに持たせればよくなる
        • ViewControllerから保持するGADBannerViewと直接やりとりするだけになりBannerViewControllerWidthDelegateは必要なくなる
  • その他
    • 広告リクエストはオートでいいのでは
      • ただし、サイズを決定してからオートにしないとクラッシュする
      • 元のコードだとupdateUIViewControllerからリクエストしているが、0でなくても何度も呼び出されてしまうため、それをガードできていないのが気になる

設計: マーメイド記法

  • 3つの自作型とSDKのGADBannerView
    • 自作型
      • AdBannerView
      • Coordinator
      • AdBannerViewController
    • SDK
      • GADBannerView
  • SDKのGADBannerViewDelegateCoordinatorが実装するかたちにしておく
    • サンプルでは必要がないが、いずれ広告リクエストが成功したかを知りたくなるはず

実装

import SwiftUI
import UIKit
import GoogleMobileAds

struct AdBannerView: UIViewControllerRepresentable {
    init() {}

    typealias UIViewControllerType = AdBannerViewController

    func makeUIViewController(context: Context) -> UIViewControllerType {
        let bannerViewController = UIViewControllerType()
        bannerViewController.adBannerView.delegate = context.coordinator

        return bannerViewController
    }

    func updateUIViewController(
        _ uiViewController: UIViewControllerType,
        context: Context
    ) {
    }

    func makeCoordinator() -> Coordinator {
        Coordinator()
    }

    ///
    class Coordinator: NSObject, GADBannerViewDelegate {
        init() {}

        // MARK: - GADBannerViewDelegate methods

        func bannerViewDidReceiveAd(_ bannerView: GADBannerView) {
            print("\(#function) called")
        }

        func bannerView(_ bannerView: GADBannerView, didFailToReceiveAdWithError error: Error) {
            print("💣 \(#function) called: \(error)")
            Logger.error(error, category: "BannerView")
        }

        func bannerViewDidRecordImpression(_ bannerView: GADBannerView) {
            print("\(#function) called")
        }

        func bannerViewWillPresentScreen(_ bannerView: GADBannerView) {
            print("\(#function) called")
        }

        func bannerViewWillDismissScreen(_ bannerView: GADBannerView) {
            print("\(#function) called")
        }

        func bannerViewDidDismissScreen(_ bannerView: GADBannerView) {
            print("\(#function) called")
        }
    }
}

class AdBannerViewController: UIViewController {
    // adaptive
    private let adUnitID = "..."

    private(set) var adBannerView = GADBannerView()

    override func loadView() {
        super.loadView()
        adBannerView.adUnitID = adUnitID
        view.addSubview(adBannerView)
        adBannerView.rootViewController = self
    }

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)

        if let error = adBannerView.responseInfo?.loadedAdNetworkResponseInfo?.error {
	  // 何らかのエラー処理
        }

        applyBanner(
            view.frame.inset(
                by: view.safeAreaInsets
            ).size.width
        )
    }

   // 回転や動的に変化する横幅のためか?未検証
    override func viewWillTransition(
        to size: CGSize,
        with coordinator: UIViewControllerTransitionCoordinator
    ) {
        coordinator.animate { _ in
            // do nothing
        } completion: { _ in
            self.applyBanner(
                self.view.frame.inset(
                    by: self.view.safeAreaInsets
                ).size.width
            )
        }
    }

    private func applyBanner(_ width: CGFloat) {
        adBannerView.adSize = GADCurrentOrientationAnchoredAdaptiveBannerAdSizeWithWidth(
            width
        )
        adBannerView.isAutoloadEnabled = true
    }
}

まとめ

UIViewControllerからSwiftUIに変換するように普通に作ればいいだけで、さらにこのサンプルではSwiftUIに変換した部分はただ表示されるだけなので状態をもつ必要がないはずです。そのためViewControllerなどももつ必要がないと感じます。

脱線しますが、parentなんぞで親を子(Coordinator)に渡すのもよしたほうが良いと感じます。これに関してはなぜこのSwiftUI変換時にこんなコード例になるのかずっと不思議に思っています。何が不思議かというと子が親のインタフェースではなく実体の参照に依存してしまうからです。親はstructであり、子がclassというのもミスが起こりやすいパターンじゃないでしょうか?Delegateパターンという考えがそもそもあるiOSアプリ開発で実体を渡すことってあまり正しいとは思えず、じゃあもちろんDelegate使うかっていうとそんなことしなくてもイベントをクロージャで渡すだけでよいです。もし、parentで直接参照渡すほうが良いということがあれば教えていただきたいと思っています。

Discussion