[iOS] Google AdMobのバナーをSwiftUIで実装する

## はじめに

SwiftUI製のアプリにAdMobのバナーを実装したく調べてみました。

https://developers.google.com/admob/ios/swiftui?hl=en#banner

公式でSwiftUIのViewが用意されているのかな？と思いましたが、なかったです。代わりに`UIViewControllerRepresentable`を使う方法がドキュメントにまとまっていたのでこちらでやっていきます。

登録方法は以前UIKitを使ったAdMobの導入方法をまとめたのでこちらを

https://qiita.com/yoshitaka/items/d697bb1bbe56923ec6f0


## 環境
Xcode 14.3

## 内容

実装手順以下の通り
- 広告の幅を決定
- `UIViewControllerRepresentable`で`GADBannerView`を作成
- `UIViewController`から`UIViewRepresentable`に幅の値を渡す`Coordinator`を作成


### 広告の幅を決定する

`UIViewController`から`SwiftUI`にデバイスのフレーム幅を渡すデリゲートを定義して実装します。
 
```swift:BannerViewController
import UIKit

// Delegate methods for receiving width update messages.
protocol BannerViewControllerWidthDelegate: AnyObject {
  func bannerViewController(_ bannerViewController: BannerViewController, didUpdate width: CGFloat)
}

class BannerViewController: UIViewController {
  weak var delegate: BannerViewControllerWidthDelegate?

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

    // Tell the delegate the initial ad width.
    delegate?.bannerViewController(
      self, didUpdate: view.frame.inset(by: view.safeAreaInsets).size.width)
  }

  override func viewWillTransition(
    to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator
  ) {
    coordinator.animate { _ in
      // do nothing
    } completion: { _ in
      // Notify the delegate of ad width changes.
      self.delegate?.bannerViewController(
        self, didUpdate: self.view.frame.inset(by: self.view.safeAreaInsets).size.width)
    }
  }
}
```

### `GADBannerView`を作成

SwiftUIでViewを表示するには、UIViewControllerRepresentableプロトコルに準拠した型を作成。Viewの幅が変更されるたびに新しいバナー広告を要求するようにします。

```swift:BannerView
import SwiftUI
import GoogleMobileAds

struct BannerView: UIViewControllerRepresentable {
    @State private var viewWidth: CGFloat = .zero
    private let bannerView = GADBannerView()
    private let adUnitID = "ca-app-pub-3940256099942544/2934735716"　　// Test用のIDが入力されています

    func makeUIViewController(context: Context) -> some UIViewController {
        let bannerViewController = BannerViewController()
        bannerView.adUnitID = adUnitID
        bannerView.rootViewController = bannerViewController
        bannerViewController.view.addSubview(bannerView)

        return bannerViewController
    }

    func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
        guard viewWidth != .zero else { return }

        // Request a banner ad with the updated viewWidth.
        bannerView.adSize = GADCurrentOrientationAnchoredAdaptiveBannerAdSizeWithWidth(viewWidth)
        bannerView.load(GADRequest())
    }
}
```

### 幅の値を渡すCoordinatorを作成

BannerViewControllerWidthDelegateに準拠するCoordinatorを作成。Coordinatorが幅の値を直接変更できるように、親BannerViewのプロパティを宣言します。

```diff_swift:BannerView
import SwiftUI
import GoogleMobileAds

struct BannerView: UIViewControllerRepresentable {
    @State private var viewWidth: CGFloat = .zero
    private let bannerView = GADBannerView()
    private let adUnitID = Bundle.main.object(forInfoDictionaryKey: "MainBannerAdUnitId") as? String

    func makeUIViewController(context: Context) -> some UIViewController {
        let bannerViewController = BannerViewController()
        bannerView.adUnitID = adUnitID
        bannerView.rootViewController = bannerViewController
        bannerViewController.view.addSubview(bannerView)
+       // Tell the bannerViewController to update our Coordinator when the ad width changes.
+       bannerViewController.delegate = context.coordinator

        return bannerViewController
    }

    func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
        guard viewWidth != .zero else { return }

        // Request a banner ad with the updated viewWidth.
        bannerView.adSize = GADCurrentOrientationAnchoredAdaptiveBannerAdSizeWithWidth(viewWidth)
        bannerView.load(GADRequest())
    }

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

+   class Coordinator: NSObject, BannerViewControllerWidthDelegate {
+       let parent: BannerView
+
+       init(_ parent: BannerView) {
+           self.parent = parent
+       }
+
+       // MARK: - BannerViewControllerWidthDelegate methods
+       func bannerViewController(_ bannerViewController: BannerViewController, didUpdate width: CGFloat) {
+           // Pass the viewWidth from Coordinator to BannerView.
+           parent.viewWidth = width
+       }
+   }
}
```

以上で準備は完了

`BannerView`を追加することで広告が表示される。

（ただし、`BannerView`を直接配置したいView、VStackなどに追加したところ広告が表示されなかったのでSwiftUIのViewで一度ラップしてあげると良いかと思います）

```swift
struct ContentView: View {

  var body: some View {
    BannerView()
  }
}
```

<img width="200" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/631585/18997e70-8059-cbb9-22fd-10e5a0fee9b6.png">

画面下部にTest modeの広告が表示されました🎉

### 各イベントを受け取りたい場合

`GADBannerViewDelegate`を`Coordinator`に追加することで実現できる。

```swift
  class Coordinator: NSObject, BannerViewControllerWidthDelegate, GADBannerViewDelegate
  {
    ...

    // MARK: - GADBannerViewDelegate methods

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

    func bannerView(_ bannerView: GADBannerView, didFailToReceiveAdWithError error: Error) {
      print("\(#function) called")
    }

    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")
    }
  }

```

## おわりに

SwiftUIのViewを用意してくれたら良いなと思いつつも、ドキュメントに丁寧に実装方法が載っていたので助かりました。
バナー以外のViewの実装方法もドキュメントに記載ありましたので、必要になったタイミングで見ていこうと思います。


## 参考

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