AdMobのNative AdsをSwiftUIで表示する

## はじめに

2019年11月20日現在、英日でググっても AdMob の Native Ads を SwiftUI で表示する記事はないので同じことをやろうとする人のためにやり方を書いておこうと思う。


## AdMobの用意

以下のドキュメントを読んで Xcode のプロジェクトが AdMob にリクエストができる状態にする。
[Get Started  |  Firebase](https://firebase.google.com/docs/admob/ios/quick-start)

## Native Templates iOSをプロジェクトに取り込む

以下のリンクから Google が用意してる native ads のテンプレートを `git clone` するかダウンロードして Xcode のプロジェクトに nativetemplates folder 以下のソースコードと xib file を追加する。

[Native Templates iOS](https://github.com/googleads/googleads-mobile-ios-native-templates)

ソースコードと xib file を Xcode に追加するときの注意点はフォルダーをドラッグせずにソードコードとxibのファイルを選択してドラッグしないとビルドターゲットにそれらのファイルが入らないのと Bridging-Header を作るかどうかを Xcode が聞いてこない。

テンプレートのコードは Objective-C なので Swift 用の Bridging-Header を用意する。

Bridging-Header で以下のように import しておく。

```_project_name_-Bridging-Header.h
//
//  Use this file to import your target's public headers that you would like to expose to Swift.
//

#import "GADTMediumTemplateView.h"
#import "GADTSmallTemplateView.h"
#import "GADTTemplateView.h"

```

## SwiftUIのNative Adsを表示する view を作る

まず GADTMediumTemplateView を表示する UIViewController を SwiftUI に持ってくるために UIViewControllerRepresentable を使って NativeAdsViewController を作る。


GADUnifiedNativeAdLoaderDelegate に適応するために NSObject を継承した NativeAdsViewController を用意して、さらに UIViewControllerRepresentable に適応する。

```swift
final class NativeAdsViewController: NSObject, UIViewControllerRepresentable {}
```

`makeUIViewController(context:)` を実装していく。
UIViewController を用意してその view に GADTMediumTemplateView を追加していく。

```swift
func makeUIViewController(context: UIViewControllerRepresentableContext<NativeAdsViewController>) -> UIViewController {

    let templateView = GADTMediumTemplateView()
    self.templateView = templateView
    
    let viewController = UIViewController()

    viewController.view.addSubview(templateView)
    templateView.addHorizontalConstraintsToSuperviewWidth()
    templateView.addVerticalCenterConstraintToSuperview()

    return viewController
}
    
```

GADAdLoader で広告をロードする。

```swift
func makeUIViewController(context: UIViewControllerRepresentableContext<NativeAdsViewController>) -> UIViewController {
      
      let templateView = GADTMediumTemplateView()
      self.templateView = templateView
      
      let viewController = UIViewController()
      
      viewController.view.addSubview(templateView)
      templateView.addHorizontalConstraintsToSuperviewWidth()
      templateView.addVerticalCenterConstraintToSuperview()
      
      #if DEBUG
      let nativeAdvanced = "ca-app-pub-3940256099942544/3986624511"
//        let nativeAdvancedVideo = "ca-app-pub-3940256099942544/2521693316"
      
      let adUnitID = nativeAdvanced
      #else
      let adUnitID = self.adUnitID
      #endif
      
      let rootViewController = UIApplication.shared.windows.first?.rootViewController
      let loader = GADAdLoader(adUnitID: adUnitID, rootViewController: rootViewController, adTypes: [GADAdLoaderAdType.unifiedNative], options: nil)
      loader.delegate = self
      
      adLoader = loader
      
      loader.load(GADRequest())
      
      return viewController
}
```

GADUnifiedNativeAdLoaderDelegate の adLoader(_:didReceive nativeAd:) で受け取った広告を templateView に渡して広告を view に表示する。
これで広告は表示される。

```swift
extension NativeAdsViewController: GADUnifiedNativeAdLoaderDelegate {
    
    func adLoader(_ adLoader: GADAdLoader, didReceive nativeAd: GADUnifiedNativeAd) {
        templateView?.nativeAd = nativeAd
    }
    
    func adLoader(_ adLoader: GADAdLoader, didFailToReceiveAdWithError error: GADRequestError) {}
}
```

しかし、今の NativeAdsViewController を SwiftUI の ScrollView に入れると高さが0になってしまう。
GADTMediumTemplateView は auto layout でレイアウトされていて、view の高さは広告のテキスト、mediaView の16:9のアスペクト比などから計算されるので一意に view のアスペクト比や高さを決めることができないので NativeAdsViewController を横方向に最大限拡がって高さをそれに合わせて fit するように NativeAdsViewController に frame() をセットするために auto layout で計算された view の高さを使う必要がある。


まず GADTMediumTemplateView のサイズを SwiftUI に持ってくるために SizeModel: ObservableObject を定義する。
ObservableObject にしてるのは height が更新された時に SwiftUI に広告の view を再レンダしてもらうためだ。

```swift
final class SizeModel: ObservableObject {
    @Published var width: CGFloat = 355
    @Published var height: CGFloat = 370
}
```

次に templateView: GADTMediumTemplateView? の \.bounds の変化を Combine で subscribe して sizeModel を更新していく。
subscribeUpdateSizeModel() を makeUIViewController(context:) 内で呼んでおく。

```swift
private func subscribeUpdateSizeModel() {
    templateView?.publisher(for: \.bounds)
        .sink(receiveValue: { (bounds) in
            self.sizeModel.width = bounds.width
            self.sizeModel.height = bounds.height
        })
        .store(in: &cancellables)
}
```

次に NativeAdsView.swift を用意する。
ここで NativeAdsViewController.SizeModel を @ObservedObject として持ち、sizeModel.height  を NativeAdsViewController に frame() で適用する。
これで auto layout された高さの値が適用される。

```NativeAdsView.swift
import SwiftUI

struct NativeAdsView: View {
    
    let adUnitID: String
    private @ObservedObject var sizeModel = NativeAdsViewController.SizeModel()
    
    var body: some View {
        NativeAdsViewController(adUnitID: adUnitID, sizeModel: sizeModel)
            .frame(height: sizeModel.height)
    }
}

struct NativeAdsView_Previews: PreviewProvider {
    static var previews: some View {
        NativeAdsView(adUnitID: "ca-app-pub-3940256099942544/3986624511")
    }
}

```

![Screen Shot.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/536067/9c598f07-3c8c-4d33-91b8-a1c1e0d926e6.png)


### NativeAdsViewController.swift

```NativeAdsViewController.swift
import SwiftUI
import Combine

final class NativeAdsViewController: NSObject, UIViewControllerRepresentable {
    
    final class SizeModel: ObservableObject {
        @Published var width: CGFloat = 355
        @Published var height: CGFloat = 370
    }
    
    var adLoader: GADAdLoader?
    var templateView: GADTMediumTemplateView?
    
    let adUnitID: String
    let sizeModel: SizeModel
    
    private var cancellables = Set<AnyCancellable>()
    
    init(adUnitID: String, sizeModel: SizeModel) {
        self.adUnitID = adUnitID
        self.sizeModel = sizeModel
    }

    func makeUIViewController(context: UIViewControllerRepresentableContext<NativeAdsViewController>) -> UIViewController {
        
        let templateView = GADTMediumTemplateView()
        self.templateView = templateView
        
        let viewController = UIViewController()
        
        viewController.view.addSubview(templateView)
        templateView.addHorizontalConstraintsToSuperviewWidth()
        templateView.addVerticalCenterConstraintToSuperview()
        
        subscribeUpdateSizeModel()
        
        #if DEBUG
        let nativeAdvanced = "ca-app-pub-3940256099942544/3986624511"
//        let nativeAdvancedVideo = "ca-app-pub-3940256099942544/2521693316"
        
        let adUnitID = nativeAdvanced
        #else
        let adUnitID = self.adUnitID
        #endif
        
        let rootViewController = UIApplication.shared.windows.first?.rootViewController
        let loader = GADAdLoader(adUnitID: adUnitID, rootViewController: rootViewController, adTypes: [GADAdLoaderAdType.unifiedNative], options: nil)
        loader.delegate = self
        
        adLoader = loader
        
        loader.load(GADRequest())
        
        return viewController
    }

    func updateUIViewController(_ uiViewController: UIViewController, context: UIViewControllerRepresentableContext<NativeAdsViewController>) {}
    
    private func subscribeUpdateSizeModel() {
        templateView?.publisher(for: \.bounds)
            .sink(receiveValue: { (bounds) in
                self.sizeModel.width = bounds.width
                self.sizeModel.height = bounds.height
            })
            .store(in: &cancellables)
    }
}

extension NativeAdsViewController: GADUnifiedNativeAdLoaderDelegate {
    
    func adLoader(_ adLoader: GADAdLoader, didReceive nativeAd: GADUnifiedNativeAd) {
        templateView?.nativeAd = nativeAd
    }
    
    func adLoader(_ adLoader: GADAdLoader, didFailToReceiveAdWithError error: GADRequestError) {}
}

```

## おわりに
最後まで読んでくださってありがとうございます。

UIViewControllerRepresentable の内包された view  の auto layout で変化する size を簡単で綺麗に ideal size にできないのが今のところ不便ですね。
もしもっと綺麗な方法があるなら教えてください。