iOSアプリにプライバシーポリシーへの同意UIを実装する

## プライバシーポリシーへの同意、実装してますか？

昨年改訂の[App Store審査ガイドライン 5.1.1](https://developer.apple.com/app-store/review/guidelines/jp/#data-collection-and-storage)によると、
 
> ユーザーまたは使用状況に関するデータを収集するアプリケーションでは、データ収集に関するプライバシーポリシーを明示し、ユーザーからの同意を得る必要があります。

だそうです。

いやはや、私はこんなルール知りませんでした（ぉぃ）。実際、[拙作のアプリ](https://itunes.apple.com/jp/app/id489857963)では、それを望むユーザーに対してのみですが、「個人の情報」をガッチリとサーバーとの間で送受信しています。が、**同意画面など実装してませんでした**。そしてその状態で、この間何度もアップデート審査に出してきましたが、**一度もリジェクトされたことはありません**。
でも、さすがに気づいてしまった以上は、ちゃんと実装しないとアカンですよね(^^ゞ。
というわけで、なんてことはない話ですが、私はこんなふうに実装しましたよ、というのを公開しておきます。

## 実装のポイント

* プライバシーポリシーを読まないと、一切の機能が使えないようにする
* いったんプライバシーポリシーに同意したら、次回以降は確認画面じたいを出さないようにする
* プライバシーポリシーは変更される場合があり、その場合には再度の同意を求める

## 画面例

### もともとの「ユーザーのデータをアレする」画面

![Simulator Screen Shot 2017.03.11 9.36.54.png](https://qiita-image-store.s3.amazonaws.com/0/98308/0870b863-8d0f-441c-640d-5c59ffc62428.png)

そもそもこれ何する画面なんだって話ですが、要は「このアプリからも、Webアプリからも、自分の記録を入力・編集できて、自動同期なんて高級な機能はないんで（笑）[^1]それをユーザーが手動で同期を実行する」という画面だと思ってください。実際にはこの画面でログイン操作を行って、その後ログインアカウントにひもづいた記録の同期を行うようになります[^2]。

なお、根本的にUIがとてつもなください点はご容赦ください。これ、フラットデザインになる前のボタン配置をそのまま継続したらこうなっちゃった、という典型的な悪例です。

### プライバシーポリシー同意画面を作る

![Simulator Screen Shot 2017.03.11 9.36.25.png](https://qiita-image-store.s3.amazonaws.com/0/98308/9b08ebba-8b49-f853-28df-285f68bf89b5.png)

こんな`UIView`をxibで作り（Storyboard導入前からのアプリなものでして…）、親Viewに重ねておきます。「プライバシーポリシーを読む」と「プライバシーポリシーに同意します」は`UIButton`、プライバシーポリシーのバージョンが書かれている部分は内容が動的な`UILabel`、他は固定の`UILabel`です。

で、安直に`IBOutlet`および`IBAction`として、以下の感じで関連づけておきます。

```objc:SyncViewController.h
// 同意画面全体
@property(nonatomic,strong) IBOutlet UIView *confirmView;
// 同意するボタン
@property(nonatomic,strong) IBOutlet UIButton *ppConfirmButton;
// プライバシーポリシーのバージョン表示
@property(nonatomic,strong) IBOutlet UILabel *ppVersionLabel;

// 表示するボタンのタップ
- (IBAction)ppLinkButtonDidClick:(id)sender;
// 同意するボタンのタップ
- (IBAction)ppConfirmButtonDidClick:(id)sender;
```

当然のことですが、xib側での設定として、「読む」の`State`は`Enabled`をオン、「同意する」の`State`はオフにしておきます。

### プライバシーポリシーを読ませる

ある意味、今回の核心部分です。
が、これは最も安直な手法、**Webアプリ上にプライバシーポリシーを作り、Mobile Safariで表示するだけ**にします！

実際に、上の画面の「読む」をタップすると、こんな感じで、レスポンシブのレの字もない、ひどいページがMobile Safariで開くわけです[^3]。

![Simulator Screen Shot 2017.03.11 9.36.45.png](https://qiita-image-store.s3.amazonaws.com/0/98308/a8479016-0e7a-e02b-35ee-76d3579a9817.png)
（[PC版はこちら](https://oritsubushi.net/staticpages/index.php/pp)）

なお、プライバシーポリシーの内容がぶっちゃけ極左だったり別表の様式がJRの旅客営業規則だったりしてますが、そこはスルーしてください。

で、まるでAndroidのバックキーをパクっただけじゃないか疑惑の、小さすぎて操作しづらい左上の「遷移前アプリ名をタップするとそのアプリに戻るUI」をタップするなりなんなりして、アプリに戻ってきますと、こうなります。

![Simulator Screen Shot 2017.03.11 9.36.50.png](https://qiita-image-store.s3.amazonaws.com/0/98308/23ee8941-b7cf-49ad-8965-779ef0165580.png)

はい、「同意する」ボタンが有効になったわけです。

で、これをタップすれば、この画面が消えて、冒頭の同期画面が現れるようにすればOKです。

##コード書きましょう

### 最新のプライバシーポリシーのバージョン管理をする

**プライバシーポリシーは変更され得ます**。そして当然ながら、その変更は周知され再度の同意を求めるのがベターです。もちろん、プライバシーポリシーじたいに「予告なしに変更されますが、特に何もアクションがなければそれにも同意したこととみなします」的な文言を入れておけばよいのかもしれませんが、それですとユーザーフレンドリーではありませんよね。

そのため、**プライバシーポリシーをバージョン管理しなければなりません**。
プライバシーポリシーじたいはWebサイト上にページとして作るという前提なので、そのプライバシーポリシーの最新改訂日を、何らかの形で、アプリが知ることができればOKとなります。

もちろん、それをWebサーバーへのAPIとして実装し、動的に参照するのがベストと思いますが、プライバシーポリシーはそうそう改訂されるものでもないよねー、ということで、今回は**マクロで持つ**という超シンプルにしました。

```objc:PpVersion.h
#ifndef PpVersion_h
#define PpVersion_h

#define PP_VERSION      20130526

#endif /* PpVersion_h */
```

このような専用のヘッダーファイルを作って、そこでマクロでプライバシーポリシーの改訂日を整数値として決め打ちます。プライバシーポリシーを改訂する場合、そのタイミングで、このマクロ定義を変更したアプリを同時リリースすればよい、ということになりますね。

### ユーザーが同意済のプライバシーポリシーのバージョンを管理する

これは、プライバシーポリシーのバージョンを、`UserDefaults`に記録すれば済みます。

```objc:Settings.h
#define SETTINGS_KEY_PP_VERSION             @"_PpVersion"
```

```objc:SyncViewController.m
- (BOOL)testPp {
    NSInteger ppVersion = [[NSUserDefaults standardUserDefaults] integerForKey:SETTINGS_KEY_PP_VERSION];
    return ppVersion >= PP_VERSION;
}

- (void)writePp {
    [[NSUserDefaults standardUserDefaults] setInteger:PP_VERSION forKey:SETTINGS_KEY_PP_VERSION];
}
```

本来なら、ちゃんと`UserDefaults`へのI/Oを管理するクラスを作るべきですが、手抜きで、キー名はマクロ定義してそれで直アクセスしています。Cノウハウそのものです（苦笑）。

これで、`writePp`メソッドは「現在のアプリのプライバシーポリシーの日付を`UserDefaults`に書き込む」、そして`testPp`メソッドは、「プライバシーポリシーへの同意があり、それが現在のアプリに埋め込まれたプライバシーポリシーの日付以上であれば`YES`を返し、同意がないか同意したポリシーの日付が古ければ`NO`を返す」という基本動作を実装できました。

### プライバシーポリシーの最新バージョンを表示する

まず、プライバシーポリシーのバージョン表示を、`viewDidLoad`で作成します。これまたC時代のベタベタなフォーマットですが。

```objc:SyncViewController.m```
- (void)viewDidLoad
{
    [super viewDidLoad];
    // other initializing...
    self.ppVersionLabel.text = [NSString stringWithFormat:NSLocalizedString(@"(%1$04d/%2$02d/%3$02d版)", nil),
                                PP_VERSION / 10000, PP_VERSION / 100 % 100, PP_VERSION % 100];
}
```

### プライバシーポリシー同意画面の表示を制御する

次に、プライバシーポリシーへの同意画面全体を表示するかどうかを、前述の`testPp`を使って、`viewWillAppear:animated`で実装します。

```objc:SyncViewController.m
- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];
    // other initializing...
    self.confirmView.hidden = [self testPp];
}
```

### 同意ボタンの有効を制御する

さらに、プライバシーポリシーに同意するボタンがタップ可能かどうかは、以下のように書いてみました。

```objc:Consts.h
#define PP_URL                          @"https://oritsubushi.net/staticpages/index.php/pp";
#define SYNC_PP_READ_WAIT_NSEC          (int64_t)(2.0 * NSEC_PER_SEC)
```

```objc:SyncViewController.m
- (void)ppLinkButtonDidClick:(id)sender {
    [[UIApplication sharedApplication] openURL:[NSURL URLWithString:PP_URL]];
    dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, SYNC_PP_READ_WAIT_NSEC);
    dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
        self.ppConfirmButton.enabled = YES;
    });
}
```

いきなりGCDであります。プライバシーポリシーを見るボタンがタップされたら、Mobile Safariでプライバシーポリシーページを開きます。で、その直後から2秒後に、同意ボタンを有効にしています。
このウェイトを入れないと、Mobile Safariが開く直前に、ボタンが有効になるところがユーザーに見えてしまうことがあります。2秒という値そのものには根拠はありません！（きっぱり）

### 同意ボタンタップ後の動作

同意ボタンが押されたら、あとは、アプリが持つプライバシーポリシーの日付を`UserDefaults`に書き込み、何らかの方法で同意画面全体を消してしまえば "Everything is ~~EVIL~~ OK." となります。

```objc:SyncViewController.m
- (void)ppConfirmButtonDidClick:(id)sender {
    [self writePp];
    NSLayoutConstraint *constraint = [NSLayoutConstraint constraintWithItem: self.view
                       attribute:  NSLayoutAttributeBottom
                       relatedBy:  NSLayoutRelationEqual
                       toItem:     self.confirmView
                       attribute:  NSLayoutAttributeTop
                       multiplier: 1.0
                       constant:   0.0
     ];
    [self.view removeConstraint:self.confirmViewTopConstraint];
    [self.view addConstraint:constraint];
    [UIView animateWithDuration:0.5
                               delay:0.3
                               options:UIViewAnimationOptionCurveEaseOut
                               animations: ^{
                                   [self.view layoutIfNeeded];
                               }
                               completion: nil
                               ];
}
```

同意画面の消し方としては、ダイアログっぽい機能だからと、なんか下にスライドアウトさせてみました。
なんでアニメ関連だけパラメーターを即値で決め打ってるんだじぶん。（実はリリース時にマクロ定義にするの忘れてましたm(__)m）


## 脚注

[^1]: SQLiteしか選択肢がない時代からのアプリなんですよね…そのため、サーバーとの同期はSQL文をそのままやり取りするようになっていて、いまさら置き換えができない状態になっています。涙
[^2]: 蛇足ながら、OAuthを使わず、Webサーバーが直接ユーザーのアカウントを新規登録する場合は、その操作はMobile Safari上にリダイレクトさせています。理由は忘れましたが、「メールアドレスの収集（認証のような単なる入力ではなく、サーバー側に新規で記録する）をアプリ内でやるのは危険」という噂に翻弄されただけかもしれません(；´∀｀)
[^3]: さすがに近日中になんとかします。。。