画像を非同期でダウンロードして Core Data に保存しようとしています。ステップ 1 は、json ファイルをダウンロードして解析し、フィード内の各オブジェクトのコア データにエンティティを保存することです。その部分はうまく機能しています。
Core Data に 10 個の Bird オブジェクトがあるとします。各 Bird には、名前、説明など、および独自のエンティティである BirdImage との対多関係があります。BirdImage には、「image_url」属性 (文字列) と「image」属性 (Transformable) があります。
さて、鳥の写真を表示するアプリの画面にたどり着いたら、まずBirdImageの「image」属性を確認します。null でない場合は、whateverBirdEntity.image を UIImageView の画像として設定します。null の場合は、画像をダウンロードする必要があります。次のようなコードでは:
@property (nonatomic, strong) AssetRequest *assetRequest; //this is just a wrapper for an asset url, cache policy, and time out
@property (nonatomic, strong) NSURLRequest *assetURLRequest;
@property (nonatomic, strong) NSURLConnection *assetConnection;
@property (nonatomic, strong) NSMutableData *assetConnectionData;
@property (nonatomic, strong) BirdImage *imageEntity;
@property (nonatomic, strong) NSManagedObjectContext *objectContext;
...
- (void)load {
dispatch_async(dispatchQueue, ^{
//Check for the image in Core Data
self.objectContext = [[NSManagedObjectContext alloc]
initWithConcurrencyType:NSPrivateQueueConcurrencyType];
self.objectContext.parentContext = [[CoreDataController sharedController] managedObjectContext];
NSFetchRequest *fetch = [NSFetchRequest fetchRequestWithEntityName:@"BirdImage"];
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"image_url = %@", [self.assetRequest.assetURL absoluteString]];
[fetch setPredicate:predicate];
NSArray *objects = [self.objectContext executeFetchRequest:fetch error:nil];
if ([objects count] > 0)
{
BirdImage *birdImage = [objects objectAtIndex:0];
if (birdImage.image) {
dispatch_async(dispatch_get_main_queue(), ^{
BirdAsset *asset = [[BirdAsset alloc] init];
asset.url = [NSURL URLWithString:birdImage.image_url];
asset.image = birdImage.image;
if (self.successBlock)
self.successBlock(asset); //the caller will use asset.image for the UIImageView
});
return;
}else{
//no image found, need to download it
self.imageEntity = birdImage; //this is the entity I want to re-save in Core Data once the image finishes downloading
dispatch_async(dispatch_get_main_queue(), ^{
self.assetURLRequest = [NSURLRequest requestWithURL:self.assetRequest.assetURL cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:self.assetRequest.timeOut];
self.assetConnection = [[NSURLConnection alloc] initWithRequest:self.assetURLRequest delegate:self startImmediately:NO];
[self.assetConnection scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
[self.assetConnection start];
});
}
}
}];
});
}
次に、ダウンロードが完了すると:
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
dispatch_async(dispatchQueue, ^{
UIImage *resultImage = [UIImage decompressImageFromData:self.assetConnectionData];
NSData *resultData = UIImagePNGRepresentation(resultImage);
DLog(@"saving to core data: %@", self.imageEntity.image_url); //THIS HAPPENS 10 TIMES (every time)
self.imageEntity.image = resultImage;
@try {
NSError *saveError = nil;
if (![self.objectContext save:&saveError])
NSLog(@"saveError %@", saveError);
}
@catch (NSException *exception) {
NSLog(@"Exception: %@", exception);
}
[[CoreDataController sharedController] saveContext];
BirdAsset *finalAsset = [[BirdAsset alloc] init];
finalAsset.data = resultData;
finalAsset.image = resultImage;
finalAsset.url = [NSURL URLWithString:self.imageEntity.image_url];
DLog(@"SUCCESS"); //THIS HAPPENS anywhere from 4-7 times. I never get all 10 images.
dispatch_async(dispatch_get_main_queue(), ^{
if (self.successBlock)
self.successBlock(finalAsset);
});
});
}
画像は正常にダウンロードされており、データベースを調べると、BirdImage の「画像」ごとに BLOB データが表示されます。問題は、10 個の画像のうち、ランダムな数の画像が実際に表示されることです (最初の実行時に 4 ~ 7 個のいずれか)。その後、もう一度この画面に戻ると、アプリはロックされ、エラー メッセージやクラッシュは発生しません。ある種の Core Data ロックであると思われます。
「コンテキストを作成したのと同じスレッドからコンテキストにアクセスする」必要があることはわかっています。しかし、異なるメソッド (上記の load および connectionDidFinishLoading メソッドなど) でコンテキストにアクセスしている場合、どうすれば同じスレッドを使用できますか? つまり、ダウンロードが完了したときに画像のスレッドセーフな CoreData コンテキスト保存を実行するようにコードを変更するにはどうすればよいでしょうか?