Copyright (C) iPhoneアプリ開発備忘録 All rights reserved.
ブログ内で記したコード、内容の正確性は保証いたしません。
記載内容を実装したことにより発生した不具合・損害等の責任は一切負いません。



2013年04月13日

iCloud & Core Data



【注:この記事は書きかけです】後述に示す通り、期待通りの動作にならない箇所があります。

Core DataドキュメントストレージをiCloudを用いて、iOSデバイス間で同期を行う手順。

1. iCloud用アプリケーションIDを作成する。
※ 2013年4月より、Provisioning Portalはリニューアルしているので注意。

(1) iOS Dev Centerから「Certificates, Identifiers & Profiles」に入る。
(2) 「Identifiers」から「App IDs」を開き、「+」(New App ID)を選択。
(3) 「Registering an App ID」ページで各項目を入力。
・App ID Description : 任意の名前
・App Services:iCloudにチェック
・App ID Prefix:デフォルト値を選択
・App ID Suffix:Explicit App IDにチェックを入れ、Bundle IDに自分のアプリケーションのバンドル識別子を入力(XcodeでプロジェクトのTARGETSからSummaryを開き、Bundle Identifierに設定されている値を入力)

2. iCloud用プロビジョニングプロファイルを作成する。
iCloud用プロビジョニングプロファイルは、特定のアプリでのみ有効とさせるため、設定時にワイルドカードを指定しないで作成する。

(1) iOS Dev Centerから「Certificates, Identifiers & Profiles」に入る。
(2) 「Provisioning Profiles」を開き、「+」(New Provisioning Profile)を選択。
(3) 「 iOS App Development」にチェックを入れて各項目を入力。
・プロファイル名:任意の名前
・証明書:ユーザ名
・アプリケーションID:上記で作成したアプリケーションID
・デバイス:アプリケーションのテストに使用する予定のデバイスを選択

3. Xcodeに上記2.で作成したプロビジョニングプロファイル等を読み込む。
(1) Xcodeでオーガナイザーを開く。
(2) Devicesをクリックし、右ペインのLIBRARYからProvisioning Profilesを選択。
(3) Refreshボタンをクリックして、作成したプロビジョニングプロファイルが画面上に表示されていることを確認。

4. プロジェクトでiCloudを使うように設定。
(1) TARGETSのBuild Settingsを開き、Code Signing Identityに上記2.で作成したプロビジョニングプロファイルを設定する。
(2) TARGETSのSummaryを開き、Entitlements欄で各項目を設定する。
・Entitlements にチェックを入れ、Use Entitlements Fileにプロジェクト名を入力。
・iCloudにチェックを入れ、Enable iCloudにする。
・Ubiquity Containersで「+」をクリックして、ユビキティコンテナ名称を設定する。(Bundle Identifierの値が自動的に設定される)
・Keychain Groupsで「+」をクリックして、キーチェーングループ名称を設定する。(Bundle Identifierの値が自動的に設定される)

5. コード修正
下記は既にローカルドキュメントとしてCore Dataが実装されているプロジェクトにiCloud対応をするという形で記述したもの。

Core DataをiCloud対応させると、データの追加更新削除時に更新ログファイルがiCloudサーバに届き、同じEntitlementsを持つアプリケーションがインストールされているデバイスに更新ログファイルがダウンロードされ、この更新ログファイルに基づいてローカルなCore Dataが更新され、UIも同時に更新されるという動作になる。

AppDelegate.m を修正する。
(1) アプリ起動時に、iCloudが使用可能かどうかをチェックする。
(2) iCloudTokenGetsメソッドで、iCloud トークンの有無を確認し、iCloud トークンがあればiCloud動作可能として、NSUserDefaultsにトークンを保存するとともに、iCloud状態監視を通知センタに登録する。
(3) iCloud トークンが前回起動時と異なる(ユーザがApple IDアカウントを変更するとiCloud トークンが異なる値となる)場合、または初めて起動する場合、iCloudを使用するかローカルドキュメントとして使用するかのアラート表示を行う。
(4) iCloudを使用する場合、アプリケーションからユビキティコンテナが使えるようにするため、initializeiCloudAccess メソッドにおいて、URLForUbiquityContainerIdentifier を呼び出す。この処理は時間がかかるため、バックグラウンドで実行する。
(5) iCloud状態監視によりiCloudAccountAvailabilityChangedメソッドが呼ばれたときは、iCloudトークンの状態を取得してNSUserDefaultsの iCloudトークンを更新する。


- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

// iCloud使用可否チェック
[self iCloudTokenGets];
}

- (void)iCloudTokenGets {

id firstLaunchWithiCloudAvailable = [[NSUserDefaults standardUserDefaults] boolForKey:@"iCloudUse"];

id currentiCloudToken = [[NSFileManager defaultManager] ubiquityIdentityToken];

if (currentiCloudToken) { // iCloud トークンあり(機内モードにしてもサインイン中であればトークンは返ってくる)
NSData *newTokenData = [NSKeyedArchiver archivedDataWithRootObject: currentiCloudToken];

[[NSUserDefaults standardUserDefaults] setObject: newTokenData forKey: @"com.apple.Hoge.UbiquityIdentityToken"];

// iCloud状態監視を通知センタに登録
[[NSNotificationCenter defaultCenter]
addObserver: self
selector: @selector (iCloudAccountAvailabilityChanged:)
name: NSUbiquityIdentityDidChangeNotification
object: nil];

// iCloud有効化選択アラート
if (currentiCloudToken && firstLaunchWithiCloudAvailable) {
UIAlertView *alert = [[UIAlertView alloc]
initWithTitle: @"Choose Storage Option"
message: @"Should documents be stored in iCloud and available on all your devices?"
delegate: self
cancelButtonTitle: @"Local Only"
otherButtonTitles: @"Use iCloud", nil];

[alert show];

} else {
[self initializeiCloudAccess];
}
} else {
[[NSUserDefaults standardUserDefaults]removeObjectForKey: @"com.apple.Hoge.UbiquityIdentityToken"];

}
}

- (void)alertView:(UIAlertView *)alertView didDismissWithButtonIndex:(NSInteger)buttonIndex {

switch (buttonIndex) {
case 0: { // Use iCloud

[[NSUserDefaults standardUserDefaults] setBool: YES forKey: @"iCloudUse"];

[self initializeiCloudAccess];

break;

}
case 1: { // Local only

[[NSUserDefaults standardUserDefaults] setBool: NO forKey: @"iCloudUse"];

break;

}
}

}

- (void)initializeiCloudAccess {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
if ([[NSFileManager defaultManager] URLForUbiquityContainerIdentifier:nil] != nil)
NSLog(@"iCloud is available\n");
else
NSLog(@"This Apps requires iCloud, but it is not available.\n");
});
}

- (void)iCloudAccountAvailabilityChanged:(NSNotification*)aNotification {

id currentiCloudToken = [[NSFileManager defaultManager] ubiquityIdentityToken];

if (currentiCloudToken) { // iCloud トークンあり(機内モードにしてもサインイン中であればトークンは返ってくる)

NSData *newTokenData = [NSKeyedArchiver archivedDataWithRootObject: currentiCloudToken];

if (![newTokenData isEqualToData:[[NSUserDefaults standardUserDefaults] integerForKey:@"com.apple.Librarian.UbiquityIdentityToken"]]) { // トークンが異なる

[[NSUserDefaults standardUserDefaults] setObject: newTokenData forKey: @"com.apple.Librarian.UbiquityIdentityToken"];



}
} else {
[[NSUserDefaults standardUserDefaults]removeObjectForKey: @"com.apple.Librarian.UbiquityIdentityToken"];

}
}




(5) Core DataをiCloudに対応させる。
(5-1) managedObjectContextメソッドで、iCloudサーバ からデータの変更通知を受ける設定を通知センタに登録する。
(5-2) iCloudサーバ からデータの変更通知がなされるとmergeChangesFrom_iCloudメソッドが呼び出されるので、ここでデータ更新を記述する。
(5-3) persistentStoreCoordinatorメソッドで、iCloud動作時のファイル名称等の設定を行う。設定後は、iCloudセットアップ完了を通知センタに通知する。

- (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_;
}

- (NSManagedObjectModel *)managedObjectModel {

if (managedObjectModel_ != nil) {
return managedObjectModel_;
}

managedObjectModel_ = [[NSManagedObjectModel mergedModelFromBundles:nil] retain];

return managedObjectModel_;
}

