A Day In The Life

とあるプログラマの備忘録

iCloud プログラミング入門

iOS データ設計入門」でデータはメモリ、フラッシュドライブ、iCloud に保存することができると説明しました。今回は iCloud にデータを保存して複数端末でデータを共有する方法について説明していきます。

iCloud って何?

クラウドと聞くとなにやら難しい感じがしますが、iCloud を簡単にいうとインターネット上にあるデータの置き場です。実体はものすごい数のサーバです。
iCloudデータセンターのサーバ群
iCloud のデータは Apple ID ごとに管理されていて同じ ID であれば iPhone iPad iMac など端末を問わずデータを共有することが出来ます。
この記事は iCloud のプログラミングについて説明するので「iCloud って何って?」ところには深入りしません。iCloud に関する詳しい説明は下記書籍を参照してください。

iCloud を使うと何がうれしいのか?

iCloud は以下の図のように、端末がネットワークにつながっていればユーザが意識することなく自動でデータを同期して複数端末でデータを共有することができます。
データの共有
これによりユーザはアプリの利用場面に応じて端末を選ぶことが出来ます。外出先は iPhone、自宅は iPad、職場は iMac、みたいなことが簡単にできるのです。iCloud に対応したアプリを開発する場合、ユニバーサルアプリ(iPhone, iPod Touch, iPad に対応したアプリ)化することをおすすめします。ユニバーサル対応したアプリの方がより iCloud のメリットを活かすことが出来ます。

iCloud を使ってデータを共有する方法

アプリから iCloud を使ってデータを共有するには以下の3つの方法があります。

  • Key-value storage
    Key-value 形式でデータを保存する。使い方は簡単だが保存できる容量が 1MB に制限されている。電子書籍アプリのしおりやブラウザアプリのブックマーク、アプリの設定などちょっとしたデータを保存するのに向いている
  • Document storage
    ドキュメント形式で iCloud にデータを保存する
  • Core Data storage
    Core Data のデータを iCloud を使って共有する。大量かつ複雑なデータの保存に向いている

本記事では上記のうち Key-value storage と Core Data storage の使い方を紹介します。ここで記事で使用する iCloud 関連のライブラリは iOSMac OS X で使うことが出来ます。

iCloud を使うための設定

アプリから iCloud を使うためには、プロビジョニングポータルページ(https://developer.apple.com/ios/manage/overview/index.action)の App ID とアプリのプロジェクトファイルを修正する必要があります。

プロビジョニングポータルの設定
  1. Provisioning Portal の「App IDs」ページを開いて、サンプルアプリに使用する App ID の Configure リンクをクリックします。
    手順1-1
  2. App ID の詳細画面で「Enable for iCloud」にチェックを入れてページ下部の「Done」ボタンを押します。
    手順1-2
  3. 「Provisioning」ページを開いて、先ほど編集した App ID を使っているプロビジョニングプロファイルの「Renew」 ボタンを押して更新します。
  4. プロビジョニングプロファイルをダウンロードしてダウンロードしたファイルをダブルクリックします。
プロジェクトファイルの設定
  1. プロジェクトからターゲットを選択して「Summary」タブの一番下にある「Entitlements」の「Enable Entitlements」にチェックを入れます。自動的に「Keychain Access Groups」に Bundle Identifier の値が1行追加されます。
    手順2-1
  2. iCloud Key-Value Store」にチェックを入れて「iCloud Key-Value Store」に Bundle Identifier の値を入力します。この設定は「Key-value storage」を使う時に行います
    手順2-2
  3. iCloud Containers」の「+」ボタンをクリック1行追加します。自動的に Bundle Identifier(Company ID + プロジェクト名)の値が入ります。この設定は「Document storage」または「Core Data storage」を使う時に行います
    手順2-3
iPhone iPad の設定

端末側の設定も必要です。設定アプリの「iCloud > 書類とデータ」をオンにします。画面は以下の通りです。
iCloud の設定
以上で iCloud を使う準備は完了です。

iCloud Key-value storageを使う方法

Key-value storage を使って iCloud とデータを同期化するには NSUbiquitousKeyValueStore オブジェクトを使用します。このオブジェクトは名前の通りキーバリュー形式でデータを保存してくれます。データを保存すると一旦端末のフラッシュドライブにデータを保存しておき、いい感じのタイミングで iCloud に同期してくれます。NSUbiquitousKeyValueStore オブジェクトを使ったデータの保存と取得処理は NSUserDefaults オブジェクトの使い方とかなり似ています。キーバリュー形式でデータを保存する方法の詳細は以下の記事を参照してください。

データの保存と取得で使用するメソッド

NSUbiquitousKeyValueStore クラスにはデータ型に応じて以下の保存と取得メソッドが定義されています。

オブジェクトの型 保存メソッド 取得メソッド
オブジェクト全般(id型) setObject: forKey: objectForKey:
NSString setString: forKey: stringForKey:
NSArray setArray: forKey: arrayForKey:
NSDictionary setDictionary: forKey: dictionaryForKey:
NSData setData: forKey: dataForKey:
long long setLongLong: forKey: longLongForKey:
double setDouble: forKey: doubleForKey:
BOOL setBool: forKey: boolForKey:
データの保存

それでは具体的に NSUbiquitousKeyValueStore オブジェクトを使ってデータを保存する方法を見ていきましょう。defaultStore メソッドを使って NSUbiquitousKeyValueStore オブジェクトを取得してからデータを保存します。
以下は setLongLong: forKey: メソッドを使った数値データの保存の例です。

NSUbiquitousKeyValueStore *ukvs = [NSUbiquitousKeyValueStore defaultStore];
[ukvs setLongLong:index forKey:@"pageNumber"];
[ukvs synchronize];
データの取得

NSUbiquitousKeyValueStore に保存されているデータを取得する方法を見てみましょう。以下は longLongForKey: メソッドを使った数値データの取得の例です。

NSUbiquitousKeyValueStore *ukvs = [NSUbiquitousKeyValueStore defaultStore];
NSUInteger index = [ukvs longLongForKey:@"pageNumber"];
データの同期

他の端末から iCloud のデータが変更されると NSUbiquitousKeyValueStoreDidChangeExternallyNotification という名前で通知が送信されます。Key-value data storage のデータを画面に表示している場合は以下のように UIViewController の viewDidLoad メソッドで通知を受け取る設定をします。

- (void)viewDidLoad
{
  [super viewDidLoad];
  // iCloud からデータの変更通知を受ける設定
  [[NSNotificationCenter defaultCenter] addObserver:self
                                           selector:@selector(ubiquitousDataDidChange:)
                                               name:NSUbiquitousKeyValueStoreDidChangeExternallyNotification
                                             object:nil];
  :
  : 省略
  :
}

受け取った通知には以下のようなデータがわたってきます。

userInfo = {
  NSUbiquitousKeyValueStoreChangeReasonKey = 1;
  NSUbiquitousKeyValueStoreChangedKeysKey =     (
    bookmark,
    pageNumber
  );
}}

