はじめてのAppsFlyer: (3) iOS SDKでのATTフレームワークの実装

前記事「[はじめてのAppsFlyer: (2) iOS SDK実装／iOSシミュレーターでの動作確認](https://qiita.com/maktag/items/9c5929450a7cc0b42e1a)」の続きの内容となります。
iOS SDKの実装においては、ATTフレームワークの考慮が必要です。AppsFlyer SDKを利用する際に、ATTフレームワーク実装において事前に考慮すべき点や、具体的な実装方法、iOSシミュレーターでの動作確認について解説します。

# ATTフレームワークとは？

ATT (App Tracking Transparency) フレームワークとは、AppleがiOS14より導入したユーザープライバシー保護のための方式です。アプリがIDFAを取得するために、定型のポップアップを表示してユーザーに明示的な許可を得ることが必要になりました。以前は、ユーザーが設定でオプトアウトしていない限りはアプリはIDFAの取得ができたのですが、iOS 14.5以降、アプリがIDFAを取得するためにはポップアップでの許可が必須になりました。

ATTフレームワークの実装はオプションであり、IDFAを使わない場合にはこの実装は不要です。ただし、IDFAがないと広告の効果測定ができないアドネットワークもあるため、より正確にアトリビューションを行うことを目指すのであれば、ATTフレームワーク実装が必要となります。

![Screenshot 2022-12-19 at 20.35.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73759/329a66c3-8e14-fce6-db11-7e7292500d47.png)



# AppsFlyerでのATTフレームワーク実装

ATTフレームワークを実装しない場合には、AppsFlyer SDK開始後、直ちにインストール計測が行われますが、ATTフレームワークを実装する場合には、AppsFLyer SDKは開始後すぐにインストール計測を行わずに、指定された「タイムアウト期間」はインストール計測を遅延させます。

このタイムアウト期間の間にユーザーからATT許諾を得られた場合には、その時点で直ちに取得したIDFAの情報を元にインストール計測を行います。
逆にユーザの"Appにトラッキングしないように要求"を選んだ場合や、タイムアウト期間内にユーザーがどちらも選択しなかった場合には、IDFAなしでインストール計測を行います。

![Screenshot 2022-12-19 at 20.41.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73759/f53d54c7-323e-3190-72b3-7e480941bc36.png)


# ATTフレームワーク実装手順

ここからは具体的な実装方法について解説します。
- [前記事](https://qiita.com/maktag/items/9c5929450a7cc0b42e1a) のようなSDK実装が完了していることを前提として、ATTフレームワークを追加実装する方法となります。
- AppsFlyer SDKはATTフレームワークに対応しているv.6以上で実装されていることを前提とします。

## 1 - ATTポップアップのメッセージを定義する
`Info.plist`内に`NSUserTrackingUsageDescription`というパラメーターとして定義します。`NSUserTrackingUsageDescription`と入力すると、`Privacy - Tracking Usage Description`に自動的に置き換わりますが、この行のValueとして、メッセージを入力します。

例: 「パーソナライズされた広告を配信するためには許可を選んでください。」

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73759/91bb7549-50ec-7b8a-444e-54ee085431da.png)

:::note info
備考
実際にアプリをリリースする際には、このメッセージをどのような文言にするのかは、結構重要なトピックであり、アプリの特性に合わせた考慮が必要です。
メッセージを検討する上で参考になりそうな情報のリンクを貼っておきます。
- [ATTオプトイン率を上げる5つの方法](https://www.appsflyer.com/ja/blog/trends-insights/increase-att-opt-in-rates/)
- [App Tracking Transparency のダイアログ事例と許可率について](https://zenn.dev/serima/articles/011b7d0028d73513a1a0)
:::

## 2 - requestTrackingAuthorizationの呼び出し
ATTポップアップを表示したい箇所で`requestTrackingAuthorization`を呼び出してください。

例えば、アプリ起動直後にATTポップアップを表示したい場合には、下記のように`didBecomeActiveNotification`内の`AppsFlyerLib.shared().start()`直後に呼び出します。

```swift:AppDelegate.swift
@objc func didBecomeActiveNotification() {
    // start is usually called here:
    // AppsFlyerLib.shared().start()
    if #available(iOS 14, *) {
      ATTrackingManager.requestTrackingAuthorization { (status) in
        switch status {
        case .denied:
            print("AuthorizationSatus is denied")
        case .notDetermined:
            print("AuthorizationSatus is notDetermined")
        case .restricted:
            print("AuthorizationSatus is restricted")
        case .authorized:
            print("AuthorizationSatus is authorized")
        @unknown default:
            fatalError("Invalid authorization status")
        }
      }
    }
}
```

アプリ内でスタートボタンを押したタイミングでATTポップアップを表示させたいなどの場合は、任意のアクションを定義して、その中で呼び出すことも可能です。

```swift:ViewController.swift
@IBAction func tapStart(_ sender: Any) {
    if #available(iOS 14, *) {
      ATTrackingManager.requestTrackingAuthorization { (status) in
        switch status {
        case .denied:
            print("AuthorizationSatus is denied")
        case .notDetermined:
            print("AuthorizationSatus is notDetermined")
        case .restricted:
            print("AuthorizationSatus is restricted")
        case .authorized:
            print("AuthorizationSatus is authorized")
        @unknown default:
            fatalError("Invalid authorization status")
        }
      }
    }
}
```

いずれの箇所で呼び出した場合にも、`requestTrackingAuthorization`を記述したファイルでは`AppTrackingTransparency`のインポートが必要になりますので、こちらも忘れずに追加します。

```swift
import AppTrackingTransparency
```

参考: [Apple Developer - requestTrackingAuthorization(completionHandler:)](https://developer.apple.com/documentation/apptrackingtransparency/attrackingmanager/3547037-requesttrackingauthorization
)

## 3 - waitForATTUserAuthorizationの設定
`AppDelegate.swift`のSDKの初期化の箇所で、`waitForATTUserAuthorization`を呼び出してください。`AppsFlyerLib.shared().start()`の前に呼び出されていることが必要になります。

```swift:AppDelegate.swift
AppsFlyerLib.shared().waitForATTUserAuthorization(timeoutInterval: 60)
```

`timeoutInterval`の値には、SDK開始後、ATTの許諾ダイアログを表示してからユーザーが操作するまでに十分な時間が確保できるような値を秒数で指定してください。
`timeoutInterval`の指定例：
- アプリの初回起動時にATTポップアップを表示する場合: 60秒程度
- 約2分のチュートリアルの完了後にATTポップアップを表示する場合: 180秒程度

:::note warn
注意
`requestTrackingAuthorization`を実装していないのに、`waitForATTUserAuthorization`だけ呼び出すのは避けてください。
無駄にタイムアウト期間分のみインストール計測を遅延させてしまうだけになります。

また、「いつATT許諾されるかわからないので、`timeoutInterval`を例えば1時間後（3,600秒後）など大きな値を設定する」というのはアンチパターンです。
定義通りであれば「ユーザーからの回答が得られないままタイムアウト期間が過ぎた場合には、その時点でインストール計測される」のですが、そのアプリが非アクティベートされたら、アプリに実装されているSDKも動作しません。つまり、タイムアウト期間を過ぎた1時間後のタイミングでインストール計測される可能性は低く、その後もう一度アプリを起動したタイミングで初めてインストール計測されることになってしまいます。
結果的に実際のインストール数と乖離した計測結果になりうるので、このような実装は避けるべきです。
:::


# 動作確認

上記の実装を行うと、ATTポップアップが表示され、IDFAが取得できることを確認できます。
`requestTrackingAuthorizationの呼び出し`はアプリ内でスタートボタンを押したタイミングでATTポップアップを表示させる実装を行い、iOSシミュレータでの動作確認をし、デバッグログの中身を検証してみます。

![Screenshot 2022-12-19 at 22.20.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73759/422160b2-42f1-8af1-0f31-1fc3004bfe54.png)


## 1 - 「Appにトラッキングしないように要求」を選択した場合

ATTポップアップ表示後、「Appにトラッキングしないように要求」を選択し、そのデバッグログをみるとATT許諾のステータスが`denied`になったことが確認できます。
```
AuthorizationSatus is denied
2022-12-19 22:12:50.003118+0900 AdventTest[34984:3506825] [AppsFlyerSDK] [com.appsflyer.serial] [ATT] Tracking authorization update from `oldStatus`: 0 to `newStatus`: 2
2022-12-19 22:12:50.003550+0900 AdventTest[34984:3506825] [AppsFlyerSDK] [com.appsflyer.serial] Denied/Restricted
2022-12-19 22:12:50.003949+0900 AdventTest[34984:3506825] [AppsFlyerSDK] [com.appsflyer.serial] Denied/Restricted
```

また、IDFAを示す`advertiserId`がnullであることが確認できます。
```
{"timestamp":"1671455570.107637","uid":"1671455556104-2851091","bundlename":"","cell":
{"mcc":"tv|sim", ...,"att_status":2,...,"advertiserId":"", ...
```

## 2 - 「許可」を選択した場合

一度iOSシミュレータのアプリを削除し、もう一度アプリをデプロイし、アプリを起動するとATTポップアップ表示されます。
「許可」を選択すると、ATT許諾のステータスが`authorized`になったことが確認できます。
```
AuthorizationSatus is authorized
2022-12-19 22:16:11.097623+0900 AdventTest[37931:3520243] [AppsFlyerSDK] [com.appsflyer.serial] [ATT] Tracking authorization update from `oldStatus`: 0 to `newStatus`: 3
2022-12-19 22:16:11.139215+0900 AdventTest[37931:3520245] [AppsFlyerSDK] [com.appsflyer.serial] Dynamically loaded library: AdSupport
2022-12-19 22:16:11.146197+0900 AdventTest[37931:3520249] [AppsFlyerSDK] [com.appsflyer.serial] dlopen
2022-12-19 22:16:11.146506+0900 AdventTest[37931:3520248] [AppsFlyerSDK] [com.appsflyer.serial] dlopen
```

IDFAを示す`advertiserId`はALL0の値になってます。
これはiOSシミュレーターがIDFAを持たないためであり、実機デバイスで確認するとその端末のIDFAの値がみられます。
```
{"timestamp":"1671455766.846198","uid":"1671455761926-8537326","bundlename":"","cell":
{"mcc":"tv|sim", ...,"att_status":3,...,"advertiserId":"00000000-0000-0000-0000-000000000000", ...
```


## 3 - タイムアウト期間超過の場合

デバッグログ上では、AppsFlyer SDK開始のメッセージ後、ちょうど60秒後のタイムアウトのタイミングで"ATT timer expired"になったことが確認できます。
```
2022-12-19 22:06:27.719555+0900 AdventTest[31579:3491202] [AppsFlyerSDK] [com.apple.main-thread] AppsFlyer SDK version 6.9.1 started build (92)
...
2022-12-19 22:07:27.962460+0900 AdventTest[31579:3493682] [AppsFlyerSDK] [com.appsflyer.serial] [ATT.2] ATT timer expired. Preparing queued events: 2
2022-12-19 22:07:27.962871+0900 AdventTest[31579:3493681] [AppsFlyerSDK] [com.appsflyer.serial] NotDetermined
2022-12-19 22:07:27.963274+0900 AdventTest[31579:3493683] [AppsFlyerSDK] [com.appsflyer.serial] NotDetermined
```

`denied`の場合と同様に、IDFAを示す`advertiserId`はnullになっています。
```
{"timestamp":"1671453713.383594","uid":"1671455185355-7453864","bundlename":"","cell":
{"mcc":"tv|sim", ...,"att_status":0,...,"advertiserId":"", ...
```

# まとめ
この記事では、少し取っ付きづらいATTフレームワークの実装について解説しました。
これから実装される方の参考になればうれしいです。

前記事、この記事ではiOSシミュレーターでの検証まででした。次の記事では、これまで実装したアプリを利用して、実機デバイスを使った非オーガニックインストール計測のテスト方法を解説します。