- (NSPersistentStoreCoordinator *)persistentStoreCoordinator {

if (persistentStoreCoordinator_ != nil) {
return persistentStoreCoordinator_;
}

NSURL *storeURL = [NSURL fileURLWithPath: [[self applicationDocumentsDirectory] stringByAppendingPathComponent: @"Data.sqlite"]];

persistentStoreCoordinator_ = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];

// 管理ドキュメントの更新ログファイルの場所を設定する
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 is available
// iCloud 用の設定
cloudURL = [cloudURL URLByAppendingPathComponent:@"data"];
[options setValue:@"Data.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_;
}

- (void)mergeChangesFrom_iCloud:(NSNotification *)notification {

NSManagedObjectContext* moc = [self managedObjectContext];
[moc performBlock:^{
[moc mergeChangesFromContextDidSaveNotification:notification];
}];
}



ViewController.mの修正
(1) ViewDidLoadメソッドで、iCloudセットアップ 完了通知を受ける通知センタを登録する。
(2) iCloudセットアップ 完了通知がされると、reloadFetchedResultsメソッドが呼び出され、データの更新、UIの更新を行う。

- (void)viewDidLoad {

[super viewDidLoad];

NSError *error = nil;
if (![[self fetchedResultsController] performFetch:&error]) {
NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
abort();
}

// Core Data / iCloudのセットアップが終わったときに通知を受ける
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(reloadFetchedResults:)
name:@"RefetchAllDatabaseData"
object:nil];
}

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


上記コードでiCloudによる複数デバイス間のデータ同期は可能になるが、2013年4月13日現在、iCloudサーバ上のアプリケーションのドキュメントファイルを削除した場合などの動作検証を実施中、iCloudへのアクセス時にタイムアウトするようになってしまった。

コンソール上に表示されたメッセージ。
Safe save failed for file, error: Error Domain=NSCocoaErrorDomain Code=512 "The file upload timed out."


Stack OverflowやAppleのForumによると、iOSデバイス上のプロビジョニングプロファイルを一旦削除する、iCloud上のドキュメントファイルを削除する、iOSデバイスそのものを完全リセットするなどの対処法が記載されているが、対処してもタイムアウト現象が発生してiCloudが使えない問題が継続しているという報告が多数なされている。

当方でもiOSデバイスの完全リセットを除く対処を実施したもののタイムアウトは解消しなかった。

これについて、iCloudのバグという指摘もあり、現時点においてはCore DataでiCloudをサポートするのは避けた方がよいかも知れない。

なお、iCloudドキュメントストレージをUIManagedDocumentを使用したCore Dataではなく、UIDocumentで標準的なドキュメント取り扱う場合は問題なく動作する。
(Appleの開発ドキュメント「3つ目のiOSアプリケーション:iCloud」のサンプルコードで検証済。)
https://developer.apple.com/jp/devcenter/ios/library/documentation/iCloud101.pdf



参考記事:
https://developer.apple.com/jp/devcenter/ios/library/documentation/iCloud101.pdf
https://developer.apple.com/jp/devcenter/ios/library/documentation/iCloudDesignGuide.pdf
https://goddess-gate.com/dc2/index.php/post/452
http://timroadley.com/2012/04/03/core-data-in-icloud/
http://blog.morizotter.com/2013/01/13/icloud-first-step/

posted by mobileDeveloper at 19:52 | Comment(0) | TrackBack(0) | Core Data はてなブックマーク - iCloud & Core Data | このブログの読者になる | 更新情報をチェックする
この記事へのコメント
コメントを書く
お名前:

メールアドレス:

ホームページアドレス:

コメント:

認証コード: [必須入力]


※画像の中の文字を半角で入力してください。
※ブログオーナーが承認したコメントのみ表示されます。

この記事へのトラックバック
Apple、Appleのロゴ、App Store、iPodのロゴ、iTunesは、米国および他国のApple Inc.の登録商標です。
iPhone、iPod touch、iPadはApple Inc.の商標です。
iPhone商標は、アイホン株式会社のライセンスに基づき使用されています。
その他、本ブログに記載されている製品名、会社名は、それぞれ各社の商標または登録商標です。