Flutter で AdMob ネイティブ広告を実装する (iOS)

# やりたいこと

Flutter で AdMob のネイティブ広告(Native Ad)を導入したい。  
[codelabs](https://codelabs.developers.google.com/codelabs/admob-inline-ads-in-flutter#7) にわかりやすく実装手順がまとまっているが、いくつか詰まったので共有。

# 環境
* flutter: 3.3.9
* google_mobile_ads: ^2.3.0

# 実装

基本的には [codelab](https://codelabs.developers.google.com/codelabs/admob-inline-ads-in-flutter#0) を順当に進め、最後の “**iOS 用の NativeAdFactory を実装する (Swift)”** を見ればいける。
ただ、そのまま実装してもエラーになってしまう。
とりまちゃっちゃと実装して以下のエラーに会いに行こう。

### info.plist に SKAdNetWork の設定が必要
まず実行すると、`SKAdNetWork` の設定が `info.plist` にないぞって言われる。
[こちらの記事](https://developers.google.com/admob/ios/ios14#skadnetwork)を確認し `SKAdNetworkItems` を `info.plist` にコピペすれば OK。
やたら多いけど全部必要なのでコピペしよう。

### テスト用を使う場合は AppDelegate に追記が必要

次は、実機で実行する。以下のようなエラー文が表示されるはず。

```swift
GADMobileAds.sharedInstance().requestConfiguration.testDeviceIdentifiers = [ "任意のデバイスID?" ]
```

表示通りに AppDelegate に追記してあげる

```swift
import UIKit
import Flutter

import google_mobile_ads

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    GeneratedPluginRegistrant.register(with: self)
    
    let ad = NativeAdPostThumbnail()
    FLTGoogleMobileAdsPlugin.registerNativeAdFactory(
        self, factoryId: "admobPostThumbnail", nativeAdFactory: ad)
    
    // これを追記
    GADMobileAds.sharedInstance().requestConfiguration.testDeviceIdentifiers = [ "任意のデバイスID?" ]
    // ここまで

    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }
}
```

これで何故か実機はもちろんエミュでもちゃんと動くようになる。よくわからん。
これでも問題があったら、[こちらのスレッド](https://groups.google.com/g/google-admob-ads-sdk/c/0OWK5_4ZGMY?pli=1)を見る限り [AdMob UI](https://apps.admob.com/v2/settings/test-devices/list) 上から直接デバイスIDを登録すればよさそう。

### ListTileNativeAdView.xib に GADUnifiedNativeAdView クラスを適切に接続してあげる

また実行すると、`GADUnifiedNativeAdView` が見つからない的なエラーが出る。
CodeLab の `ListTileNativeAdView.xib` は Custom Class で `GADUnifiedNativeAdView` を参照しているが、どうやら見つからないっぽい。
実際試してみても `GADUnifiedNativeAdView` が出てこない...

![スクリーンショット 2022-12-16 11.10.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/516428/514e4a4a-65ba-4a3e-9eea-98f334839e0d.png)


というわけで、[こちらの回答](https://stackoverflow.com/a/69314572/14349984)をもとに `GADNativeAd.h` というヘッダーファイルを作成。

![スクリーンショット 2022-12-16 11.16.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/516428/f2107f70-541d-6ebe-6082-057d9d74b058.png)

あとは回答のコメントにある[こちらの記事](https://pastebin.com/YVf96Tzu)の中身をコピペ。一応以下にも示す。

```objc
//
//  GADNativeAd.h
//  Google Mobile Ads SDK
//
//  Copyright 2017 Google LLC. All rights reserved.
//

#ifndef GADNativeAd_h
#define GADNativeAd_h

#endif /* GADNativeAd_h */

#import <Foundation/Foundation.h>
#import <GoogleMobileAds/GADAdChoicesView.h>
#import <GoogleMobileAds/GADAdLoaderDelegate.h>
#import <GoogleMobileAds/GADAdValue.h>
#import <GoogleMobileAds/GADMediaContent.h>
#import <GoogleMobileAds/GADMediaView.h>
#import <GoogleMobileAds/GADMuteThisAdReason.h>
#import <GoogleMobileAds/GADNativeAdAssetIdentifiers.h>
#import <GoogleMobileAds/GADNativeAdDelegate.h>
#import <GoogleMobileAds/GADNativeAdImage.h>
#import <GoogleMobileAds/GADResponseInfo.h>
#import <GoogleMobileAds/GADVideoController.h>
#import <UIKit/UIKit.h>

/// Native ad. To request this ad type, pass kGADAdLoaderAdTypeNative
/// (see GADAdLoaderAdTypes.h) to the |adTypes| parameter in GADAdLoader's initializer method. If
/// you request this ad type, your delegate must conform to the GADNativeAdLoaderDelegate
/// protocol.
@interface GADNativeAd : NSObject

#pragma mark - Must be displayed if available

/// Headline.
@property(nonatomic, readonly, copy, nullable) NSString *headline;

#pragma mark - Recommended to display

/// Text that encourages user to take some action with the ad. For example "Install".
@property(nonatomic, readonly, copy, nullable) NSString *callToAction;
/// Icon image.
@property(nonatomic, readonly, strong, nullable) GADNativeAdImage *icon;
/// Description.
@property(nonatomic, readonly, copy, nullable) NSString *body;
/// Array of GADNativeAdImage objects.
@property(nonatomic, readonly, strong, nullable) NSArray<GADNativeAdImage *> *images;
/// App store rating (0 to 5).
@property(nonatomic, readonly, copy, nullable) NSDecimalNumber *starRating;
/// The app store name. For example, "App Store".
@property(nonatomic, readonly, copy, nullable) NSString *store;
/// String representation of the app's price.
@property(nonatomic, readonly, copy, nullable) NSString *price;
/// Identifies the advertiser. For example, the advertiser’s name or visible URL.
@property(nonatomic, readonly, copy, nullable) NSString *advertiser;
/// Media content. Set the associated media view's mediaContent property to this object to display
/// this content.
@property(nonatomic, readonly, nonnull) GADMediaContent *mediaContent;

#pragma mark - Other properties

/// Optional delegate to receive state change notifications.
@property(nonatomic, weak, nullable) id<GADNativeAdDelegate> delegate;

/// Reference to a root view controller that is used by the ad to present full screen content after
/// the user interacts with the ad. The root view controller is most commonly the view controller
/// displaying the ad.
@property(nonatomic, weak, nullable) UIViewController *rootViewController;

/// Dictionary of assets which aren't processed by the receiver.
@property(nonatomic, readonly, copy, nullable) NSDictionary<NSString *, id> *extraAssets;

/// Information about the ad response that returned the ad.
@property(nonatomic, readonly, nonnull) GADResponseInfo *responseInfo;

/// Called when the ad is estimated to have earned money. Available for allowlisted accounts only.
@property(nonatomic, nullable, copy) GADPaidEventHandler paidEventHandler;

/// Indicates whether custom Mute This Ad is available for the native ad.
@property(nonatomic, readonly, getter=isCustomMuteThisAdAvailable) BOOL customMuteThisAdAvailable;

/// An array of Mute This Ad reasons used to render customized mute ad survey. Use this array to
/// implement your own Mute This Ad feature only when customMuteThisAdAvailable is YES.
@property(nonatomic, readonly, nullable) NSArray<GADMuteThisAdReason *> *muteThisAdReasons;

/// Registers ad view, clickable asset views, and nonclickable asset views with this native ad.
/// Media view shouldn't be registered as clickable.
/// @param clickableAssetViews Dictionary of asset views that are clickable, keyed by asset IDs.
/// @param nonclickableAssetViews Dictionary of asset views that are not clickable, keyed by asset
///        IDs.
- (void)registerAdView:(nonnull UIView *)adView
       clickableAssetViews:
           (nonnull NSDictionary<GADNativeAssetIdentifier, UIView *> *)clickableAssetViews
    nonclickableAssetViews:
        (nonnull NSDictionary<GADNativeAssetIdentifier, UIView *> *)nonclickableAssetViews;

/// Unregisters ad view from this native ad. The corresponding asset views will also be
/// unregistered.
- (void)unregisterAdView;

/// Reports the mute event with the mute reason selected by user. Use nil if no reason was selected.
/// Call this method only if customMuteThisAdAvailable is YES.
- (void)muteThisAdWithReason:(nullable GADMuteThisAdReason *)reason;

@end

#pragma mark - Protocol and constants

/// The delegate of a GADAdLoader object implements this protocol to receive GADNativeAd ads.
@protocol GADNativeAdLoaderDelegate <GADAdLoaderDelegate>
/// Called when a native ad is received.
- (void)adLoader:(nonnull GADAdLoader *)adLoader didReceiveNativeAd:(nonnull GADNativeAd *)nativeAd;
@end

#pragma mark - Unified Native Ad View

/// Base class for native ad views. Your native ad view must be a subclass of this class and must
/// call superclass methods for all overridden methods.
@interface GADNativeAdView : UIView

/// This property must point to the native ad object rendered by this ad view.
@property(nonatomic, strong, nullable) GADNativeAd *nativeAd;

/// Weak reference to your ad view's headline asset view.
@property(nonatomic, weak, nullable) IBOutlet UIView *headlineView;
/// Weak reference to your ad view's call to action asset view.
@property(nonatomic, weak, nullable) IBOutlet UIView *callToActionView;
/// Weak reference to your ad view's icon asset view.
@property(nonatomic, weak, nullable) IBOutlet UIView *iconView;
/// Weak reference to your ad view's body asset view.
@property(nonatomic, weak, nullable) IBOutlet UIView *bodyView;
/// Weak reference to your ad view's store asset view.
@property(nonatomic, weak, nullable) IBOutlet UIView *storeView;
/// Weak reference to your ad view's price asset view.
@property(nonatomic, weak, nullable) IBOutlet UIView *priceView;
/// Weak reference to your ad view's image asset view.
@property(nonatomic, weak, nullable) IBOutlet UIView *imageView;
/// Weak reference to your ad view's star rating asset view.
@property(nonatomic, weak, nullable) IBOutlet UIView *starRatingView;
/// Weak reference to your ad view's advertiser asset view.
@property(nonatomic, weak, nullable) IBOutlet UIView *advertiserView;
/// Weak reference to your ad view's media asset view.
@property(nonatomic, weak, nullable) IBOutlet GADMediaView *mediaView;
/// Weak reference to your ad view's AdChoices view. Must set adChoicesView before setting
/// nativeAd, otherwise AdChoices will be rendered according to the preferredAdChoicesPosition
/// defined in GADNativeAdViewAdOptions.
@property(nonatomic, weak, nullable) IBOutlet GADAdChoicesView *adChoicesView;

@end
```
    

んで、`ListTileNativeAdView.xib` に戻って Custom Class に `GADNativeAdView` ってのが出てくるようになるはずなので、それを指定。
これで完了。

# 完成
![スクリーンショット 2022-12-16 11.04.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/516428/32b54bf4-e694-abfc-1aec-77255da0ea56.png)