AppStoreでアプリがアップデート公開されたら、とんでもない自体になりました。
バージョンアップしたらアプリが起動しない!
と恐ろしい報告が。。。
今回のアップデートはSQLite に 12 カラム追加したので恐らく CoreData あたりかな?と思うのですが、migrate の仕方も問題ないですし開発でもアップデートできているので・・・原因が掴めません。
まず、アプリのログを見たい!ってことで、どうすればいいのかと言うと、 (1) iPhone のアプリを iTunes 同期させる。 (2) 以下のディレクトリにクラッシュログが保存されている。
/Users/ユーザ/Library/Logs/App Store/CrashReporter
するとこんなログが
execution code が 0x8badf00d ("ate bad food")
これを元にググってみる。
CoreData - マイグレーションを考慮した CoreDataManager パターン の記事で解決しました。 (まだAppStoreで公開されていないので、多分ですが・・・) CoreData に限らず
application:didFinishLaunchingWithOptions で制限時間を超えると起動しない
のです。 改めて前のバージョンの開発バージョンに戻して試した結果
データ200件超で、カラム12個追加で 30秒くらいでした
ということで、自分のアプリの対策をすることにしました。
一番の肝は、didFinishLaunchingWithOptionsで マイグレーションの待機処理させない事です。 問題のアプリの構成は、
(1) UITabView で複数画面存在する (2) 最初の起動画面で CoreData にアクセスしての一覧表示 (3) 他の画面でも CoreData にアクセスする
シーケンス的には、
didFinishLaunchingWithOptions 起動 → UITabView 起動 → ViewController 起動 → マイグレーション判別と処理 ※非同期処理 → トップ画面表示
重要なのは、dispatch_async(GCD) のブロック文で非同期処理する事です。 でないと、didFinishLaunchingWithOptions で待機してしまい、また時間制限で終了してしまいます。
ということで、まずUITabView にマイグレーション処理する ViewController を追加。
ユーザにあまり違和感を与えないように、Top画面としました。 この辺は作るアプリによって違いますので、それぞれ考える必要がありますね。
CoreData - マイグレーションを考慮した CoreDataManager パターン の記事では、CoreDataManager を別途作ってやっていますが、それをすると今回は修正が大変なので AppDelegate 経由で処理させることに。 ただし、あくまでも AppDelegate に 一般的な CoreData の処理が実装されている話が前提です。
まずは、マイグレーションを判別するコード
- (BOOL)isRequiredMigration {
NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:@"Sample.sqlite"];
NSError* error = nil;
NSDictionary* sourceMetaData = [NSPersistentStoreCoordinator metadataForPersistentStoreOfType:NSSQLiteStoreType
URL:storeURL
error:&error];
if (sourceMetaData == nil) {
return NO;
} else if (error) {
NSLog(@"Checking migration was failed (%@, %@)", error, [error userInfo]);
abort();
}
BOOL isCompatible = [self.managedObjectModel isConfiguration:nil
compatibleWithStoreMetadata:sourceMetaData];
return !isCompatible;
}
isConfiguration:nil
マイグレーションの更新に関しては、従来通り persistentStoreCoordinator で処理すればいいけど、気分的に明示的に別途コードを追加。
- (NSPersistentStoreCoordinator *)doMigration {
NSLog(@"--- doMigration ---");
NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:@"Sample.sqlite"];
NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithBool:YES], NSMigratePersistentStoresAutomaticallyOption,
[NSNumber numberWithBool:YES], NSInferMappingModelAutomaticallyOption,
nil];
NSError *error = nil;
__persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
if (![__persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:options error:&error])
{
NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
abort();
}
return __persistentStoreCoordinator;
}
マイグレション更新という意味では、NSPersistentStoreCoordinator を返す必要はないので void か BOOL でよいかも。
UITabView で最初に起動される TopViewController とマイグレーション実行画面 MigrateViewController も作成。 TopViewController の ViewDidLoad にマイグレーション更新時に TopViewController を起動させる処理を記述。
- (void)viewDidLoad {
[super viewDidLoad];
SampleAppDelegate *appDelegate = [[UIApplication sharedApplication] delegate];
if ([appDelegate isRequiredMigration]) {
[self showMigrateView];
}
}
- (void)showMigrateView {
SampleAppDelegate *appDelegate = [[UIApplication sharedApplication] delegate];
MigrateViewController *controller = [[MigrateViewController alloc]
initWithNibName:@"MigrateViewController"
bundle:nil];
controller.delegate = self;
controller.view.alpha = 0.0f;
[appDelegate.window addSubview:controller.view];
[UIView animateWithDuration:0.5f
delay:0.0f
options:UIViewAnimationOptionAllowUserInteraction
animations:^{
[controller.view setFrame:CGRectMake(0, 0, 320, 480)];
[controller.view setAlpha:1.0f];
}
completion:^(BOOL finished){
}];
}
最後に、MigrateViewController が起動された時にマイグレーション更新処理をする。
- (void)doMigration {
SampleAppDelegate *appDelegate = [[UIApplication sharedApplication] delegate];
[appDelegate doMigration];
}
- (void)viewWillAppear:(BOOL)animated {
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
self.message.text = NSLocalizedString(@"MIGRATION_MESSAGE_UPDATE", nil);
self.okButton.hidden = YES;
self.indicator.hidden = NO;
dispatch_async(queue, ^{[self doMigration];
dispatch_async(dispatch_get_main_queue(), ^{
self.message.text = NSLocalizedString(@"MIGRATION_MESSAGE_RESULT", nil);
self.indicator.hidden = YES;
self.okButton.hidden = NO;
});
});
}
これで、CoreData のマイグレーションが発生した場合は、非同期で更新処理がされるはずです。 いやぁ、本当に盲点でした。過信はいけませんねぇ。