🐥
Google AdMobのSwiftUI実装例をシンプルに書き換える
はじめに
Google AdMobを使ったモバイル広告のSwiftUI実装例は最初読んだ際にかなり混乱させられました。
かなり改善点がある気がするのでそれをここに書いておきます(上記の例は2023.8.12のもの)。
前提としては上記のコードはアダプティブな広告のため、横幅を動的に取得するようになっています。
何が混乱するのか
- structのSwiftUI.Viewである
UIViewControllerRepresentable
がclassであるGADBannerView
を保持している-
GADBannerView
が実際に広告リクエストを行うが、ViewController
からサイズ変更の値を間接的にやりとする必要が出てきてしまう-
ViewController
はCoordinator
を介してBannerViewControllerWidthDelegate
でやりとりしている- parentという名前で直接親を参照してフルにアクセスしてしまっている
- そもそも型どうしのやり取りで子に自身を渡すのはかなりアンチパターンだと私は考えているので必要がなければ避けたい
- 子が親の直接的な型に依存してしまっている
- そもそも型どうしのやり取りで子に自身を渡すのはかなりアンチパターンだと私は考えているので必要がなければ避けたい
- parentという名前で直接親を参照してフルにアクセスしてしまっている
-
-
案
- structのSwiftUI.Viewである
UIViewControllerRepresentable
がclassであるGADBannerView
を保持しないようにする-
ViewController
がGADBannerView()
を保持する- 利点
- 参照型が参照型を保持する
-
adUnitID
もViewController
に持たせればよくなる -
ViewController
から保持するGADBannerView
と直接やりとりするだけになりBannerViewControllerWidthDelegate
は必要なくなる
- 利点
-
- その他
- 広告リクエストはオートでいいのでは
- ただし、サイズを決定してからオートにしないとクラッシュする
- 元のコードだと
updateUIViewController
からリクエストしているが、0でなくても何度も呼び出されてしまうため、それをガードできていないのが気になる
- 広告リクエストはオートでいいのでは
設計: マーメイド記法
- 3つの自作型とSDKのGADBannerView
- 自作型
- AdBannerView
- Coordinator
- AdBannerViewController
- SDK
- GADBannerView
- 自作型
- SDKの
GADBannerViewDelegate
はCoordinator
が実装するかたちにしておく- サンプルでは必要がないが、いずれ広告リクエストが成功したかを知りたくなるはず
実装
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