NSUbiquitousKeyValueStoreChangeReasonKey にはどのような理由でデータが変更されたかがわかる enum 値が格納されています。また NSUbiquitousKeyValueStoreChangedKeysKey には変更があったデータのキー情報が配列で格納されています。通知データの詳細は以下のページを参照してください。

変更が発生したデータは以下のようにキーごとに for 文でまわして取得します。ちなみに通知処理はメインスレッドで実行されます。

- (void)ubiquitousDataDidChange:(NSNotification *)notification
{
  // 通知データ
  NSDictionary *dict = [notification userInfo];
  NSLog(@"%@", dict);
  // 変更されたデータのキー値
  NSArray *keys = [dict objectForKey:NSUbiquitousKeyValueStoreChangedKeysKey];
  NSUbiquitousKeyValueStore *ukvs = [NSUbiquitousKeyValueStore defaultStore];
  for (NSString *key in keys) {
    // 変更が発生したデータの取得
    NSUInteger index = [ukvs longLongForKey:key];
    NSLog(@"index:%d", index);
  }
  :
  : データの同期処理、画面の再表示など
  :
}

iCloud Core Data storage を使う方法

Core Data storage を使って iCloud とデータを同期するには Core Data のセットアップ処理で iCloud 用のオプション設定を追加する必要があります。
サルでもわかる Core Data 入門【実装編】で作成したアドレス帳アプリのサンプルコードを拡張して iCloud に対応させてみます。
これから紹介するサンプルプログラムは GCD(Grand Central Dispatch) や Blocks を使った非同期処理が多く全体的にコードの難易度が高めです。GCD や Blocks に関する詳しい説明は下記書籍を参考にしてください。おすすめです。

AppDelegate クラスの修正

はじめに managedObjectContext メソッドを変更します。
NSManagedObjectContext オブジェクトを生成して、生成したオブジェクトに NSPersistentStoreCoordinator オブジェクトをセットします。iCloud を使うと managedObjectContext メソッドがメインスレッド以外から呼び出される可能性があるため NSManagedObjectContext オブジェクトの performBlockAndWait: メソッドを使って NSPersistentStoreCoordinator オブジェクトのセットと通知の設定をメインスレッドで行います*1

- (NSManagedObjectContext *)managedObjectContext
{
  if (_managedObjectContext != nil) {
    return _managedObjectContext;
  }  
  NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
  if (coordinator != nil) {
    NSManagedObjectContext *moc = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
    [moc performBlockAndWait:^{
      [moc setPersistentStoreCoordinator:coordinator];
      // iCloud からデータの変更通知を受ける設定
      [[NSNotificationCenter defaultCenter] addObserver:self 
                                               selector:@selector(mergeChangesFrom_iCloud:) 
                                                   name:NSPersistentStoreDidImportUbiquitousContentChangesNotification 
                                                 object:coordinator];
    }];
    _managedObjectContext = moc;
  }
  return _managedObjectContext;
}

