I am trying to implement a contact app.
In the MyContactsViewController, I am trying to get access to Contacts and if the access is granted I will fetch the contacts from my address book. The ContactHandler is my model class(singleton) which has the method called getAllContacts to get the contacts in a NSMutableArray.
- (void)viewDidLoad {
[super viewDidLoad];
contactHandler = [ContactHandler sharedInstance];
if(!self.accessGranted){
NSOperationQueue *queue =[[ NSOperationQueue alloc]init];
[queue performSelectorOnMainThread:#selector(getAccessToAddressBook) withObject:self waitUntilDone:YES];
contactList = [contactHandler getAllContacts];
}
else{
contactList = [contactHandler getAllContacts];
}
}
-(BOOL)getAccessToAddressBook{
CNContactStore * contactStore = [[CNContactStore alloc] init];
if( [CNContactStore authorizationStatusForEntityType:CNEntityTypeContacts] == CNAuthorizationStatusNotDetermined){
[contactStore requestAccessForEntityType:CNEntityTypeContacts completionHandler:^(BOOL granted, NSError * _Nullable error) {
if(granted){
self.accessGranted = YES;
}
else{
self.accessGranted = NO;
}
}];
}
else if( [CNContactStore authorizationStatusForEntityType:CNEntityTypeContacts]== CNAuthorizationStatusAuthorized){
self.accessGranted = YES;
}
else{
self.accessGranted = NO;
}
return self.accessGranted;
}
But I am getting this error -
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[NSOperationQueue getAccessToAddressBook]: unrecognized selector sent to instance 0x137630700'
Can anyone please help.
Your problem is this line:
[queue performSelectorOnMainThread:#selector(getAccessToAddressBook) withObject:self waitUntilDone:YES];
You're asking queue to perform getAccessToAddressBook when is self who has this selector
If you want to run the method getAccessToAddressBook on the queue, you can use - addOperationWithBlock:
As in this line: performSelectorOnMainThread you are passing withObject:self, where as in method definition i.e (BOOL)getAccessToAddressBookyou are not using it, which is causing the crash, so make withObject:nil , if you don't want to pass any object or value to the method.
Change this line:-
[queue performSelectorOnMainThread:#selector(getAccessToAddressBook) withObject:self waitUntilDone:YES];
to this:-
[queue performSelectorOnMainThread:#selector(getAccessToAddressBook) withObject:nil waitUntilDone:YES];
Or the better approach will be to write like this:-
NSOperationQueue *opQueue=[NSOperationQueue new];
NSInvocationOperation *op = [[NSInvocationOperation alloc] initWithTarget:self selector:#selector(getAccessToAddressBook) object:nil];
[opQueue addOperation:op ];
Related
I have few different questions about NSOperation and NSOperationQueue and I know guys that yours answers will help me;
I have to load a big amount of images and I have created my own loader based on NSOperation, NSOperationQueue and NSURLConnection (asynchronous loading);
Questions:
If I set maxConcurrentOperationCount (for example 3) for queue (NSOperationQueue), does it mean that only 3 operations performed in the same time even queue has 100 operations?
When I set property maxConcurrentOperationCount for queue sometimes "setCompletionBlock" doesn't work and count (operationCount) only increases; Why?
MyLoader:
- (id)init
{
self = [super init];
if (self) {
_loadingFiles = [NSMutableDictionary new];
_downloadQueue = [NSOperationQueue new];
_downloadQueue.maxConcurrentOperationCount = 3;
_downloadQueue.name = #"LOADER QUEUE";
}
return self;
}
- (void)loadFile:(NSString *)fileServerUrl handler:(GetFileDataHandler)handler {
if (fileServerUrl.length == 0) {
return;
}
if ([_loadingFiles objectForKey:fileServerUrl] == nil) {
[_loadingFiles setObject:fileServerUrl forKey:fileServerUrl];
__weak NSMutableDictionary *_loadingFiles_ = _loadingFiles;
MyLoadOperation *operation = [MyLoadOperation new];
[operation fileServerUrl:fileServerUrl handler:^(NSData *fileData) {
[_loadingFiles_ removeObjectForKey:fileServerUrl];
if (fileData != nil) {
handler(fileData);
}
}];
[operation setQueuePriority:NSOperationQueuePriorityLow];
[_downloadQueue addOperation:operation];
__weak NSOperationQueue *_downloadQueue_ = _downloadQueue;
[operation setCompletionBlock:^{
NSLog(#"completion block :%i", _downloadQueue_.operationCount);
}];
}
}
MyOperation:
#interface MyLoadOperation()
#property (nonatomic, assign, getter=isOperationStarted) BOOL operationStarted;
#property(nonatomic, strong)NSString *fileServerUrl;
#property(nonatomic, copy)void (^OnFinishLoading)(NSData *);
#end
#implementation MyLoadOperation
- (id)init
{
self = [super init];
if (self) {
_executing = NO;
_finished = NO;
}
return self;
}
- (void)fileServerUrl:(NSString *)fileServerUrl
handler:(void(^)(NSData *))handler {
#autoreleasepool {
self.fileServerUrl = fileServerUrl;
[self setOnFinishLoading:^(NSData *loadData) {
handler(loadData);
}];
[self setOnFailedLoading:^{
handler(nil);
}];
self.url = [[NSURL alloc] initWithString:self.fileServerUrl];
NSMutableURLRequest *request = [[NSMutableURLRequest alloc]
initWithURL:self.url
cachePolicy:NSURLRequestReloadIgnoringLocalCacheData
timeoutInterval:25];
[request setValue:#"" forHTTPHeaderField:#"Accept-Encoding"];
self.connection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:NO];
[self.connection scheduleInRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
[self.connection start];
_data = [[NSMutableData alloc] init];
}
}
- (void)main {
#autoreleasepool {
[self stop];
}
}
- (void)start {
[self setOperationStarted:YES];
[self willChangeValueForKey:#"isFinished"];
_finished = NO;
[self didChangeValueForKey:#"isFinished"];
if ([self isCancelled])
{
[self willChangeValueForKey:#"isFinished"];
_finished = YES;
_executing = NO;
[self didChangeValueForKey:#"isFinished"];
}
else
{
[self willChangeValueForKey:#"isExecuting"];
_finished = NO;
_executing = YES;
[self didChangeValueForKey:#"isExecuting"];
}
}
- (BOOL)isConcurrent {
return YES;
}
- (BOOL)isExecuting {
return _executing;
}
- (BOOL)isFinished {
return _finished;
}
- (void)cancel {
[self.connection cancel];
if ([self isExecuting])
{
[self stop];
}
[super cancel];
}
#pragma mark -NSURLConnectionDelegate
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
[_data appendData:data];
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
if ([self OnFinishLoading]) {
[self OnFinishLoading](_data);
}
if (![self isCancelled]) {
[self stop];
}
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
;
if (![self isCancelled]) {
[self stop];
}
}
- (void)stop {
#try {
__weak MyLoadOperation *self_ = self;
dispatch_async(dispatch_get_main_queue(), ^{
[self_ completeOperation];
});
}
#catch (NSException *exception) {
NSLog(#"Exception! %#", exception);
[self completeOperation];
}
}
- (void)completeOperation {
if (![self isOperationStarted]) return;
[self willChangeValueForKey:#"isFinished"];
[self willChangeValueForKey:#"isExecuting"];
_executing = NO;
_finished = YES;
[self didChangeValueForKey:#"isExecuting"];
[self didChangeValueForKey:#"isFinished"];
}
You must start the connection in the Operation's start method, and not in fileServerUrl:handler:.
I would remove this method altogether, and only provide an init method with all required parameters where you can completely setup the operation. Then, in method start start the connection.
Additionally, it's not clear why you override main.
Modifying the state variables _executing and _finished could be more concise and more clear (you don't need to set them initially, since the are already initialized to NO). Only set them in the "final" method completeOperation including KVO notifications.
You also do not need a #try/#catch in stop, since function dispatch_async() does not throw Objective-C exceptions.
Your cancel method is not thread safe, and there are also a few other issues. I would suggest the following changes:
#implementation MyOperation {
BOOL _executing;
BOOL _finished;
NSError* _error; // remember the error
id _result; // the "result" of the connection, unless failed
completion_block_t _completionHandler; //(your own completion handler)
id _self; // strong reference to self
}
// Use the "main thread" as the "synchronization queue"
- (void) start
{
// Ensure start will be called only *once*:
dispatch_async(dispatch_get_main_queue(), ^{
if (!self.isCancelled && !_finished && !_executing) {
[self willChangeValueForKey:#"isExecuting"];
_executing = YES;
[self didChangeValueForKey:#"isExecuting"];
_self = self; // keep a strong reference to self in order to make
// the operation "immortal for the duration of the task
// Setup connection:
...
[self.connection start];
}
});
}
- (void) cancel
{
dispatch_async(dispatch_get_main_queue, ^{
[super cancel];
[self.connection cancel];
if (!_finished && !_executing) {
// if the op has been cancelled before we started the connection
// ensure the op will be orderly terminated:
self.error = [[NSError alloc] initWithDomain:#"MyOperation"
code:-1000
userInfo:#{NSLocalizedDescriptionKey: #"cancelled"}];
[self completeOperation];
}
});
}
- (void)completeOperation
{
[self willChangeValueForKey:#"isExecuting"];
self.isExecuting = NO;
[self didChangeValueForKey:#"isExecuting"];
[self willChangeValueForKey:#"isFinished"];
self.isFinished = YES;
[self didChangeValueForKey:#"isFinished"];
completion_block_t completionHandler = _completionHandler;
_completionHandler = nil;
id result = self.result;
NSError* error = self.error;
_self = nil;
if (completionHandler) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
completionHandler(result, error);
});
}
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
if ([self onFinishLoading]) {
[self onFinishLoading](self.result);
}
[self completeOperation];
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
if (self.error == nil) {
self.error = error;
}
[self completeOperation];
}
In answer to your questions:
Yes, a maxConcurrentOperationCount of three means that only three will run at a time. Doing network requests like this is perfect example of when you'd want to use maxConcurrentOperationCount, because failure to do so would result in too many network requests trying to run, most likely resulting in some of the connections failing when using a slower network connection.
The main issue here, though, is that you're calling your operation's fileServerUrl method (which is starting the connection) from MyLoader. You've disconnected the request from the operation's start (defeating the purpose of maxConcurrentCount of 3 and possibly confusing the state of the operation).
The start method should be initiating the connection (i.e. don't start the request until one of those three available concurrent operations is available). Furthermore, since you cannot pass the URL and the handler to the start method, you should move your logic that saves those values to a customized rendition of your init method.
There are other minor edits we might suggest to your operation (main not needed, operationStarted is a little redundant, simplify the _executing/_finished handling, etc.), but the starting of the connection in fileServerUrl rather than being initiated by the start method is the key issue.
Thus:
- (id)initWithServerUrl:(NSString *)fileServerUrl
handler:(void(^)(NSData *))handler
{
self = [super init];
if (self) {
_executing = NO;
_finished = NO;
// do your saving of `fileServerURL` and `handler` here, e.g.
self.fileServerUrl = fileServerUrl;
self.OnFinishLoading:^(NSData *loadData) {
handler(loadData);
}];
[self setOnFailedLoading:^{
handler(nil);
}];
}
return self;
}
- (void)startRequest {
self.url = [[NSURL alloc] initWithString:self.fileServerUrl];
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:self.url
cachePolicy:NSURLRequestReloadIgnoringLocalCacheData
timeoutInterval:25];
[request setValue:#"" forHTTPHeaderField:#"Accept-Encoding"];
self.connection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:NO];
[self.connection scheduleInRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
[self.connection start];
_data = [[NSMutableData alloc] init];
}
- (void)start {
if ([self isCancelled])
{
[self willChangeValueForKey:#"isFinished"];
_finished = YES;
[self didChangeValueForKey:#"isFinished"];
return;
}
[self setOperationStarted:YES]; // personally, I'd retire this and just reference your `executing` flag, but I'll keep it here for compatibility with the rest of your code
[self willChangeValueForKey:#"isExecuting"];
_executing = YES;
[self didChangeValueForKey:#"isExecuting"];
[self startRequest];
}
For the first question, the answer is yes, if set 3 as a max number of operations, only 3 can be running togheter.
The second is bit strange problem and I'm not totally sure that this answer will be correct. When you leave operations to an NSOperationQueue, you can't be sure on which thread they will be executed, this lead a huge problem with async connection. When you start an NSURLConnection as usual you receive the delegate callbacks without a problem, that is because the connection is running on a thread with a living run loop. If you start the connection on a secondary thread, callbacks will be called on that thread, but if you don't keep the run loop alive they will be never received.That's where probably my answer isn't correct, GCD should take care of living run loops, because GCD queues runs on living threads. But if not, the problem could be that operations are started on a different thread, the start method is called, but the callbacks are never called. Try to check if the thread is always the main thread.
First of all, I'd like to say that I'm really having a bad time trying to configure and use my SQLite DB in a background thread so that the main thread is not blocked.
After I found a little guide somewhere on the Internet, I've decided to go for the FMDB wrapper.
All the methods related to the DB operations are in the same class and this is where I'm getting errors:
I've set the static variables like this:
static FMDatabaseQueue *_queue;
static NSOperationQueue *_writeQueue;
static NSRecursiveLock *_writeQueueLock;
Then in my init method I have:
- (id)init {
self = [super init];
if (self) {
_queue = [FMDatabaseQueue databaseQueueWithPath:[self GetDocumentPath]];
_writeQueue = [NSOperationQueue new];
[_writeQueue setMaxConcurrentOperationCount:1];
_writeQueueLock = [NSRecursiveLock new];
}
return self;
}
And this is the method that gives me the error:
- (void)UpdateTime:(NSString *)idT :(int)userId {
[_writeQueue addOperationWithBlock:^{
[_writeQueueLock lock];
[_queue inDatabase:^(FMDatabase *dbase) {
AppDelegate *deleg = (AppDelegate *)[[UIApplication sharedApplication] delegate];
if (![dbase executeUpdate:#"update orari set orario=datetime(orario, '? minutes') where nome=? and dataid>=? and idutente=?"
withArgumentsInArray:#[[NSNumber numberWithFloat:deleg.diff], deleg.nome, [NSNumber numberWithInt:deleg.idMed], [NSNumber numberWithInt: userId]]]) {
NSLog(#"error");
}
}];
[_writeQueueLock unlock];
}];
[_writeQueue addOperationWithBlock:^{
[_writeQueueLock lock];
[_queue inDatabase:^(FMDatabase *dbase) {
AppDelegate *deleg = (AppDelegate *)[[UIApplication sharedApplication] delegate];
if (![dbase executeUpdate:#"UPDATE orari SET presa=1 where dataid=? and idutente=?"
withArgumentsInArray:#[[NSNumber numberWithInt:deleg.identific], [NSNumber numberWithInt: userId]]]) {
NSLog(#"error");
}
}];
[_writeQueueLock unlock];
}];
[self AddNotification];
}
These are the errors I'm getting:
*** -[NSRecursiveLock dealloc]: lock (<NSRecursiveLock: 0xc38b350> '(null)') deallocated while still in use
DB Error: 5 "database is locked"
*** -[NSRecursiveLock unlock]: lock (<NSRecursiveLock: 0x13378d20> '(null)') unlocked when not locked
From the guide I've read, I supposed that the access to my DB would have been "serialized", and each update would have been added to a queue and executed one at a time.
As you can see, I have a lot to learn about this topic, so any help would really be appreciated.
As I can See you have not created shared instance or singleton instance of this init call
- (id)init {
self = [super init];
if (self) {
_queue = [FMDatabaseQueue databaseQueueWithPath:[self GetDocumentPath]];
_writeQueue = [NSOperationQueue new];
[_writeQueue setMaxConcurrentOperationCount:1];
_writeQueueLock = [NSRecursiveLock new];
}
return self;
}
This should be a singleton call as you will create multiple instance of NSOperationQueue which will make DB vulnurable in a multi-threaded environment, try making it singleton call for your database either using GCD or
static DBManager *sharedInstance = nil;
+(DBManager*)getSharedInstance{
if (!sharedInstance) {
sharedInstance = [[super allocWithZone:NULL]init];
_queue = [FMDatabaseQueue databaseQueueWithPath:[self GetDocumentPath]];
_writeQueue = [NSOperationQueue new];
[_writeQueue setMaxConcurrentOperationCount:1];
_writeQueueLock = [NSRecursiveLock new];
}
return sharedInstance;
}
It might solve your problem and this is first time I am answering here and I am new to the environment so please be little bit forgiving :) Thanks
Hi i Intigrate SpotifyLib CocoaLibSpotify iOS Library 17-20-26-630 into my Project. I open its SPLoginViewController using Bellow Method:-
-(void)OpenSpotify
{
NSError *error = nil;
[SPSession initializeSharedSessionWithApplicationKey:[NSData dataWithBytes:&g_appkey length:g_appkey_size]
userAgent:#"com.mycomp.spotify"
loadingPolicy:SPAsyncLoadingImmediate
error:&error];
if (error != nil) {
NSLog(#"CocoaLibSpotify init failed: %#", error);
abort();
}
[[SPSession sharedSession] setDelegate:self];
[self performSelector:#selector(showLogin) withObject:nil afterDelay:0.0];
}
-(void)showLogin
{
SPLoginViewController *controller = [SPLoginViewController loginControllerForSession:[SPSession sharedSession]];
controller.allowsCancel = YES;
//controller.view.frame=;
[self presentViewController:controller animated:YES completion:nil];
}
At First time that Appear Spotify Login Screen. After that I tap On Cancel Button, and Try to open again login screen then i got crash EXC_BAD_EXE at this line. sp_error createErrorCode = sp_session_create(&config, &_session);
UPDATE
I Found exet where is got BAD_EXC
in this method
+(void)dispatchToLibSpotifyThread:(dispatch_block_t)block waitUntilDone:(BOOL)wait {
NSLock *waitingLock = nil;
if (wait) waitingLock = [NSLock new];
// Make sure we only queue one thing at a time, and only
// when the runloop is ready for it.
[runloopReadyLock lockWhenCondition:1];
CFRunLoopPerformBlock(libspotify_runloop, kCFRunLoopDefaultMode, ^() {
[waitingLock lock];
if (block) { #autoreleasepool { block(); } }
[waitingLock unlock];
});
if (CFRunLoopIsWaiting(libspotify_runloop)) {
CFRunLoopSourceSignal(libspotify_runloop_source);
CFRunLoopWakeUp(libspotify_runloop);
}
[runloopReadyLock unlock]; // at hear when my debug poin reach after pass this i got bad_exc
if (wait) {
[waitingLock lock];
[waitingLock unlock];
}
}
after doing lots of search i got Solution i check that whether the session already exists then i put if condition like:-
-(void)OpenSpotify
{
SPSession *session = [SPSession sharedSession];
if (!session) {
NSError *error = nil;
[SPSession initializeSharedSessionWithApplicationKey:[NSData dataWithBytes:&g_appkey length:g_appkey_size]
userAgent:#"com.mycomp.spotify"
loadingPolicy:SPAsyncLoadingImmediate
error:&error];
if (error != nil) {
NSLog(#"CocoaLibSpotify init failed: %#", error);
abort();
}
[[SPSession sharedSession] setDelegate:self];
}
[self performSelector:#selector(showLogin) withObject:nil afterDelay:0.0];
}
-(void)showLogin
{
SPLoginViewController *controller = [SPLoginViewController loginControllerForSession:[SPSession sharedSession]];
controller.allowsCancel = YES;
[self presentViewController:controller animated:YES completion:nil];
}
Now no crash and working fine.
I have a singleton class that saves JSON objects to a CoreData database in the background using GCD (Grand Central Dispatch) queues. This works perfectly the majority of the time, but on iPad 2 and iPad Mini devices I am experiencing some issues with the process freezing.
My setup is pretty straight forward. I have a background dispatch queue (backgroundQueue) that is set to run serially, and I have a seperate instance of NSManagedObjectContext for the background queue. When I want to save something to the database I call the method that begins the save process, and In that method I use dispatch_async to invoke my save logic on the background thread.
Once all processing logic has ran I save the background MOC, and use NSManagedObjectContextDidSaveNotification to merge the changes from the background MOC to the main thread MOC. The background thread waits for this to complete, and once it's done it'll run the next block in the queue, if any. Below is my code.
dispatch_queue_t backgroundQueue;
- (id)init
{
if (self = [super init])
{
// init the background queue
backgroundQueue = dispatch_queue_create(VS_CORE_DATA_MANAGER_BACKGROUND_QUEUE_NAME, DISPATCH_QUEUE_SERIAL);
}
return self;
}
- (void)saveJsonObjects:(NSDictionary *)jsonDict
objectMapping:(VS_ObjectMapping *)objectMapping
class:(__unsafe_unretained Class)managedObjectClass
completion:(void (^)(NSArray *objects, NSError *error))completion
{
[VS_Log logDebug:#"**** Queue Save JSON Objects for Class: %#", managedObjectClass];
// process in the background queue
dispatch_async(backgroundQueue, ^(void)
{
[VS_Log logDebug:#"**** Dispatch Save JSON Objects for Class: %#", managedObjectClass];
// create a new process object and add it to the dictionary
currentRequest = [[VS_CoreDataRequest alloc] init];
// set the thead name
NSThread *currentThread = [NSThread currentThread];
[currentThread setName:VS_CORE_DATA_MANAGER_BACKGROUND_THREAD_NAME];
// if there is not already a background context, then create one
if (!_backgroundQueueManagedObjectContext)
{
NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
if (coordinator != nil)
{
// create the background queue
_backgroundQueueManagedObjectContext = [[NSManagedObjectContext alloc] init];
[_backgroundQueueManagedObjectContext setPersistentStoreCoordinator:coordinator];
[_backgroundQueueManagedObjectContext setMergePolicy:NSMergeByPropertyStoreTrumpMergePolicy];
_backgroundQueueManagedObjectContext.undoManager = nil;
// listen for the merge changes from context did save notification on the background queue
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(mergeChangesFromBackground:) name:NSManagedObjectContextDidSaveNotification object:_backgroundQueueManagedObjectContext];
}
}
// save the JSON dictionary
NSArray *objects = [self saveJSON:jsonDict objectMapping:objectMapping class:managedObjectClass managedObjectContext:_backgroundQueueManagedObjectContext level:0];
// save the objects so we can access them later to be re-fetched and returned on the main thread
if (objects.count > 0)
[currentRequest.objects addObjectsFromArray:objects];
// save the object IDs and the completion block to global variables so we can access them after the save
if (completion)
currentRequest.completionBlock = completion;
[VS_Log logDebug:#"**** Save MOC for Class: %#", managedObjectClass];
// save all changes object context
[self saveManagedObjectContext];
});
}
- (void)mergeChangesFromBackground:(NSNotification *)notification
{
[VS_Log logDebug:#"**** Merge Changes From Background"];
// save the current request to a local var since we're about to be processing it on the main thread
__block VS_CoreDataRequest *request = (VS_CoreDataRequest *)[currentRequest copy];
__block NSNotification *bNotification = (NSNotification *)[notification copy];
// clear out the request
currentRequest = nil;
dispatch_sync(dispatch_get_main_queue(), ^(void)
{
[VS_Log logDebug:#"**** Start Merge Changes On Main Thread"];
// merge changes to the primary context, and wait for the action to complete on the main thread
[_managedObjectContext mergeChangesFromContextDidSaveNotification:bNotification];
NSMutableArray *objects = [[NSMutableArray alloc] init];
// iterate through the updated objects and find them in the main thread MOC
for (NSManagedObject *object in request.objects)
{
NSError *error;
NSManagedObject *obj = [[self managedObjectContext] existingObjectWithID:object.objectID error:&error];
if (error)
[self logError:error];
if (obj)
[objects addObject:obj];
}
// call the completion block
if (request.completionBlock)
{
void (^saveCompletionBlock)(NSArray *, NSError *) = request.completionBlock;
saveCompletionBlock(objects, nil);
}
[VS_Log logDebug:#"**** Complete Merge Changes On Main Thread"];
// clear the request
request = nil;
});
[VS_Log logDebug:#"**** Complete Merge Changes From Background"];
}
When the issue occurs everything seems to run fine, and it gets into the mergeChangesFromBackground: method, but the dispatch_async() is not invoked.
As I mentioned above the code runs perfectly in most cases. Only time it has issues running is when I am testing on either an iPad 2 or iPad Mini and saving large objects.
Does anyone out there have any ideas why this is happening?
Thanks
** EDIT **
Since writing this I've learned of nested NSManagedObjectContexts. I have modified my code to use it and it seems to be working well. However, I still have the same issues. Thoughts? Also, any comments on nested MOCs?
- (void)saveJsonObjects:(NSDictionary *)jsonDict
objectMapping:(VS_ObjectMapping *)objectMapping
class:(__unsafe_unretained Class)managedObjectClass
completion:(void (^)(NSArray *objects, NSError *error))completion
{
// process in the background queue
dispatch_async(backgroundQueue, ^(void)
{
// create a new process object and add it to the dictionary
__block VS_CoreDataRequest *currentRequest = [[VS_CoreDataRequest alloc] init];
// set the thead name
NSThread *currentThread = [NSThread currentThread];
[currentThread setName:VS_CORE_DATA_MANAGER_BACKGROUND_THREAD_NAME];
// if there is not already a background context, then create one
if (!_backgroundQueueManagedObjectContext)
{
NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
if (coordinator != nil)
{
// create the background queue
_backgroundQueueManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[_backgroundQueueManagedObjectContext setParentContext:[self managedObjectContext]];
[_backgroundQueueManagedObjectContext setMergePolicy:NSMergeByPropertyStoreTrumpMergePolicy];
_backgroundQueueManagedObjectContext.undoManager = nil;
}
}
// save the JSON dictionary starting at the upper most level of the key path
NSArray *objects = [self saveJSON:jsonDict objectMapping:objectMapping class:managedObjectClass managedObjectContext:_backgroundQueueManagedObjectContext level:0];
// if no objects were processed, then return with an error
if (!objects || objects.count == 0)
{
currentRequest = nil;
// dispatch the completion block
dispatch_async(dispatch_get_main_queue(), ^(void)
{
NSError *error = [self createErrorWithErrorCode:100 description:#"No objects were found for the object mapping"];
[self logMessage:error.debugDescription];
completion(nil, error);
});
}
// save the objects so we can access them later to be re-fetched and returned on the main thread
if (objects.count > 0)
[currentRequest.objects addObjectsFromArray:objects];
// save the object IDs and the completion block to global variables so we can access them after the save
if (completion)
currentRequest.completionBlock = completion;
// save all changes object context
NSError *error = nil;
[_backgroundQueueManagedObjectContext save:&error];
if (error)
[VS_Log logError:error.localizedDescription];
dispatch_sync(dispatch_get_main_queue(), ^(void)
{
NSMutableArray *objects = [[NSMutableArray alloc] init];
// iterate through the updated objects and find them in the main thread MOC
for (NSManagedObject *object in currentRequest.objects)
{
NSError *error;
NSManagedObject *obj = [[self managedObjectContext] existingObjectWithID:object.objectID error:&error];
if (error)
[self logError:error];
if (obj)
[objects addObject:obj];
}
// call the completion block
if (currentRequest.completionBlock)
{
void (^saveCompletionBlock)(NSArray *, NSError *) = currentRequest.completionBlock;
saveCompletionBlock(objects, nil);
}
});
// clear out the request
currentRequest = nil;
});
}
** EDIT **
Hi Everyone,
I'd first like to thank everyone for this input on this, as that is what lead me to finding the solution for my problem. Long story short I did away completely with using the GCD and the custom background queue, and instead am now using nested contexts and the "performBlock" method to perform all saving on the background thread. This is working great and I elevated my hang up issue.
However, I now have a new bug. Whenever I run my app for the first time, whenever I try to save an object that has child object relations, I get the following exception.
-_referenceData64 only defined for abstract class. Define -[NSTemporaryObjectID_default _referenceData64]!
Below is my new code.
- (void)saveJsonObjects:(NSDictionary *)jsonDict
objectMapping:(VS_ObjectMapping *)objectMapping
class:(__unsafe_unretained Class)managedObjectClass
completion:(void (^)(NSArray *objects, NSError *error))completion
{
[_backgroundManagedObjectContext performBlock:^(void)
{
// create a new process object and add it to the dictionary
VS_CoreDataRequest *currentRequest = [[VS_CoreDataRequest alloc] init];
currentRequest.managedObjectClass = managedObjectClass;
// save the JSON dictionary starting at the upper most level of the key path
NSArray *objects = [self saveJSON:jsonDict objectMapping:objectMapping class:managedObjectClass managedObjectContext:_backgroundManagedObjectContext level:0];
if (objects.count == 0)
{
currentRequest.error = [self createErrorWithErrorCode:100 description:#"No objects were found for the object mapping"];
[self performSelectorOnMainThread:#selector(backgroundSaveProcessDidFail:) withObject:currentRequest waitUntilDone:NO];
}
else
{
// save the objects so we can access them later to be re-fetched and returned on the main thread
[currentRequest.objects addObjectsFromArray:objects];
// save the object IDs and the completion block to global variables so we can access them after the save
currentRequest.completionBlock = completion;
[_backgroundManagedObjectContext lock];
#try
{
[_backgroundManagedObjectContext processPendingChanges];
[_backgroundManagedObjectContext save:nil];
}
#catch (NSException *exception)
{
currentRequest.error = [self createErrorWithErrorCode:100 description:exception.reason];
}
[_backgroundManagedObjectContext unlock];
// complete the process on the main thread
if (currentRequest.error)
[self performSelectorOnMainThread:#selector(backgroundSaveProcessDidFail:) withObject:currentRequest waitUntilDone:NO];
else
[self performSelectorOnMainThread:#selector(backgroundSaveProcessDidSucceed:) withObject:currentRequest waitUntilDone:NO];
// clear out the request
currentRequest = nil;
}
}];
}
- (void)backgroundSaveProcessDidFail:(VS_CoreDataRequest *)request
{
if (request.error)
[self logError:request.error];
if (request.completionBlock)
{
void (^saveCompletionBlock)(NSArray *, NSError *) = request.completionBlock;
saveCompletionBlock(nil, request.error);
}
}
- (void)backgroundSaveProcessDidSucceed:(VS_CoreDataRequest *)request
{
// get objects from main thread
NSArray *objects = nil;
if (request.objects.count > 0)
{
NSFetchRequest *fetchReq = [NSFetchRequest fetchRequestWithEntityName:[NSString stringWithFormat:#"%#", request.managedObjectClass]];
fetchReq.predicate = [NSPredicate predicateWithFormat:#"self IN %#", request.objects];
objects = [self executeFetchRequest:fetchReq];
}
// call the completion block
if (request.completionBlock)
{
void (^saveCompletionBlock)(NSArray *, NSError *) = request.completionBlock;
saveCompletionBlock(objects, nil);
}
}
- (NSManagedObjectContext *)managedObjectContext
{
if (_managedObjectContext != nil) {
return _managedObjectContext;
}
NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
if (coordinator != nil)
{
// create the MOC for the backgroumd thread
_backgroundManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[_backgroundManagedObjectContext setPersistentStoreCoordinator:coordinator];
[_backgroundManagedObjectContext setMergePolicy:NSMergeByPropertyStoreTrumpMergePolicy];
_backgroundManagedObjectContext.undoManager = nil;
// create the MOC for the main thread
_managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[_managedObjectContext setParentContext:_backgroundManagedObjectContext];
[_managedObjectContext setMergePolicy:NSMergeByPropertyStoreTrumpMergePolicy];
}
return _managedObjectContext;
}
Do any of you have any idea why this crash is happening?
You might want to consider this kind of context architecture (untested code):
The mainManagedObjectContext will be initialised in the same manner but with NSMainQueueConcurrencyType and no observer (by the way, remember to remove your observer)
- (void) mergeChangesFromBackground:(NSNotification*)notification
{
__block __weak NSManagedObjectContext* context = self.managedObjectContext;
[context performBlockAndWait:^{
[context mergeChangesFromContextDidSaveNotification:notification];
}];
}
- (NSManagedObjectContext*) bgContext
{
if (_bgContext) {
return _bgContext;
}
NSPersistentStoreCoordinator* coordinator = [self persistentStoreCoordinator];
if (!coordinator) {
return nil;
}
_bgContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[_bgContext setPersistentStoreCoordinator:coordinator];
[_bgContext setMergePolicy:NSMergeByPropertyStoreTrumpMergePolicy];
[_bgContext setUndoManager:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(mergeChangesFromBackground:)
name:NSManagedObjectContextDidSaveNotification
object:_bgContext];
return _bgContext;
}
- (void) doSomethingOnBGContextWithoutBlocking:(void(^)(NSManagedObjectContext*))contextBlock
{
__block __weak NSManagedObjectContext* context = [self bgContext];
[context performBlock:^{
NSThread *currentThread = [NSThread currentThread];
NSString* prevName = [currentThread name];
[currentThread setName:#"BGContextThread"];
contextBlock(context);
[context reset];
[currentThread setName:prevName];
}];
}
I'd first like to thank everyone for this input on this, as that is what lead me to finding the solution for my problem. Long story short I did away completely with using the GCD and the custom background queue, and instead am now using nested contexts and the "performBlock" method to perform all saving on the background thread. This is working great and I elevated my hang up issue.
However, I now have a new bug. Whenever I run my app for the first time, whenever I try to save an object that has child object relations, I get the following exception.
-_referenceData64 only defined for abstract class. Define -[NSTemporaryObjectID_default _referenceData64]!
Below is my new code.
- (void)saveJsonObjects:(NSDictionary *)jsonDict
objectMapping:(VS_ObjectMapping *)objectMapping
class:(__unsafe_unretained Class)managedObjectClass
completion:(void (^)(NSArray *objects, NSError *error))completion
{
[_backgroundManagedObjectContext performBlock:^(void)
{
// create a new process object and add it to the dictionary
VS_CoreDataRequest *currentRequest = [[VS_CoreDataRequest alloc] init];
currentRequest.managedObjectClass = managedObjectClass;
// save the JSON dictionary starting at the upper most level of the key path
NSArray *objects = [self saveJSON:jsonDict objectMapping:objectMapping class:managedObjectClass managedObjectContext:_backgroundManagedObjectContext level:0];
if (objects.count == 0)
{
currentRequest.error = [self createErrorWithErrorCode:100 description:#"No objects were found for the object mapping"];
[self performSelectorOnMainThread:#selector(backgroundSaveProcessDidFail:) withObject:currentRequest waitUntilDone:NO];
}
else
{
// save the objects so we can access them later to be re-fetched and returned on the main thread
[currentRequest.objects addObjectsFromArray:objects];
// save the object IDs and the completion block to global variables so we can access them after the save
currentRequest.completionBlock = completion;
[_backgroundManagedObjectContext lock];
#try
{
[_backgroundManagedObjectContext processPendingChanges];
[_backgroundManagedObjectContext save:nil];
}
#catch (NSException *exception)
{
currentRequest.error = [self createErrorWithErrorCode:100 description:exception.reason];
}
[_backgroundManagedObjectContext unlock];
// complete the process on the main thread
if (currentRequest.error)
[self performSelectorOnMainThread:#selector(backgroundSaveProcessDidFail:) withObject:currentRequest waitUntilDone:NO];
else
[self performSelectorOnMainThread:#selector(backgroundSaveProcessDidSucceed:) withObject:currentRequest waitUntilDone:NO];
// clear out the request
currentRequest = nil;
}
}];
}
- (void)backgroundSaveProcessDidFail:(VS_CoreDataRequest *)request
{
if (request.error)
[self logError:request.error];
if (request.completionBlock)
{
void (^saveCompletionBlock)(NSArray *, NSError *) = request.completionBlock;
saveCompletionBlock(nil, request.error);
}
}
- (void)backgroundSaveProcessDidSucceed:(VS_CoreDataRequest *)request
{
// get objects from main thread
NSArray *objects = nil;
if (request.objects.count > 0)
{
NSFetchRequest *fetchReq = [NSFetchRequest fetchRequestWithEntityName:[NSString stringWithFormat:#"%#", request.managedObjectClass]];
fetchReq.predicate = [NSPredicate predicateWithFormat:#"self IN %#", request.objects];
objects = [self executeFetchRequest:fetchReq];
}
// call the completion block
if (request.completionBlock)
{
void (^saveCompletionBlock)(NSArray *, NSError *) = request.completionBlock;
saveCompletionBlock(objects, nil);
}
}
- (NSManagedObjectContext *)managedObjectContext
{
if (_managedObjectContext != nil) {
return _managedObjectContext;
}
NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
if (coordinator != nil)
{
// create the MOC for the backgroumd thread
_backgroundManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[_backgroundManagedObjectContext setPersistentStoreCoordinator:coordinator];
[_backgroundManagedObjectContext setMergePolicy:NSMergeByPropertyStoreTrumpMergePolicy];
_backgroundManagedObjectContext.undoManager = nil;
// create the MOC for the main thread
_managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[_managedObjectContext setParentContext:_backgroundManagedObjectContext];
[_managedObjectContext setMergePolicy:NSMergeByPropertyStoreTrumpMergePolicy];
}
return _managedObjectContext;
}
Do any of you have any idea why this crash is happening?
** EDIT **
Ladies and gents I have found the solution. Apparently, even when using nested contexts you still have to merge the changes via the NSManagedObjectContextDidSaveNotification. Once I added that into my code it all started to work perfectly. Below is my working code. Thanks a million guys!
- (NSManagedObjectContext *)managedObjectContext
{
if (_managedObjectContext != nil)
return _managedObjectContext;
NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
if (coordinator != nil)
{
// create the MOC for the backgroumd thread
_backgroundManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[_backgroundManagedObjectContext setPersistentStoreCoordinator:coordinator];
[_backgroundManagedObjectContext setMergePolicy:NSMergeByPropertyStoreTrumpMergePolicy];
_backgroundManagedObjectContext.undoManager = nil;
// create the MOC for the main thread
_managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[_managedObjectContext setParentContext:_backgroundManagedObjectContext];
[_managedObjectContext setMergePolicy:NSMergeByPropertyStoreTrumpMergePolicy];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(mergeChangesFromContextDidSaveNotification:) name:NSManagedObjectContextDidSaveNotification object:_backgroundManagedObjectContext];
}
return _managedObjectContext;
}
- (void)saveJsonObjects:(NSDictionary *)jsonDict
objectMapping:(VS_ObjectMapping *)objectMapping
class:(__unsafe_unretained Class)managedObjectClass
completion:(void (^)(NSArray *objects, NSError *error))completion
{
// perform save on background thread
[_backgroundManagedObjectContext performBlock:^(void)
{
// create a new process object and add it to the dictionary
VS_CoreDataRequest *currentRequest = [[VS_CoreDataRequest alloc] init];
currentRequest.managedObjectClass = managedObjectClass;
// save the JSON dictionary starting at the upper most level of the key path
NSArray *objects = [self saveJSON:jsonDict objectMapping:objectMapping class:managedObjectClass managedObjectContext:_backgroundManagedObjectContext level:0];
if (objects.count == 0)
{
currentRequest.error = [self createErrorWithErrorCode:100 description:#"No objects were found for the object mapping"];
[self performSelectorOnMainThread:#selector(backgroundSaveProcessDidFail:) withObject:currentRequest waitUntilDone:NO];
}
else
{
// save the objects so we can access them later to be re-fetched and returned on the main thread
[currentRequest.objects addObjectsFromArray:objects];
// save the object IDs and the completion block to global variables so we can access them after the save
currentRequest.completionBlock = completion;
[_backgroundManagedObjectContext lock];
#try
{
[_backgroundManagedObjectContext processPendingChanges];
[_backgroundManagedObjectContext save:nil];
}
#catch (NSException *exception)
{
currentRequest.error = [self createErrorWithErrorCode:100 description:exception.reason];
}
[_backgroundManagedObjectContext unlock];
// complete the process on the main thread
if (currentRequest.error)
[self performSelectorOnMainThread:#selector(backgroundSaveProcessDidFail:) withObject:currentRequest waitUntilDone:NO];
else
[self performSelectorOnMainThread:#selector(backgroundSaveProcessDidSucceed:) withObject:currentRequest waitUntilDone:NO];
// clear out the request
currentRequest = nil;
}
}];
}
- (void)mergeChangesFromContextDidSaveNotification:(NSNotification *)notification
{
[_managedObjectContext mergeChangesFromContextDidSaveNotification:notification];
}
- (void)backgroundSaveProcessDidFail:(VS_CoreDataRequest *)request
{
if (request.error)
[self logError:request.error];
if (request.completionBlock)
{
void (^saveCompletionBlock)(NSArray *, NSError *) = request.completionBlock;
saveCompletionBlock(nil, request.error);
}
}
- (void)backgroundSaveProcessDidSucceed:(VS_CoreDataRequest *)request
{
// get objects from main thread
NSArray *objects = nil;
if (request.objects.count > 0)
{
NSFetchRequest *fetchReq = [NSFetchRequest fetchRequestWithEntityName:[NSString stringWithFormat:#"%#", request.managedObjectClass]];
fetchReq.predicate = [NSPredicate predicateWithFormat:#"self IN %#", request.objects];
objects = [self executeFetchRequest:fetchReq];
}
// call the completion block
if (request.completionBlock)
{
void (^saveCompletionBlock)(NSArray *, NSError *) = request.completionBlock;
saveCompletionBlock(objects, nil);
}
}
I've just setup a UITableview with Core Data and Grand Central Dispatch to update my app and display information through my fetchedResultsController. I have the application updating my database; however, the UITableView only gets populated once I redeploy the application to my phone through Xcode. For instance I run the update and everything works fine except I have an empty UITableView. Then I can close the app and click "Run" again through Xcode and when the app comes up the information is in the UITableView. I'm including the code below in hopes someone can help me discover why this is the case. If I need to include more code please just let me know. Thanks!
TeamTableViewController.m
- (NSFetchedResultsController \*)fetchedResultsController {
...
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchREquest:fetchRequest managedObjectContext:self.managedObjectContext sectionNameKeyPath:#"stateCode" cacheName:nil];
self.fetchedResultsController = aFetchedResultsController;
...
}
-(IBAction) refreshList:(id)sender {
dispatch_queue_t queue = dispatch_queue_create("updateQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue,^ { [self updateFromXMLFile:#"https://path/to/file.xml"];});
dispatch_async(queue,^ { [self updateFromXMLFile:#"https://path/to/file2.xml"];});
dispatch_async(queue,^ { [self updateFromXMLFile:#"https://path/to/file3.xml"];});
}
- (BOOL)updateFromXMLFile:(NSString *)pathToFile {
AppDelegate *appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
XMLParser *parser1 = [[XMLParser alloc] initXMLParser];
XMLParser *parser2 = [[XMLParser alloc] initXMLParser];
XMLParser *parser3 = [[XMLParser alloc] initXMLParser];
BOOL success = FALSE;
NSURL *url = [[NSURL alloc] initWithString:pathToFile];
NSXMLParser *xmlParser = [[NSXMLParser alloc] initWithContentsOfURL:url];
if ([pathToFile rangeOfString:#"people"].location != NSNotFound) {
NSManagedObjectContext * peopleMOC = [[NSManagedObjectContext alloc] init];
[peopleMOC setPersistentStoreCoordinator:[appDelegate persistentStoreCoordinator]];
NSNotificationCenter * notify = [NSNotificationCenter defaultCenter];
[notify addObserver:self selector:#selector(mergeChanges:) name:NSManagedObjectContextDidSaveNotification object: peopleMOC];
parser1.managedObjectContext = peopleMOC;
[xmlParser setDelegate: parser1];
success = [xmlParser parse];
if (success) {
NSError * error = nil;
#try {
[parser1.managedObjectContext save:&error];
} catch (NSException * exception) {
// NSLog logs the exception...
}
[NSNotificationCenter defaultCenter] removeObserver:self];
Return TRUE;
} else {
// NSLog logs errors
return FALSE;
}
} elseif ... { // other 3 use practically same code here }
[appDelegate saveContext];
}
-(void) mergeChanges:(NSNotification *) notification {
AppDelegate *theDelegate = [[UIApplication sharedApplication] delegate];
[[theDelegate managedObjectContext] performSelectorOnMainThread:#selector(mergeChangesFromContextDidSaveNotification:) withObject: notification waitUntilDone:YES];
}
UPDATE
I'm able to kind of get it to work by adding [theDelegate saveContext]; to the end of my -(void)mergeChanges method... This just doesn't seem like the proper way of doing it to me. Thoughts?
UPDATE 2
The above method worked one time but I've been unable to get it to replicate.
You should use the NSFetchedResultsControllerDelegate protocol to inform your view controller of any changes in the data.
Use controllerDidChangeContent: rather than the other methods. Like this, the results will be reflected once all the downloads have finished. Shorter incremental updates might get computationally expensive.
There is a very succinct "Typical Use" example in the delegate documentation.