はじめに
- パブリックベータの審査ではダメ出しされなかったので、対応が必要なことに気が付いておらず、いざ製品リリースしようとしたらリジェクトされて焦りました。
- この記事は、審査を通すまでに行った調査や対処をまとめたものです。
この記事でできること
- Unityで作ったAdMob広告付きアプリのiOS14対応
- AppTrackingTransparency(ATT)を導入してIDFAの取得を試み、不許可ならSKAdNetworkが使われるようにします。
- ATTの説明文をローカライズします。
- Unity側の作業のみで実現し、Xcodeでの手作業を不要にします。
- Unityエディタの使い方や、Google Mobile Ads Unity PluginやUnity Data Privercyの導入、Xcodeでの作業を含むiOSビルドの方法などについては、この記事では扱いません。
前提
- Xcode 12.5.1
- iOS 14.5 supported
- Unity 2018.4.x (LTS), 追加確認済み 2021.3.2f1 (LTS)
- Unity Analytics を使用
- Unity IAP を使用
- Google Mobile Ads Unity Plugin v5.4.0, 追加確認済み v6.1.2
- Google Mobile Ads iOS SDK 7.68.0
状況の理解
- Appleが2020年末にポリシーを変更して、AppTrackingTransparencyとSKAdNetworkが導入されました。
- 2021年4月26日にiOS 14.5がリリースされ、それまでオプトアウト方式だったIDFAの取得許可がオプトインに切り替わりました。
- IDFAを使用する場合は、プライバシーラベルにその旨を記載する必要があり、アプリ内でATTを介してダイアログを表示してユーザーの許諾を得る必要があります。
- プライバシーラベルに「トラッキング」がある場合は、アプリがATTを使用してユーザーから許諾を得るようになっていないと、審査でリジェクトされます。
- IDFAを使用せずにSKAdNetworkだけを使用する場合は、プライバシーラベルでトラッキングの記載をなくすことができ、ATTを使用して許可を得る必要がなくなります。
用語と概念
IDFA(Identifier for Advertisers)
従来様式の広告トラッキング用のIDで、SKAdNetworkに比較して追跡性能が高いです。ユーザーは使用許諾やリセットなどをコントロール可能です。
ATT (App Tracking Transparency)
IDFAを使用するにあたって、ユーザーに許可を得る仕組みです。
ユーザの選択結果はOSで記憶され、ユーザが再度確認を求められることはありません。
SKAdNetwork
IDFAを使用せずに、プライバシーに配慮しつつ広告の最適化を行うための仕組みで、IDFAに比較して追跡性能が低いようです。
IDFAの使用許可が得られない場合は、自動的に使用されます。
プライバシーラベル
App Store Connectの「Appのプライバシー」で設定し、ストアに表示される個人情報の取り扱いに関する定型の情報表示です。
可能な選択肢
IDFAを使用しない
- Appのプライバシーで「トラッキング」を申告する必要も、ATTを使用する必要もありません。
- AdMobに対して特別な制御は不要です。
- ATTを使わないので、IDFAを得ることはできません。
- IDFAが得られないので、自動的にSKAdNetworkが使用されます。
IDFAの使用許可を求める
- Appのプライバシーで「トラッキング」を申告しなければなりません。
- ATTを使用してIDFAの使用許諾を求める必要があります。
- システムの許可要請ダイアログを表示する前に、あらかじめアプリ側で説明を表示することも考えられます。
- ATTの結果に拠らず、AdMobに対して特別な制御は不要です。
- ユーザーの許可が無い限り、IDFAを得ることはできません。
- IDFAが得られなかった場合は、自動的にSKAdNetworkが使用されます。
具体的な対処
以降は、「IDFAの使用許可を求める」場合について述べます。
SKAdNetwork 対応
-
info.plist
へ該当のIDを登録することでアクティベートされるようです。 - 「ビルドの都度に手動で登録する、あるいは自動的に登録するコードを実装する」記事と、「AdMob SDKが自動的に登録する」と主張する記事が混在しています。
- 古いSDKではできなかったのかとも考えましたが、より新しい記事で自前の実装を行っているようです。
-
Google Mobile Ads Unity Plugin v5.4.0には、自動的に
info.plist
に挿入されると書かれています。 - 実際に試してみたところ、何もしなくても自動的に登録されていました。
- 他のIDも登録する場合は、対処が必要という話なのでしょうか。
結論
- 特に対処は不要でした。
AppTrackingTransparency 対応
-
AdSupport.framework
およびAppTrackingTransparency.framework
を手動で導入する必要があると書かれている記事がありましたが、前者は何もしなくても導入されていました。-
PostProcessBuild
で、PBXProject.AddFrameworkToProject
を使用することでXcodeプロジェクトに導入できます。
-
- ATTを呼び出すと、IDFAの使用許可を求めるシステムダイアログが表示されます。
- 呼び出す際に説明文を渡すようになっていて、そのローカライズが必要になります。
ネイティブプラグインで ATT を呼び出す
#import <Foundation/Foundation.h>
#import <AppTrackingTransparency/ATTrackingManager.h>
extern "C" {
/// <summary>ATT 許可状態取得</summary>
/// <return>
/// ATTracking Manager.Authorization Status
/// 0: Not Determined, 1: Restricted, 2: Denied, 3: Authorized (, -1: No Needs)
/// https://developer.apple.com/documentation/apptrackingtransparency/attrackingmanager/authorizationstatus
/// </return>
int GetTrackingAuthorizationStatus() {
if (@available(iOS 14, *)) {
return (int)ATTrackingManager.trackingAuthorizationStatus;
} else {
return -1;
}
}
/// <summary>コールバック型</summary>
/// <param name="status">ATTracking Manager.Authorization Status</param>
typedef void (*Callback)(int status);
/// <summary>ATT 許可要求</summary>
/// <param name="callback">コールバック関数</param>
void RequestTrackingAuthorization(Callback callback) {
if (@available(iOS 14, *)) {
[ATTrackingManager requestTrackingAuthorizationWithCompletionHandler:^(ATTrackingManagerAuthorizationStatus status) {
if (callback != nil) {
callback((int)status);
}
}];
} else {
callback(-1);
}
}
}
Appleのフレームワークを呼び出すネイティブコードです。
参考
プラグインを呼び出してコールバックから結果を得る
#if UNITY_IOS
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
public static class AttService {
private static TaskCompletionSource<bool> AttTcs; // boolを返すタスク
private static SynchronizationContext Context; // Unityのスレッドの同期環境を保持
/// <summary>クラス初期化</summary>
static AttService () {
Context = SynchronizationContext.Current;
}
/// <summary>ATT 許可状態取得 プラグイン</summary>
[DllImport ("__Internal")]
private static extern int GetTrackingAuthorizationStatus ();
/// <summary>コールバック型</summary>
private delegate void OnCompleteStatusCallback (int status);
/// <summary>ATT 許可要求 プラグイン</summary>
/// <param name="callback">コールバック関数</param>
[DllImport ("__Internal")]
private static extern void RequestTrackingAuthorization (OnCompleteStatusCallback callback);
/// <summary>ATT の同意状態を取得する</summary>
/// <return>true: Authorized or No Needs, false: Denied or Restricted, null: Not Determined</return>
public static bool? GetIOSTrackingAuthorizationStatus () {
#if UNITY_EDITOR
return true;
#else
switch (GetTrackingAuthorizationStatus ()) {
case -1: // No Needs
case 3: // Authorized
return true;
case 0: // Not Determined
return null;
default:
return false;
}
#endif
}
/// <summary>ATT 許可を要求する</summary>
/// <return>(Authorized or No Needs)なら真を結果とする非同期タスク</return>
public static Task<bool> RequestTrackingAuthorization () {
#if UNITY_EDITOR
return Task.FromResult (true);
#else
AttTcs = new TaskCompletionSource<bool> ();
RequestTrackingAuthorization (OnRequestTrackingAuthorizationComplete);
return AttTcs.Task;
#endif
}
/// <summary>コールバックハンドラ</summary>
[AOT.MonoPInvokeCallback (typeof (OnCompleteStatusCallback))]
private static void OnRequestTrackingAuthorizationComplete (int status) {
if (AttTcs != null) {
Context.Post (_ => {
switch (status) {
case -1: // No Needs
case 3: // Authorized
AttTcs.TrySetResult (true);
break;
default:
AttTcs.TrySetResult (false);
break;
}
}, null);
}
}
}
#endif
C#で記述された、ネイティブ・プラグインのラッパーです。
参考
ATT を介してユーザーから許可を得る
#if UNITY_IOS
var status = AttService.GetIOSTrackingAuthorizationStatus ();
if (!status.HasValue) {
status = await AttService.RequestTrackingAuthorization () as bool?;
}
#endif
請求に対する結果(許可の有無)は使いません。
この後、AdMobがIDFAを請求した際に、許可があれば取得でき、なければ取得できないというだけです。IDFAが得られないとSKAdNetworkが使われます。
参考
ビルド後に Xcode プロジェクトへ追記する
#if UNITY_IOS
using System.Collections.Generic;
using System.Text.RegularExpressions;
using System.IO;
using UnityEngine;
using UnityEditor;
using UnityEditor.Callbacks;
using UnityEditor.iOS.Xcode;
public static class PostBuildProcessForIos {
private const string ATT_FRAMEWORK = "AppTrackingTransparency.framework";
private const string ATT_USAGE = "NSUserTrackingUsageDescription";
private const string ATT_USAGE_TEXT = "This identifier will be used to deliver personalized ads to you.";
private const string LOCALIZATION_ARRAY_KEY = "CFBundleLocalizations";
private const string TargetDirectory = "Unity-iPhone Tests";
private const string TargetFolder = "Unity-iPhone Tests";
private static readonly string LocalizationFolderPath = Path.Combine (Application.dataPath, "Editor/iOS/Localizations");
/// <summary>ビルド後処理</summary>
[PostProcessBuild]
public static void OnPostProcessBuild (BuildTarget buildTarget, string buildPath) {
if (buildTarget != BuildTarget.iOS) { return; } // iOS専用
#region edit project
// read Project
var pbxPath = PBXProject.GetPBXProjectPath (buildPath);
var project = new PBXProject ();
project.ReadFromFile (pbxPath);
// ATT
project.AddFrameworkToProject (project._GetUnityFrameworkTargetGuid (), ATT_FRAMEWORK, true);
#region localization
// アセットのローカライズフォルダをプロジェクトへコピーし、リージョン・リストを作る
var targetMainGuid = project._GetUnityMainTargetGuid ();
var targetFrameworkGuid = project._GetUnityFrameworkTargetGuid ();
var localizationFolders = Directory.GetDirectories (LocalizationFolderPath);
var regions = new List<string> (); // リージョン・リスト
var targetPath = Path.Combine (buildPath, TargetDirectory); // コピー先パス
for (int i = 0; i < localizationFolders.Length; i++) {
var folderName = Path.GetFileName (localizationFolders [i]); // フォルダ名
var regionName = Path.GetFileNameWithoutExtension (localizationFolders [i]); // リージョン名
CopyWithoutMeta (localizationFolders [i], targetPath); // アセットのフォルダをプロジェクトへコピー
regions.Add (regionName); // リストにリージョンを追加
// コピーしたフォルダをプロジェクトに登録
var guid = project.AddFolderReference (Path.Combine (targetPath, folderName), Path.Combine (TargetFolder, folderName));
if (targetMainGuid != null) {
project.AddFileToBuild (targetMainGuid, guid);
}
project.AddFileToBuild (targetFrameworkGuid, guid);
}
#region edit project with string
// convert to string
var pbxstr = project.WriteToString ();
// modify knownRegions
pbxstr = Regex.Replace (pbxstr, @"(?<!\w)(developmentRegion\s*=\s*)English;", "$1en;", RegexOptions.Singleline | RegexOptions.IgnoreCase);
pbxstr = Regex.Replace (pbxstr,
@"(?<!\w)(knownRegions\s*=\s*\()((?:[\s\w\-""]+,)*)?(\s*\)\s*;)",
$"$1\n{string.Join ("\n", regions.ConvertAll (region => $"\t\t\t\t{region},"))}\n$3",
RegexOptions.Singleline | RegexOptions.IgnoreCase);
// convert from string
project.ReadFromString (pbxstr);
#endregion
#endregion
// write Project
project.WriteToFile (pbxPath);
#endregion
#region edit plist
// read Info.plist
var plistPath = Path.Combine (buildPath, "Info.plist");
var plist = new PlistDocument ();
plist.ReadFromFile (plistPath);
// ATT
plist.root.SetString (ATT_USAGE, ATT_USAGE_TEXT);
#region localization
// リージョンを登録
var lArray = plist.root.CreateArray (LOCALIZATION_ARRAY_KEY);
foreach (var region in regions) {
lArray.AddString (region);
}
#endregion
// write Info.plist
plist.WriteToFile (plistPath);
#endregion
}
/// <summary>指定のアセットフォルダを('.meta'を除外して)コピーする</summary>
/// <param name="spath">コピー元フォルダのフルパス (ノーチェック)</param>
/// <param name="ddir">コピー先ディレクトリ (ノーチェック)</param>
private static void CopyWithoutMeta (string spath, string ddir) {
var dpath = Path.Combine (ddir, Path.GetFileName (spath));
if (!Directory.Exists (dpath)) {
Directory.CreateDirectory (dpath);
}
foreach (var file in Directory.GetFiles (spath)) {
if (!file.EndsWith (".meta")) {
var dest = Path.Combine (dpath, Path.GetFileName (file));
File.Copy (file, dest, true);
}
}
}
/// <summary>Returns GUID of the framework target in Unity project.</summary>
private static string _GetUnityFrameworkTargetGuid (this PBXProject project) {
#if UNITY_2019_3_OR_NEWER
return project.GetUnityFrameworkTargetGuid ();
#else
return project.TargetGuidByName (PBXProject.GetUnityTargetName ());
#endif
}
/// <summary>Returns GUID of the main target in Unity project.</summary>
private static string _GetUnityMainTargetGuid (this PBXProject project) {
#if UNITY_2019_3_OR_NEWER
return project.GetUnityMainTargetGuid ();
#else
return null;
#endif
}
}
#endif
Xcodeでの手作業を避けるため、引き渡すべき設定を行います。
- ATT
- PBXプロジェクトへ、フレームワークの導入を設定
- ローカライズ
- PBXプロジェクトへ、アセット中に用意したリソースとリージョンを登録
- Info.plistへ、リージョンを登録
参考
- App Tracking Transparency で許可をリクエストする ~ 自動反映する場合
- How to add InfoPlist.strings into xcode project in script?
- [Unity/iOS]PlistInfo.strings作成自動化でローカライズ
- UnityのiOSビルド時のplist設定の自動化
ローカライズ用ファイル
/* Localized versions of Info.plist keys */
CFBundleDisplayName = "AppName";
NSUserTrackingUsageDescription = "This identifier will be used to deliver personalized ads to you.";
/* Localized versions of Info.plist keys */
CFBundleDisplayName = "アプリ名";
NSUserTrackingUsageDescription = "この識別子は、パーソナライズされた広告を配信するために使用されます。";
/* Localized versions of Info.plist keys */
CFBundleDisplayName = "アプリ名";
NSUserTrackingUsageDescription = "この識別子は、パーソナライズされた広告を配信するために使用されます。";
概略のフロー
App のプライバシーに関する質問への回答
- 公式ガイド(Google、Unity、Apple)に従って回答します。
- Googleのガイドでは、読み替えが必要になります。
- 例えば、Googleの言う「IP アドレス: デバイスのおおよその位置の推定に使われる場合があります。」は、「おおよその場所をユーザの個人情報に関連付けてサードパーティ広告とトラッキング目的に使用する」と解釈します。
- こちらの記事「AdMob利用時の「Appのプライバシー」の入力方法虎の巻」では、解釈の結果がまとめられています。
- Unityのガイドでは、Appleの質問毎に「収集するか? (Collected?)」、「ユーザに関連付けるか? (Linked to user?)」、「使途 (Purpose)」がまとめられているので、そのまま使えます。
- トラッキングの有無は、「tracking」が使途に含まれるかどうかで判別できます。
参考資料
Apple公式
App StoreでのAppのプライバシーに関する詳細情報の表示
App のプライバシーの管理 (App Store Connect ヘルプ)
Google公式
Unity用 AdMob Plug-in
Google Mobile Ads Unity Plugin v5.4.0 ~ Google Mobile Ads iOS SDK 7.68.0
Apple の App Store データ開示要件に備える
このガイドでは、7.68.0 以降の Google Mobile Ads SDK のデータ収集での慣行を説明
iOS 14 以降に備える
前提条件: Google Mobile Ads SDK 7.64.0 以降
AdMobでSKAdNetworkに対応する
App Tracking Transparency で許可をリクエストする
Google Mobile Ads SDK に同意を転送する
パーソナライズされていない広告だけをリクエストする
AdRequest request = new AdRequest.Builder().AddExtra("npa", "1").Build();
Unity公式
Unity Analytics
Unity IAP
Unity Ads
UnityEditor.iOS.Xcode.PBXProject
(2018.4)
UnityEditor.iOS.Xcode.PBXProject
(2019.3)
UnityEditor.iOS.Xcode.PBXProject
(2019.4)
Unity Data Privacy プラグインの使用
一般記事
以下の記事を参考にさせていただきました。
どうもありがとうございました。
-
AdMob利用時の「Appのプライバシー」の入力方法虎の巻
- Appのプライバシーに関するGoogleの公式見解に対する解釈
-
【Unity】AdMob GoogleMobileAds-v5.4.0 を v6.0.0に上げた際のエラー対応
- 「前バージョンのv5.4.0が無難」
-
Unity + AdMob 利用時に iOS 14 の ATT 対応する
- 「ユーザー情報を収集しない場合に
AddExtra("npa", "1")
のメソッドを使用する」 - 「
Info.plist
にAdMob用のSKAdNetworkIdentifier
が自動で追加されていた」
- 「ユーザー情報を収集しない場合に
-
Unity で iOS 14 のトラッキング許可ダイアログを表示する
- 「プラグインスクリプト~中身はこんな感じ」
-
Unity/GoogleMobileAds/SKAdNetwork
- 「Info.plistに~自動反映する場合」
- How to add InfoPlist.strings into xcode project in script?
- 【iOS/Xcode】アプリ名をローカライズする時の設定【Unity】
- [Unity/iOS]PlistInfo.strings作成自動化でローカライズ
- UnityのiOSビルド時のplist設定の自動化
その他
-
- 「SKAdNetworkを有効にする必要もある~自動的になっていた」
-
【Unity】IDFAを取得するべくATT対応した話
- 「Objective-Cのコードでエラーが出たので、こんな風に書き換えてみた」
-
- 「AdSupport.framework及びAppTrackingTransparency.frameworkをフォルダごとUnityプロジェクトのAssets\Plugins\iOSに格納」
-
【Unity】[iOS] Admob のための SKAdNetwork 設定
- 「Info.plistのSKAdNetworkItems キーに、Google が定義する値を追加~EditorのPost処理に書くことで自動化」
-
【Unity】[iOS]Xcode projectの Localization 自動化
- 「UnityEditor.iOS.Xcodeをわかる範囲使用して実装」
-
【Unity / iOS14】IDFA 広告のトラッキング追跡の実装方法
- 「
PostXcodeBuild.cs
~ビルドした際に出力されるXcodeプロジェクトに必要な設定がされる」- Xcodeに対する
AppTrackingTransparency.framework
の自動追加
- Xcodeに対する
- 「
-
- 「実装したところで、ユーザーをわずらわせるだけになってしまいそう」
- 「SKAdNetworkの強化によって~広告業者やAppleに任せておいた方がよさそう」
-
[Unity] iOS14のATT(App Tracking Transparency)に対応してAdMob広告を表示した
おわりに
- この記事は、あくまでも私の理解です。
- この記事のコードは基本的に借り物です。
- ただし、多少の手を入れている場合があります。
- お気づきの点や疑わしい点、あるいは解りづらいところなどございましたら、コメントやリクエストをお寄せいただけると助かります。
- 最後までお読みいただき、どうもありがとうございました。