How can I remove an NSPersistentStore from my coordinator once it is unused? - core-data

I'm using in-memory NSPersistentStores to hold transient objects. Once the store is unused - which doesn't necessarily mean empty - I'd like to remove it from the coordinator to free up the memory it uses.
My first attempt was to have each controller create a store in its init, and remove it in its dealloc. This didn't work, because background threads were still using NSManagedObjects in that store; it was being removed while it was still being used, and things broke.
My second attempt was to wrap the stores in an object that could remove the real store once the wrapped object's retain count hit zero. That way, the background threads could retain the wrapped store, and it would only be removed once nothing was using it any more. I used message forwarding to create a proxy object, like so:
#interface MyStoreWrapper : NSObject
#property (nonatomic, retain) NSPersistentStore *persistentStore;
+(MyStoreWrapper *) wrappedInMemoryStore;
+(MyStoreWrapper *) wrapStore:(NSPersistentStore *)aStore;
-(id) initWithStore:(NSPersistentStore *)aStore;
#end
#implementation MyStoreWrapper
#synthesize persistentStore;
+(MyStoreWrapper *)wrappedInMemoryStore
{
NSError *error = nil;
NSPersistentStore *store = [[[MyAppDelegate sharedDelegate] persistentStoreCoordinator] addPersistentStoreWithType:NSInMemoryStoreType configuration:nil URL:nil options:nil error:&error];
if (error)
{
[NSException raise:#"Core data error" format:#"Could not add temporary store: %#, %#", error, [error userInfo]];
}
return [self wrapStore:store];
}
+(MyStoreWrapper *)wrapStore:(NSPersistentStore *)aStore
{
return [[[self alloc] initWithStore:aStore] autorelease];
}
-(id)initWithStore:(NSPersistentStore *)aStore
{
self = [super init];
if (self)
{
self.persistentStore = aStore;
}
return self;
}
-(void)dealloc
{
NSError *error = nil;
[[[MyAppDelegate sharedDelegate] persistentStoreCoordinator] removePersistentStore:self.persistentStore error:&error];
if (error)
{
[NSException raise:#"Core data error" format:#"Could not remove temporary store: %#, %#", error, [error userInfo]];
}
[super dealloc];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
if ([self.persistentStore respondsToSelector:[anInvocation selector]])
{
[anInvocation invokeWithTarget:self.persistentStore];
}
else
{
[super forwardInvocation:anInvocation];
}
}
- (BOOL)respondsToSelector:(SEL)aSelector
{
if ([super respondsToSelector:aSelector])
{
return YES;
}
else
{
return [self.persistentStore respondsToSelector:aSelector];
}
}
- (NSMethodSignature*)methodSignatureForSelector:(SEL)aSelector
{
NSMethodSignature* signature = [super methodSignatureForSelector:aSelector];
if (!signature)
{
signature = [self.persistentStore methodSignatureForSelector:aSelector];
}
return signature;
}
+(BOOL)instancesRespondToSelector:(SEL)aSelector
{
if ([super instancesRespondToSelector:aSelector])
{
return YES;
}
else
{
return [NSPersistentStore instancesRespondToSelector:aSelector];
}
}
+(NSMethodSignature *)instanceMethodSignatureForSelector:(SEL)aSelector
{
NSMethodSignature* signature = [super instanceMethodSignatureForSelector:aSelector];
if (!signature)
{
signature = [NSPersistentStore instanceMethodSignatureForSelector:aSelector];
}
return signature;
}
#end
...but this wrapped object doesn't seem to be a valid substitute in e.g. NSFetchRequests.
Have I made some mistake in my wrapper class? Is there some other way to remove an NSPersistentStore from my coordinator once it is unused?

You can't have a question that says both Once the store is unused and then because background threads were still using NSManagedObjects in that store.
By definition, if background threads are still using it, it's not unused ;)
Your method is correct but you are deallocing too early. Your background threads should be retaining the store if they are interested in it's contents. That way, as soon as all your background threads are finished, they will call release and the store will dealloc itself safely.

Related

Save data with Magical Record inside a block

I am retrieving some data from an API resource and I want to store the result inside my City entity using Magical Record and when the process finish, reload a tableView in my ViewController with the results.
All is fine but when I start the app for the first time,dowload process is started and the data is saved in core data.
but the table view in my ViewControllers is empty.
If I launch the app after the first time
the tableView refresh correctly.
I don't know if the problem is in threads... Can anybody help me?
ViewController :
Here I start the request. When block is called, I store cities array and reload tableView
- (void)getCitiesFromDataStore {
[[APIManager sharedManager] getCitiesWithCompletion:^(NSArray *cities) {
_dataSourceArray = cities;
[self.citiesTableView reloadData];
} failure:^(NSError *error) {
NSLog(#"%#",error.localizedDescription);
}];
}
APIMAnager
- (void)getCitiesWithCompletion:(void (^)(NSArray *))succesBlock
failure:(void (^)(NSError *))errorBlock
{
NSArray *cachedCities = [City findAllCities];
if ([cachedCities count] == 0) {
[self GET:#"cities" parameters:nil success:^(NSURLSessionDataTask *task, id responseObject) {
NSArray *results = responseObject[#"cities"];
[City MR_importFromArray:results];
[[NSManagedObjectContext MR_defaultContext] MR_saveToPersistentStoreAndWait];
NSArray *cities = [City findAllCities];
succesBlock(cities);
} failure:^(NSURLSessionDataTask *task, NSError *error) {
errorBlock(error);
}];
return;
}
// Si ya hay ciudades almacenadas en CoreData, devuelvo el
// succesblock con las ciudades de CoreData
succesBlock(cachedCities);
}
I have a Category also to manage actions with the City entity
City+DBOperations
+ (NSArray *)findAllCities
{
NSArray *cities = [City MR_findAll];
return cities;
}
I know you said you resolved it, but for others who might be coming here another thing you could try is wrapping the import in a saveWithBlock:completion: and do your find in the completion block.
Also make sure you know which context each method is using. It is often helpful to be explicit about that.
Therefore you could change it to (this is untested, but should give you the concept):
[self GET:#"cities" parameters:nil success:^(NSURLSessionDataTask *task, id responseObject) {
NSArray *results = responseObject[#"cities"];
[MagicalRecord saveWithBlock:^(NSManagedObjectContext *localContext) {
[City MR_importFromArray:results inContext:localContext];
} completion:^(BOOL contextDidSave, NSError *error) {
NSArray *cities = [User MR_findAllInContext:[NSManagedObjectContext MR_defaultContext]];
succesBlock(cities);
}];
} failure:^(NSURLSessionDataTask *task, NSError *error) {
errorBlock(error);
}];

What is the best way to remove logs file Core Data creates, when removing a UIManagedDocument from iCloud?

I would have thought NSFileManagers method of removeItemAtURL:error: would remove the Core Data log files created when using UIManagedDocuments with iCloud.
What is the best way to make sure all of these log files are removed?
I have used...
- (void)deleteRemnantsOfOldDatabaseDocumentAndItsTransactionLogsWithCompletionHandler:(completion_success_t)completionBlock
{
__weak CloudController *weakSelf = self;
NSURL *databaseStoreFolder = self.iCloudDatabaseStoreFolderURL;
NSURL *transactionLogFolder = self.transactionLogFilesFolderURL;
[self deleteFileAtURL:databaseStoreFolder withCompletionBlock:^(BOOL docSuccess) {
[weakSelf deleteFileAtURL:transactionLogFolder withCompletionBlock:^(BOOL logSuccess) {
completionBlock(docSuccess && logSuccess);
}];
}];
}
In conjunction with...
- (void)deleteFileAtURL:(NSURL *)fileURL withCompletionBlock:(completion_success_t)completionBlock
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSFileCoordinator *fileCoordinator = [[NSFileCoordinator alloc] initWithFilePresenter:nil];
NSError *coordinatorError = nil;
__block BOOL success = NO;
[fileCoordinator coordinateWritingItemAtURL:fileURL
options:NSFileCoordinatorWritingForDeleting
error:&coordinatorError
byAccessor:^(NSURL *writingURL) {
NSFileManager *fileManager = [[NSFileManager alloc] init];
NSError *removalError = nil;
if ([fileManager fileExistsAtPath:[writingURL path]]) {
if (![fileManager removeItemAtURL:writingURL error:&removalError]) {
NSLog(#"deleteFileAtURL: removal error: %#", removalError);
} else {
success = YES;
}
}
}];
if (coordinatorError) {
NSLog(#"deleteFileAtURL: coordinator error: %#", coordinatorError);
}
completionBlock(success);
});
}
Note: this was used for a single document toolbox style app, and was intended more for clearing out the iCloud container before creating a brand new document, in an 'apparently' empty iCloud store for the first time. But I'm sure it can be adapted without too much work.
Oops, the above won't make sense/work without:
typedef void (^completion_success_t)(BOOL success);
You can debug the contents of your iCloud container and verify things have been removed by using a method like (which to be honest I've probably lifted from somewhere else and modified):
- (void)logDirectoryHierarchyContentsForURL:(NSURL *)url
{
NSFileManager *fileManager = [NSFileManager defaultManager];
NSDirectoryEnumerator *directoryEnumerator = [fileManager enumeratorAtURL:url
includingPropertiesForKeys:#[NSURLNameKey, NSURLContentModificationDateKey]
options:NSDirectoryEnumerationSkipsHiddenFiles
errorHandler:nil];
NSMutableArray *results = [NSMutableArray array];
for (NSURL *itemURL in directoryEnumerator) {
NSString *fileName;
[itemURL getResourceValue:&fileName forKey:NSURLNameKey error:NULL];
NSDate *modificationDate;
[itemURL getResourceValue:&modificationDate forKey:NSURLContentModificationDateKey error:NULL];
[results addObject:[NSString stringWithFormat:#"%# (%#)", itemURL, modificationDate]];
}
NSLog(#"Directory contents: %#", results);
}
And it's also worth logging onto developer.icloud.com and examining what is actually in the iCloud store. There is sometimes a difference between what is retained in the device ubiquity container, and what is actually in the iCloud server folder structure. Between all of these you can get quite a good idea of what's going on.

How to prevent race conditions when merging changes across threads?

A typical setup: we have a main thread with a mainMOC and a background thread with its own backgroundMOC. The background thread performs read-only operations on the backgroundMOC by dispatching blocks to a backgroundQueue.
The backgroundMOC needs to merge the changes from the mainMOC so we register for NSManagedObjectContextDidSaveNotification and then do something like
- (void)mainMocDidSave:(NSNotification *)notification {
dispatch_async(backgroundQueue, ^{
[backgroundMoc mergeChangesFromContextDidSaveNotification:notification];
});
}
Let's say the user deletes an object in the mainMOC. The code above does not seem safe to me, since the merge will be done at some point in the future. Until the merge is done, there might still be blocks on the backgroundQueue that are trying to use the deleted object.
The obvious solution would be to use dispatch_sync instead (or performBlockAndWait, performSelector:OnThread:...) instead. From the code snippets I see on the interwebs, this seems to be what everybody is doing. But I'm not comfortable with this solution either.
The name NSManagedObjectContextDidSaveNotification implies that the save has already happened when the notification is delivered. So the corresponding row has already been deleted from the underlying database (assuming an sqlite store). dispatch_sync will have to wait for other blocks on the queue to finish before it can merge the changes, and these other blocks could still try to work with the deleted object, leading to an NSObjectInaccessibleException.
It seems to me that the correct way to merge changes from one thread/queue to another would be to
Subscribe to NSManagedObjectContextWillSaveNotification and NSManagedObjectContextDidSaveNotification on the background thread.
On NSManagedObjectContextWillSaveNotification: empty the backgroundQueue and suspend any operations that dispatch new blocks to the queue.
On NSManagedObjectContextDidSaveNotification: merge the changes synchronously.
Resume normal operation on the background queue.
Is this the correct approach or am I missing something?
I use the following structure in two projects in which I experienced similar troubles as you did. First of all I use singleton service to ensure there is only one background thread merging and reading changes.
AppDelegate.m
- (NSManagedObjectContext *)managedObjectContext {
if (_managedObjectContext != nil) {
return _managedObjectContext;
}
NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
if (coordinator != nil) {
// It is crucial to use the correct concurrency type!
_managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[_managedObjectContext setPersistentStoreCoordinator:coordinator];
}
return _managedObjectContext;
}
- (void)saveContext {
NSError *error = nil;
NSManagedObjectContext *managedObjectContext = self.managedObjectContext;
if (managedObjectContext != nil) {
if ([managedObjectContext hasChanges] && ![managedObjectContext save:&error]) {
// Replace this implementation with code to handle the error appropriately.
// abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
else {
[[NSNotificationCenter defaultCenter] postNotificationName:#"ParentContextDidSaveNotification" object:nil];
}
}
}
BackgroundService.m
- (id)init {
self = [super init];
if (self) {
[self managedObjectContext];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(parentContextDidSave) name:#"ParentContextDidSaveNotification" object:nil];
}
return self;
}
- (NSManagedObjectContext *)managedObjectContext {
if (!_managedObjectContext) {
// Again, make sure you use the correct concurrency type!
_managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[_managedObjectContext setParentContext:[(AppDelegate *)[[UIApplication sharedApplication] delegate] managedObjectContext]];
}
return _managedObjectContext;
}
- (BOOL)saveContext {
#synchronized(self) {
BOOL successful = YES;
// Bad practice, process errors appropriately.
[[self managedObjectContext] save:nil];
[[[self managedObjectContext] parentContext] performBlock:^{
[(AppDelegate *)[[UIApplication sharedApplication] delegate] saveContext];
}];
return successful;
}
}
- (void)parentContextDidSave {
[[self managedObjectContext] reset];
[[NSNotificationCenter defaultCenter] postNotificationName:#"ManagedObjectContextResetNotification" object:nil];
}

UIManagedDocument - How to deal with UIDocumentStateSavingError?

I am working on my first iCloud App. After working for a while the app cannot access a UIManagedDocument any more due to an "UIDocumentStateSavingError". Is there any way to actually find out what error occurred?
This is my code to create the UIManagedDocument:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
iCloudURL = [[NSFileManager defaultManager] URLForUbiquityContainerIdentifier:nil];
if (iCloudURL == nil) {
dispatch_async(dispatch_get_main_queue(), ^{
[self iCloudNotAvailable];
});
return;
}
iCloudDocumentsURL = [iCloudURL URLByAppendingPathComponent:#"Documents"];
iCloudCoreDataLogFilesURL = [iCloudURL URLByAppendingPathComponent:#"TransactionLogs"];
NSURL *url = [iCloudDocumentsURL URLByAppendingPathComponent:#"CloudDatabase"];
iCloudDatabaseDocument = [[UIManagedDocument alloc] initWithFileURL:url];
NSMutableDictionary *options = [NSMutableDictionary dictionary];
NSString *name = [iCloudDatabaseDocument.fileURL lastPathComponent];
[options setObject:name forKey:NSPersistentStoreUbiquitousContentNameKey];
[options setObject:iCloudCoreDataLogFilesURL forKey:NSPersistentStoreUbiquitousContentURLKey];
iCloudDatabaseDocument.persistentStoreOptions = options;
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(documentContentsChanged:) name:NSPersistentStoreDidImportUbiquitousContentChangesNotification object:iCloudDatabaseDocument.managedObjectContext.persistentStoreCoordinator];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(documentStateChanged:) name:UIDocumentStateChangedNotification object:iCloudDatabaseDocument];
if ([[NSFileManager defaultManager] fileExistsAtPath:[iCloudDatabaseDocument.fileURL path]]) {
// This is true, the document exists.
if (iCloudDatabaseDocument.documentState == UIDocumentStateClosed) {
[iCloudDatabaseDocument openWithCompletionHandler:^(BOOL success) {
if (success) {
dispatch_async(dispatch_get_main_queue(), ^{
[self documentConnectionIsReady];
});
} else {
dispatch_async(dispatch_get_main_queue(), ^{
[self connectionError:iCloudConnectionErrorFailedToOpen];
});
}
}];
} else if (iCloudDatabaseDocument.documentState == UIDocumentStateNormal) {
...
}
} else {
...
}
});
The Document already exists and thus openWithCompletionHandler: is called on the document. This fails and the UIDocumentStateChangedNotification is fired which shows a document states of 5:
UIDocumentStateClosed and
UIDocumentStateSavingError
After this the completion block gets called. What is correct way to proceed from here? Is there any way to find out what went wrong and what kind of error occurred?
I tried to re-open the document in the completion block but the result is the same.
I guess I could solve the problem by just deleting the file and recreate it. But this is obviously not an option once the app will be out in the store. I would like to know what is going wrong and give the user an appropriator way to handle the problem.
I already checked other questions here handling the UIDocumentStateSavingError (there a not a lot of them) but the seem not to be applicable for the problem here.
Any idea how I can find out what the problem is? I cannot belive that the API tells you "Something went wrong during saving but I will not tell you what!"
You can query the documentState in the completion handler. Unfortunately, if you want the exact error, the only way to get it is to subclass and override handleError:userInteractionPermitted:
Maybe something like this would help (typed freehand without compiler)...
#interface MyManagedDocument : UIManagedDocument
- (void)handleError:(NSError *)error
userInteractionPermitted:(BOOL)userInteractionPermitted;
#property (nonatomic, strong) NSError *lastError;
#end
#implementation MyManagedDocument
#synthesize lastError = _lastError;
- (void)handleError:(NSError *)error
userInteractionPermitted:(BOOL)userInteractionPermitted
{
self.lastError = error;
[super handleError:error
userInteractionPermitted:userInteractionPermitted];
}
#end
Then in you can create it like this...
iCloudDatabaseDocument = [[UIManagedDocument alloc] initWithFileURL:url];
and use it in the completion handler like this...
[iCloudDatabaseDocument openWithCompletionHandler:^(BOOL success) {
if (success) {
dispatch_async(dispatch_get_main_queue(), ^{
[self documentConnectionIsReady];
});
} else {
dispatch_async(dispatch_get_main_queue(), ^{
[self connectionError:iCloudConnectionErrorFailedToOpen
withError:iCloudDatabaseDocument.lastError];
});
}
}];
Based on #JodyHagins excellent snippet, I have made a UIDocument subclass.
#interface SSDocument : UIDocument
- (void)openWithSuccess:(void (^)())successBlock
failureBlock:(void (^)(NSError *error))failureBlock;
#end
#interface SSDocument ()
#property (nonatomic, strong) NSError *lastError;
#end
#implementation SSDocument
- (void)handleError:(NSError *)error userInteractionPermitted:(BOOL)userInteractionPermitted {
self.lastError = error;
[super handleError:error userInteractionPermitted:userInteractionPermitted];
}
- (void)clearLastError {
self.lastError = nil;
}
- (void)openWithSuccess:(void (^)())successBlock failureBlock:(void (^)(NSError *error))failureBlock {
NSParameterAssert(successBlock);
NSParameterAssert(failureBlock);
[self clearLastError];
[self openWithCompletionHandler:^(BOOL success) {
if (success) {
successBlock();
} else {
NSError *error = self.lastError;
[self clearLastError];
failureBlock(error);
}
}];
}
#end

"Type name requires a specifier or qualifier" error

I have the following code but can't compile it because I have a "Type name requires a specifier or qualifier" error" for (self).
How to fix this error? I have compared it with the original code and there are no differences, so I don't know what's going on.
#import "CurrentTimeViewController.h"
#implementation CurrentTimeViewController
{
// Call the superclass's designated initializer
self = [super initWithNibName:#"CurrentTimeViewController"
bundle:nil];
if (self) {
// Get the tab bar item
UITabBarItem *tbi = [self tabBarItem];
// Give it a label
[tbi setTitle:#"Time"];
}
return self;
}
Here is the code from the mirror file HynososViewController.h, and which I cut, pasted and modified:
#import "HypnososViewController.h"
#implementation HypnososViewController
- (id) init
{
// Call the superclass's designated initializer
self = [super initWithNibName:nil
bundle:nil];
if (self) {
// Get the tab bar item
UITabBarItem *tbi = [self tabBarItem];
// Give it a label
[tbi setTitle:#"Hypnosis"];
}
return self;
}
- (id) initWithNibName:(NSString *)nibName bundle:(NSBundle *)nibBundle
{
// Disregard parameters - nib name is an implementation detail
return [self init];
}
// This method gets called automatically when the view is created
- (void)viewDidLoad
{
[super viewDidLoad];
NSLog(#"Loaded the view for HypnosisViewController");
// Set the background color of the view so we can see it
[[self view] setBackgroundColor:[UIColor orangeColor]];
}
#end
Here is the complete code for CurrentTimeViewController.h:
#import "CurrentTimeViewController.h"
#implementation CurrentTimeViewController
{
// Call the superclass's designated initializer
self = [super initWithNibName:#"CurrentTimeViewController"
bundle:nil];
if (self) {
// Get the tab bar item
UITabBarItem *tbi = [self tabBarItem];
// Give it a label
[tbi setTitle:#"Time"];
}
return self;
}
- (id) initWithNibName:(NSString *)nibName bundle:(NSBundle *)Bundle
{
// Disregard parameters - nib name is an implementation detail
return [self init];
}
// This method gets called automatically when the view is created
- (void)viewDidLoad
{
[super viewDidLoad];
NSLog(#"Loaded the view for CurrentTimeViewController");
// Set the background color of the view so we can see it
[[self view] setBackgroundColor:[UIColor greenColor]];
}
#end
Is the code above a cut-and-paste of EXACTLY what you are trying to compile? If so, I think you are missing something very important that would make that code block a method implementation:
-(id)init // This, or something like it, is missing???
{
...
}
Check your code, here, and in your project. :-)

Resources