次に persistentStoreCoordinator メソッドを変更します。NSFileManager オブジェクトの URLForUbiquityContainerIdentifier メソッドを使って iCloud と同期するフォルダのパスを取得します。このメソッドは処理に数秒間かかる可能性があるため dispatch_async 関数を使って非同期で実行します。取得した iCloud のパスは NSPersistentStoreCoordinator オブジェクトの addPersistentStoreWithType: configuration: URL: options: error: メソッドにオプションとして渡します。

- (NSPersistentStoreCoordinator *)persistentStoreCoordinator
{
  if (_persistentStoreCoordinator != nil) {
    return _persistentStoreCoordinator;
  }
  NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:@"AddressBook.sqlite"];
    
  _persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
    
  __weak NSPersistentStoreCoordinator *psc = _persistentStoreCoordinator;
  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    NSMutableDictionary *options =  [@{NSMigratePersistentStoresAutomaticallyOption : @(YES),
                                             NSInferMappingModelAutomaticallyOption : @(YES)} mutableCopy];
    NSURL *cloudURL = [[NSFileManager defaultManager] URLForUbiquityContainerIdentifier:nil];
    if (cloudURL) {
      // iCloud 用の設定
      cloudURL = [cloudURL URLByAppendingPathComponent:@"data"];
      [options setValue:@"AddressBook.store" forKey:NSPersistentStoreUbiquitousContentNameKey];
      [options setValue:cloudURL forKey:NSPersistentStoreUbiquitousContentURLKey];
    }
    NSError *error = nil;
    [psc lock];
    if (![psc addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:options error:&error]) {
      NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
      abort();
    }
    [psc unlock];
        
    dispatch_async(dispatch_get_main_queue(), ^{
      // Core Data のセットアップが終わったことを通知する
      [[NSNotificationCenter defaultCenter] postNotificationName:@"RefetchAllDatabaseData" object:self userInfo:nil];
    });
  });
  return _persistentStoreCoordinator;
}

最後に mergeChangesFrom_iCloud: メソッドを新たに作成して処理を追加します。他の端末でデータを保存するなど iCloud のデータに変更があった場合、NSPersistentStoreDidImportUbiquitousContentChangesNotification という通知が送信されます。mergeChangesFrom_iCloud: メソッドはそのときに実行されるメソッドです。このメソッドはメインスレッドで実行されないため NSManagedObjectContext オブジェクトの performBlock: メソッドを使って処理をメインスレッドで実行します。

- (void)mergeChangesFrom_iCloud:(NSNotification *)notification {
  __weak NSManagedObjectContext* moc = [self managedObjectContext];
  [moc performBlock:^{
    [moc mergeChangesFromContextDidSaveNotification:notification]; 
  }];
}
MasterViewController クラスの修正

viewDidLoad メソッドに Core Data のセットアップ終了通知を受け取る処理を追加します。

- (void)viewDidLoad
{
  [super viewDidLoad];
  :
  : 省略
  :
  // 通知を受け取る設定
  [[NSNotificationCenter defaultCenter] addObserver:self 
                                             selector:@selector(reloadFetchedResults:)
                                                 name:@"RefetchAllDatabaseData" 
                                               object:nil];
}

reloadFetchedResults: メソッドを新たに作成してデータとテーブルの再読み込み処理を追加します。

- (void)reloadFetchedResults:(NSNotification*)notification {
  NSError *error = nil;
  if (![[self fetchedResultsController] performFetch:&error]) {
    NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
    abort();
  }             
  [self.tableView reloadData];
}

動作確認

それではここまで作成したアプリの動作を確認しましょう。
まずはアプリに何件か適当にデータを入れて一旦アプリを削除します。再度アプリをインストールして起動した時に前のデータがちゃんと残っているか確認します。
次に同じ Apple ID の端末を2台用意します。1台目のアプリのデータを変更したときに、2台目のアプリにそのデータの変更が反映されたかどうかを確認します*2

iCloud に保存されたデータを消去したい時は

プログラムを試行錯誤して作成していると無駄なデータが iCloud に溜まっていきます。iCloud のデータを削除する時は設定アプリの「iCloud > ストレージとバックアップ > ストレージを管理 > 書類およびデータ」で削除することができます。画面は以下の通りです。
iCloud のデータ削除

サンプルコード

Key-value storage を使ったサンプルコード(UIPageViewController を使ったカレンダーにブックマークを設定するサンプル)

Core Data storage を使ったサンプルコード(Address Book の拡張)

*1:NSManagedObjectContext オブジェクトはスレッドセーフではなく単一スレッドで使用されることが想定されているためこのような処理が必要になります

*2:検証完了しました問題なく動きます