I have a data manager (singleton class) that loads JSON from my backend service and then perform some caching in it, write it to disk, etc
so then I put this method inside a :
dispatch_sync(dispatch_queue_create("com.testing.serialQueue", DISPATCH_QUEUE_SERIAL), ^{
//my method that does disk caching here
});
inside this method when everything is done, I have a UINotification using the following:
dispatch_async(dispatch_get_main_queue(),^{
[[NSNotificationCenter defaultCenter] postNotificationName:kNewStoriesNotification object:nil userInfo:userInfo];
});
and inside the notification I do the following:
[self.collectionView performBatchUpdates:^{
if ([highlightItems count] > 0){
//doing some computation to find the index path
[weakSelf.collectionView insertItemsAtIndexPaths:indexPathArray];
}
} completion:^(BOOL finished) {
}];
and it sometimes gives me this error:
*** Assertion failure in -[UICollectionView _endItemAnimations], /SourceCache/UIKit_Sim/UIKit-2372/UICollectionView.m:2662
I am not sure why.. but my guess is that it has to do with my use of GCD and notification. Any ideas?
UPDATE:
Even if I remove the insertItemsAtIndexPaths: it still crashes at performBatchUpdates.. wonder why
Related
I am successfully adding and updating records in my core data on a second thread without issue.
However, deletes don't seem to take effect until I stop and restart the App. So the delete is obviously working to a extent. I read the data before loading the tableview and don't do anything different for when there has been a deletion.
The code I'm using is
....fetch records....
BOOL deleteGem = FALSE;
if ([[attributeDict objectForKey:#"headline"] hasPrefix:#"VOID"])
deleteGem = TRUE;
if ([mutableFetchResults count] == 0) {
// not there so create a new one
if (!deleteGem) {
// so create a new one unless it needs deleting
gem = (Gem *)[NSEntityDescription insertNewObjectForEntityForName:#"Gem" inManagedObjectContext:managedObjectContext];
[gem setID:[attributeDict objectForKey:#"ID"]];
}
} else {
// already exists so either get it and then update or delete it
gem = [mutableFetchResults objectAtIndex:0];
if (deleteGem) {
// delete it if required
[managedObjectContext deleteObject:gem];
gemDeletes ++;
}
}
.....
Later on I have a method to save any updates including:
NSError *error;
if (![self.managedObjectContext save:&error]) {
....
Any ideas warmly welcomed...
Edit - with full answer based on #TechZen's answer..
Register for notifications of updates on the 2nd thread in viewDidLoad
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(handleSaveNotification:) name:NSManagedObjectContextDidSaveNotification object:nil];
Unregister for notifications in viewDidUnload
[[NSNotificationCenter defaultCenter] removeObserver:self name:NSManagedObjectContextDidSaveNotification object:nil];
Handle the update in the main thread (a new method in the view controller)
-(void)handleSaveNotification:(NSNotification *)aNotification {
[managedObjectContext mergeChangesFromContextDidSaveNotification:aNotification];
}
You have to merge the background context with the foreground context if you want the changes made in the background context to show up in the foreground context.
I make a call to a web service, passing a parameter and then register an observer in a viewcontroller class (to notify completion of download) :
[self callWebservice:parameter1];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(dataDownloadComplete:) name:OP_DataComplete object:nil];
and then post a notification in my parser class:
-(void)connectionDidFinishLoading:(NSURLConnection *)connection method of the parser class. [[NSNotificationCenter defaultCenter] postNotificationName:OP_DataComplete object:nil];
In the callback method dataDownloadComplete: i would like to call the same web service again several times.
-(void)dataDownloadComplete
{
if([anArray objectAtindex:N]<10)
{
[self callWebservice:parameterN];
NSLog(#"This is getting called everytime (9 times)");
[self writeintoDatabase];
N++;
}
}
But the problem is that i want to write into a database what data i download from the service. The DB writing happens for 'parameter1' call strangely, and goes on for the others but not for parameter9(which i need as well). Note the Log is called all 9 times though. The writeintoDatabase code is perfect. Please help. Thanks in advance.
I have an asset manager that needs to notify the owner it's assets are ready. I'm sending a token back for the consumer to listen to listen for a notification to avoid tighter coupling. The issue is when the assets are already loaded I need to call the loadComplete after a delay. What's the best way to do this in objective-c?
Asset Manager
-(tokenString*) loadAssetPath:(NSString*) asset {
//start asynchronous load
//or if assets ready send complete <-- issue
return nonceToken;
}
-(void)loadComplete {
[[NSNotificationCenter defaultCenter]
postNotificationName:tokenString object:self];
}
Consumer
NSString* token;
-(void) loadSomething {
if(token)
[self removeListener];
token = [[AssetManager sharedManager]
loadAssetPath:#"http://server.dev/myLargeImage.png"];
[[NSNotificationCenter defaultCenter]
addObserver:[AssetManager sharedManager]
selector:#selector(assetLoaded:) name:token];
}
-(void)assetLoader:(NSNotifcation*)aNotification {
[self removeListener];
//continue on with stuffing stuff
}
Use NSObject's performSelector function which allows it to be called after a delay.
- (void)performSelector:(SEL)aSelector withObject:(id)anArgument afterDelay:(NSTimeInterval)delay
You can even use a form of this function to run it on another thread, which is useful to not blocking the main thread when doing lengthy operations (just don't muck with the UI objects in this thread).
#DavidNeiss is correct about performSelector:withObject:afterDelay:, but you almost certainly don't want an actual time delay here. At most you want to perform your selector on the next event loop, just so things are consistent for the listener. So you should make the delay 0. This differs from the normal performSelect:withObject: which will immediately perform the selector synchronously.
-(tokenString*) loadAssetPath:(NSString*) asset {
//start asynchronous load
if (<load is actually complete>) {
// -loadComplete will execute on the next event loop
[self performSelector:#selector(loadComplete) withObject:nil afterDelay:0];
}
return nonceToken;
}
I try to solve this problem for several days now I have to ask you...
I've got a View (and a ViewController) with a UITableview. There is a TableViewController for that table which is generated in the ViewController. The TableViewController calls a DataSyncManager sharedInstant object (which is obviously in a separate class) which starts to sync data with the server.
I do it this way (first the refresh method):
-(void) refresh{
[serverQueueProgressView setProgress:0.0];
[syncingLabel setAlpha:0.5];
[serverQueueProgressView setAlpha:1];
[self performSelector:#selector(reloadTableViewDataSource) withObject:nil afterDelay:1.0];
}
Then the method reloadTableViewDataSource (of TableViewController) is called:
- (void)reloadTableViewDataSource
{
[dataSyncManager getEntriesFromServer];
}
dataSyncManager is my sharedInstance.
In the getEntriesFromServer method of dataSyncManager I do the loop with different sync items and call everytime
[[NSNotificationCenter defaultCenter]
postNotificationName:#"ServerQueueProgress"
object:progress];
with the proper progress as NSNumber (that part works well). The message is now sent and catched by my ViewController (it works, I checked with a breakpoint, it also gets the right progress-NSNumber and converts it to float):
- (void)serverQueueProgress:(NSNotification *)notification {
if(![NSThread isMainThread])
{
[self performSelectorOnMainThread:_cmd withObject:notification waitUntilDone:NO];
return;
}
[queueProgressView setProgress:[[notification object] floatValue]];
}
This is one solution which I found here on stackoverflow. But the if is always skipped because obviously I'm on main thread.
Unfortunately the UIProgressview doesn't get updated, it just hangs around, but I connected it well in Interface Builder (I checked that by setting the progress in another method of ViewController.
I also tried to catch the Notification with my TableViewController and put in some other solutions, but no chance, the UIProgressView doesn't get updated live. Only after the sync is done.
Here is the mentioned code in TableViewController which also gets executed without errors (I also stepped it to make sure every line gehts executed well):
This is the method called when received a the notification:
- (void)serverQueueProgress:(NSNotification *)notification {
[self performSelectorOnMainThread:#selector(updateProgress:) withObject:[notification object] waitUntilDone:NO];
[serverQueueProgressView setProgress:[[notification object] floatValue]];
}
Which also calls updateProgress: of the same class:
- (void)updateProgress:(NSNumber *)newProgressValue {
[serverQueueProgressView setProgress:[newProgressValue floatValue]];
}
No chance. I tried many ways and implemented some in parallel as you see, but the ProgressView won't get updated live. Only at the end of syncing. What am I doing wrong??
EDIT: Here is my getEntriesFromServer and some other stuff in DataSyncManager:
- (void)getEntriesFromServer
{
[[NSNotificationCenter defaultCenter]
postNotificationName:#"SynchingStarted"
object:nil];
[self completeServerQueue];
...
}
and completeServerQueue is the function which sends messages to my ViewController with the proper progress float value (it's only a dummy for loop, which gets executed properly... I've checked it):
- (void)completeServerQueue {
NSNumber *progress = [[NSNumber alloc] init];
for (int i=0; i<15; i++) {
progress = [[NSNumber alloc] initWithFloat:(100/15*i) ];
[[NSNotificationCenter defaultCenter]
postNotificationName:#"ServerQueueProgress"
object:progress];
sleep(1);
}
}
also, when you're having trouble, break the problem down a bit. Instead of:
[serverQueueProgressView setProgress:[[notification object] floatValue]];
do this;
float prog = [notification object] floatValue];
[serverQueueProgressView setProgress:prog];
then debugging would give a clue that this isn't working.
my guess is the problem isn't the code you've shown here, but other code in getEntriesFromServer. Are you using NSURLConnection? Something like:
NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request delegate:self];
then you will get callbacks asynchronously that you can use to update your progress view.
I have a view which loads data via an NSOperation within an NSOperationQueue. I want to allow users to leave this view before the operation has completed. My problem is that I can't seem to consistently do this without crashing. Here is my code to start the operation:
NSOperationQueue* tmpQueue = [[NSOperationQueue alloc] init];
self.queue = tmpQueue;
[tmpQueue release];
SportsLoadOperation* loadOperation = [[SportsLoadOperation alloc] init];
[loadOperation addObserver:self forKeyPath:#"isFinished" options:0 context:NULL];
[self.queue addOperation:loadOperation];
[loadOperation release];
If I leave the view while the operation is still executing, I often get this error:
[SportsViewController retain]: message sent to deallocated instance 0x38b5a0
If I try to remove the observers so that this doesn't occur, like this:
-(void)viewWillDisappear:(BOOL)animated {
if (self.isLoadingData) {
for (NSOperation *operation in [self.queue operations]) {
if([operation isExecuting]) {
[operation cancel];
[operation removeObserver:self forKeyPath:#"isFinished"];
}
}
}
[super viewWillDisappear:animated];
}
Then I sometimes get this error:
Terminating app due to uncaught exception 'NSRangeException', reason:
'Cannot remove an observer <SportsViewController 0x661c730> for the key path "isFinished" from <SportsLoadOperation 0x66201a0> because it is not registered as an observer.'
How can I avoid these problems?
The 2nd error message says it all.
Have you tried to not removeObserver after [operation cancel] and see what happens then?
Have you tried to first removeObserver and only then cancel the operation?
These might help to narrow down the conditions that trigger the error. Also, you might want to add log output to the code to see when it actually executes.
And, like freespace's answer says, adding & removing observers is best done in the construction / destruction methods of the observed instances. This generally yields more stable code.
Looks like you have an instance of SportsLoadOperation that doesn't have SportsViewController as an observer. Are you inserting SportsLoadOperation anywhere else in your code? If this is the case, consider writing an initWithObserver method for SportsLoadOperaion that will do the observing automatically. This avoids errors caused by forgetting to set the observer on isFinished.
Also, it is probably better to do the removal of observer in dealloc then in viewWillDisappear because viewWillDisappear is called in many circumstances, e.g. when displaying a modal view controller. Thus you might be prematurely stopping your operations.
Edit
Instead of checking against [operation isExecuting] check against [operation isCancelled]. This is because [operation cancel] doesn't stop an operation - it can and will continue executing if you don't actually check for isCancelled in your main method. This means that if viewWillDisappear is called twice, you could end up attempting to call removeObserver twice on the same instance of SportsLoadOperation, with the second attempt failing.
Add some debugging output statements in the following places:
when you create a SportsLoadOperation instance and insert it into the queueu
when you are cancelling a SportsLoadOperation instance and removing from it observers.
I ended up solving this by overriding the observed operation's addObserver and removeObserver methods, to keep track of observers so I could remove them in the [cancel] method.
All I have to do now is call the operation queue to cancel all operations before I dismiss the controller that was observing the operations.
Below, _observers is an NSMutableDictionary.
- (void)addObserver:(NSObject*)observer
forKeyPath:(NSString*)keyPath
options:(NSKeyValueObservingOptions)options context:(void*)context
{
[super addObserver:observer forKeyPath:keyPath options:options context:context];
[_observers setValue:observer forKey:keyPath];
}
- (void)removeObserver:(NSObject*)observer forKeyPath:(NSString*)keyPath
{
[super removeObserver:observer forKeyPath:keyPath];
[_observers removeObjectForKey:keyPath];
}
- (void)cancel
{
[super cancel];
for(id key in _observers)
{
id object = [_observers valueForKey:key];
[super removeObserver:object forKeyPath:key];
}
[_observers removeAllObjects];
}