SwiftUIでAdMobネイティブ広告を表示する

# 重要!
[SwiftUIの公式ドキュメント](https://developers.google.com/admob/ios/swiftui?hl=ja#native_ads)が出ました。
以下の記述は公式ドキュメントが出る前の内容になりますので、ご認識ください。


# 要約

SwiftUIで`UIViewRepresentable`を用いてAdMobのネイティブ広告を表示します。

# はじめに
SwiftUIでAdMobのネイティブ広告を表示します。いわゆるSwiftUI life cycleでの表示例です。
AdMobのネイティブを含む広告はSwiftUIではサンプルがないので、表示できた実装例を示します。
公式での実装ではないので何かしら問題が起こる可能性があります。
AdMobの導入が済んでいる事が前提となります。
https://developers.google.com/admob/ios/quick-start?hl=ja

# コードと説明
## App

AdMobはAppDelegateでの初期化が必要なので、Appで`UIApplicationDelegateAdaptor`を実装してAdMobの初期化対応を行います。

後程SceneDelegateの対応も必要になるので実装します。
SceneDelegateではwindowやwindowSceneを取得できるように用意しておきます。

このAppDelegateやSceneDelegateの実装は公式の実装を参考にしています。

https://developer.apple.com/documentation/swiftui/uiapplicationdelegateadaptor

```swift MyApp.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
        window = windowScene?.keyWindow
    }
}

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
    }
}
```

## ContentView.swift
実際に表示するView部分では、こちらのコード例では広告のロードを表示時に行い、広告用のデータ`GADNativeAd`が取得できたらネイティブ広告を表示するようにしています。
ネイティブ広告のロード周りはNativeAdModelとしてObservableObjectで実装して、結果を受け取るようにしています。

ポイントとしては`EnvironmentObject`としてsceneDelegateを定義している部分で、先ほどのUIApplicationDelegateAdaptorのドキュメントに`App delegateの実装にObservableObjectが実装されていれば、EnvironmentObjectとして使える` というものがあります。

> If your app delegate conforms to the ObservableObject protocol, as in the example above, then SwiftUI puts the delegate it creates into the Environment. You can access the delegate from any scene or view in your app using the EnvironmentObject property wrapper

sceneDelegateも同様にEnvironmentObjectとして使えるので、UIWindowSceneを持たせて利用をしています。

> As with the app delegate, if you make your scene delegate an observable object, SwiftUI automatically puts it in the Environment, from where you can access it with the EnvironmentObject property wrapper, and create bindings to its published properties.


```swift ContentView.swift
import SwiftUI
import GoogleMobileAds

struct ContentView: View {
    @EnvironmentObject private var sceneDelegate: MySceneDelegate
    @StateObject private var model = NativeAdModel()
    var body: some View {
        VStack {
            Text("Native Ad")
            if let nativeAd = model.nativeAd {
                NativeAdView(nativeAd: nativeAd)
            }
        }
        .onAppear(perform: loadAd)
    }
    
    private func loadAd() {
        model.load(windowScene: sceneDelegate.windowScene, rootViewController: sceneDelegate.window?.rootViewController)
    }
}

class NativeAdModel: NSObject, ObservableObject, GADNativeAdLoaderDelegate {
    @Published var nativeAd: GADNativeAd?
    private var adLoader: GADAdLoader?
    
    func load(windowScene: UIWindowScene?,
              rootViewController: UIViewController?) {
        let adLoader = GADAdLoader(adUnitID: "ca-app-pub-3940256099942544/3986624511", rootViewController: rootViewController, adTypes: [.native], options: nil)
        self.adLoader = adLoader
        adLoader.delegate = self
        let request = GADRequest()
        request.scene = windowScene
        adLoader.load(request)
    }
    
    func adLoader(_ adLoader: GADAdLoader, didReceive nativeAd: GADNativeAd) {
        self.nativeAd = nativeAd
    }
    
    func adLoader(_ adLoader: GADAdLoader, didFailToReceiveAdWithError error: Error) {
    }
}
```

## NativeAdView.swift (UIViewRepresentable)
最後に広告部分をUIViewRepresentableで実装します。
GADNativeAdを受け取ってViewを実装します。
基本的には、AdMobの[広告を表示する](https://developers.google.com/admob/ios/native/advanced?hl=ja#displaying_the_ad)実装と同じく、xibからの生成から各種パラメータの設定を行なっています。


### 注意点
xcframeworkに含まれているIBOutlet付きのヘッダーはInterface builderで認識できず、アウトレット接続ができない問題があるようです。
GADNativeAdViewに関しても同様に問題があり、自分のプロジェクトにGADNativeAdView.hをコピーするのが対処法になるようです。
https://groups.google.com/g/google-admob-ads-sdk/c/YKCW9Be2_cs/m/FScvu4XnAQAJ

```swift NativeAdView.swift
import GoogleMobileAds

struct NativeAdView: UIViewRepresentable {
    let nativeAd: GADNativeAd
    func makeUIView(context: Context) -> GADNativeAdView {
        let nativeAdView: GADNativeAdView = Bundle.main.loadNibNamed("NativeAdView", owner: nil, options: nil)?.first as! GADNativeAdView

        (nativeAdView.bodyView as? UILabel)?.text = nativeAd.body
        nativeAdView.bodyView?.isHidden = nativeAd.body == nil

        (nativeAdView.callToActionView as? UIButton)?.setTitle(nativeAd.callToAction, for: .normal)
        nativeAdView.callToActionView?.isHidden = nativeAd.callToAction == nil

        (nativeAdView.iconView as? UIImageView)?.image = nativeAd.icon?.image
        nativeAdView.iconView?.isHidden = nativeAd.icon == nil

        (nativeAdView.storeView as? UILabel)?.text = nativeAd.store
        nativeAdView.storeView?.isHidden = nativeAd.store == nil

        (nativeAdView.priceView as? UILabel)?.text = nativeAd.price
        nativeAdView.priceView?.isHidden = nativeAd.price == nil

        (nativeAdView.advertiserView as? UILabel)?.text = nativeAd.advertiser
        nativeAdView.advertiserView?.isHidden = nativeAd.advertiser == nil

        nativeAdView.callToActionView?.isUserInteractionEnabled = false

        nativeAdView.nativeAd = nativeAd
        
        return nativeAdView
    }
    
    func updateUIView(_ uiView: GADNativeAdView, context: Context) {
        
    }
}
```


# 結果
こんな感じでネイティブ広告が表示されます。
![Simulator Screen Shot - iPhone 14 - 2022-11-21 at 17.11.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/687960/edae5ac8-4dfa-22e8-8ada-21a64ed31acf.png)

iPadに関しても表示が確認できます。
![Simulator Screen Shot - iPad mini (6th generation) - 2022-11-21 at 17.12.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/687960/05163b79-3f34-100a-55dd-5f84481e7e3c.png)