MPMediaLibraryAuthorizationStatusチェックについて

iOS10からは、アプリがiTunesメディアファイルにアクセスする際に、ユーザの許可が必要になりました。

そのアラート画面で許可された後、画面などのUIを更新する場合は、そのままではバックグラウンドで処理されてクラッシュの原因になってしまいます。(毎回クラッシュする訳ではないから厄介)

ブロック内でperformSelectorOnMainThreadかGCDを使ってメインスレッドからUIを更新します。

[MPMediaLibrary requestAuthorization:^(MPMediaLibraryAuthorizationStatus status) {
    if (status == MPMediaLibraryAuthorizationStatusAuthorized) {
        // This will work in the background
        NSLog(@"isMainThread: %d", NSThread.isMainThread);
        dispatch_async(dispatch_get_main_queue(), ^{
            [self.tableView reloadData];
        });
    }
}];

或いは、UIとは分離させて通知を投げる様にしておくと良いと思います。むしろそうゆう使い方が前提かも。

NSUserDefaultsの罠

アプリの設定や状態を記録しておくのに便利なNSUserDefaultsですが、必ず保存できるとは限りません。そもそもiOSには処理が集中した時に、アラームや着信などの重要な処理を優先させる仕組みになっているため、synchronizeに失敗する場合があります。

と言っても、さほど問題ではありませんが、以下のような場合には注意が必要です。

[[NSUserDefaults standardUserDefaults] setObject:currentArray forKey:@"ListArray"];
[[NSUserDefaults standardUserDefaults] setInteger:selectedIndex forKey:@"ListSelectedIndex"];
[[NSUserDefaults standardUserDefaults] synchronize];

これは閲覧していたリストの「配列」と「何番目が選択されていたかのインデックス」を保存するコードですが、この内の片方だけしか保存されない稀なケースがあります。こうしてset~で複数のデータを記録する場合、配列とそのインデックスの様に相関関係がある場合には、使う際にチェックしないとNSRangeExceptionでクラッシュするかもしれません。

こういった場合、配列とインデックスを1つの配列にまとめてから保存すれば、synchronizeの失敗、イコール、配列とインデックスの両方の保存失敗となって整合性が保てるかもしれません(未確認)。NSUserDefaultsは手軽な反面、データベースの機能が少ないので、重要なデータを扱う場合は、トランザクション機構を備えた別の方法にした方が良い場合もあります。最近だとCore Data以外にもAndroidでも使えるRealmも人気です。

最近はXcodeやiTunes Connectからアプリのクラッシュ情報が見やすくなっていますので、起動時のクラッシュが頻発している場合は、こういったバックアップ処理がネックになっているかもしれません。

NSKeyedUnarchiverを使った正しい復元方法

NSKeyedArchiverで圧縮(シリアライズ)したコードは復元できない場合があるらしく、例外処理を挟んでいないとクラッシュするかもしれません。Appleのドキュメント「NSKeyedUnarchiver」のunarchiveObjectWithDataには注意書きがあります。

Discussion
This method raises an NSInvalidArchiveOperationException if data is not a valid archive.

と、言う事はアーカイブした後で復元できるかチェックしておけば良いとも考えたんですが、どうやら、iOSやアプリのアップデートにより、復元できない場合がある模様。アーカイブのチェックは、復元の時だけ例外処理を挟んでおくのが良さそうです。

NSData *archiveData = [[NSUserDefaults standardUserDefaults] objectForKey:@"ArchiveKey"];
if (archiveData != nil) {
    NSArray *array;
    @try {
        array = [NSKeyedUnarchiver unarchiveObjectWithData:archiveData];
    }
    @catch (NSException *exception) {
        [[NSUserDefaults standardUserDefaults] removeObjectForKey:@"ArchiveKey"];
        [[NSUserDefaults standardUserDefaults] synchronize];
    }
}

例えば、こんな感じでユーザーデフォルトにアーカイブDataを保存していて、例外をキャッチした時には、ゴミDataを取り除いておきます。

Appleのドキュメント「Archives and Serializations Programming Guide」のArchiveに例外についての詳しい説明があります。型が一致しない場合に例外が発生するようです。

Type Coercions
NSKeyedUnarchiver supports limited type coercion. A value encoded as any type of integer, be it a standard int or an explicit 32-bit or 64-bit integer, can be decoded using any of the integer decode methods. Likewise, a value encoded as a float or double can be decoded as either a float or a double value. When decoding a double value as a float, though, the decoded value loses precision. If an encoded value is too large to fit within the coerced decoded type, the decoding method throws an NSRangeException. Further, when trying to coerce a value to an incompatible type, such as decoding an int as a float, the decoding method throws an NSInvalidUnarchiveOperationException.