SwiftUIで実装した公式AdMob Bannerのコードを修正する

https://developers.google.com/admob/ios/swiftui

iOSに広告を表示するためのライブラリAdMobは公式にSwiftUIに対応しています。

# Bannerのコード
広告にはいろいろな種別がありますが、バナーのコードを見ていきます。
下記のコードは、公式に沿って実装し、ビルドが通るようにしたコードになります。

```swift BannerViewControllerWidthDelegate.swift
protocol BannerViewControllerWidthDelegate: AnyObject {
    func bannerViewController(_ bannerViewController: BannerViewController, didUpdate width: CGFloat)
}
```

```swift BannerViewController.swift
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 _: 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
            )
        }
    }
}
```

```swift BannerView.swift
struct BannerView: UIViewControllerRepresentable {
    @State private var viewWidth: CGFloat = .zero
    private let bannerView = GADBannerView()
    private let adUnitID = "ca-app-pub-3940256099942544/2934735716"

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

        return bannerViewController
    }

    func updateUIViewController(_: 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, GADBannerViewDelegate {
        let parent: BannerView

        init(_ parent: BannerView) {
            self.parent = parent
        }

        // MARK: - BannerViewControllerWidthDelegate methods

        func bannerViewController(_: BannerViewController, didUpdate width: CGFloat) {
            // Pass the viewWidth from Coordinator to BannerView.
            parent.viewWidth = width
        }

        // MARK: - GADBannerViewDelegate methods

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

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

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

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

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

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

```

このコードに沿ってバナーを表示すると、実際にバナーが表示されます。

```swift
struct AdMobAdView: View {
    var body: some View {
        BannerView()
    }
}
```
![Simulator Screenshot - iPhone 15 - 2023-11-17 at 08.43.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/687960/c086c9f7-1378-950c-320c-c42e7bbcbb37.png)

### 横画面表示

横画面にすると、BannerViewControllerでviewWillTransitionが動作し、delegate経由で取得した最適なバナーサイズをviewWidthとして取得します。
その際にupdateUIViewControllerが動作し、バナーサイズを変更して再ロードが行われるため、横画面にしても最適なバナーサイズの広告が表示されます。

![Simulator Screenshot - iPhone 15 - 2023-11-17 at 08.43.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/687960/5a2c75da-3193-5e77-5fa1-c3ae06115d3b.png)


## Bannerの問題点
しかし、このコードにはいくつかの不具合があります。

### バナーが最下部に表示されない

```swift
    var body: some View {
        VStack {
            Text("Contents")
            Spacer()
            BannerView()
        }
    }
```

例えば上記のコードを用意します。
これは画面上部にコンテンツを用意し、最下部にバナー表示を意図したコードにしていますが、実際はコンテンツにくっつく形で表示されます。

![Simulator Screenshot - iPhone 15 - 2023-11-17 at 08.57.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/687960/baf74def-206e-86c7-8d7a-d0fb0e6b14cc.png)

これを防ぐために、BannerViewのmakeUIViewControllerにNSLayoutConstraintで制約を追加することで対応します。
[公式のGithub](https://github.com/googleads/googleads-mobile-ios-examples/blob/d1cabf7217be917f4d6506f8b750ae1272e78ba5/Swift/advanced/SwiftUIDemo/SwiftUIDemo/Banner/BannerContentView.swift#L24-L40)でもこの対応がとられています。


```swift
    func makeUIViewController(context: Context) -> some UIViewController {
        let bannerViewController = BannerViewController()
        bannerView.adUnitID = adUnitID
        bannerView.rootViewController = bannerViewController
        bannerView.delegate = context.coordinator
        bannerView.translatesAutoresizingMaskIntoConstraints = false
        bannerViewController.view.addSubview(bannerView)
        // Constrain GADBannerView to the bottom of the view.
        NSLayoutConstraint.activate([
            bannerView.bottomAnchor.constraint(
                equalTo: bannerViewController.view.safeAreaLayoutGuide.bottomAnchor),
            bannerView.centerXAnchor.constraint(equalTo: bannerViewController.view.centerXAnchor),
        ])
        bannerViewController.delegate = context.coordinator
        
        return bannerViewController
    }
```

### iPadで広告が表示されない
https://developers.google.com/admob/ios/multiscene?hl=ja

新規のアプリをSwiftUIで作成した場合、Scene対応されることになりますが、その場合、iPadでバナー広告が表示されません。
GADRequestにwindowSceneを設定する必要があります。

[AppDelegateとSceneDelegateをEnvironmentObjectに対応する方法](https://developer.apple.com/documentation/swiftui/uiapplicationdelegateadaptor)があるので、SceneDelegate経由でwindowSceneを取得することで表示が可能です。

以下、AppDelegateも含めたSceneDelegateを用意しています。

```swift 
import SwiftUI
import GoogleMobileAds

@main
struct MyApp: App {
    @UIApplicationDelegateAdaptor(MyAppDelegate.self) var appDelegate
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

class MyAppDelegate: NSObject, UIApplicationDelegate, ObservableObject {
    func application(_ application: UIApplication, didFinishLaunchingWithOptions
                     launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
        GADMobileAds.sharedInstance().start(completionHandler: nil)
        return true
    }
}

class MySceneDelegate: NSObject, UIWindowSceneDelegate, ObservableObject {
    var windowScene: UIWindowScene?
    var window: UIWindow? {
        windowScene?.keyWindow
    }

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        windowScene = scene as? UIWindowScene
    }
}

extension MyAppDelegate {
    func application(
        _ application: UIApplication,
        configurationForConnecting connectingSceneSession: UISceneSession,
        options: UIScene.ConnectionOptions
    ) -> UISceneConfiguration {
        let configuration = UISceneConfiguration(
            name: nil,
            sessionRole: connectingSceneSession.role)
        if connectingSceneSession.role == .windowApplication {
            configuration.delegateClass = MySceneDelegate.self
        }
        return configuration
    }
}
```

用意したsceneDelegateをGADRequestに設定することでiPadでも広告が表示できるようになりました。

```swift
struct BannerView: UIViewControllerRepresentable {
    @State private var viewWidth: CGFloat = .zero
    // SceneDelegateを追加
    @EnvironmentObject private var sceneDelegate: MySceneDelegate
    private let bannerView = GADBannerView()
    private let adUnitID = "ca-app-pub-3940256099942544/2934735716"

    ...
    
    func updateUIViewController(_: UIViewControllerType, context _: Context) {
        guard viewWidth != .zero else { return }
        
        // Request a banner ad with the updated viewWidth.
        bannerView.adSize = GADCurrentOrientationAnchoredAdaptiveBannerAdSizeWithWidth(viewWidth)
        let request = GADRequest()
        request.scene = sceneDelegate.windowScene
        bannerView.load(request)
    }
    
    ...
}
```

![Simulator Screenshot - iPad (10th generation) - 2023-11-17 at 09.23.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/687960/9d5f8eb5-d481-5f4c-50af-4d5c7619982d.png)