I got an issue. The scenario is like this: I got an NSOperationQueue that contain various NSOperationQueue that need to waitUntilDone:YES. And I need to update the UI too as the queue or the operation is running. What is the best way to handle this situation?
I have tried performSelectorOnMainThread, but is it necessary to use this method every time I need to update the UI. It is seems not a good solution.
- (void)loadPreviewPageWithMagazineID:(NSString *)magazineID userID:(NSString *)userID {
NSMutableArray *operationArray = [NSMutableArray array];
for (NSInteger i = 1; i <= _numTotalPages; ++i) {
//NSLog(#"currenpage = %d, index = %d",_selectedPage,pageIndex);
NSDictionary *arguments = [NSDictionary dictionaryWithObjectsAndKeys:magazineID,
#"itemID", userID, #"userID", [NSNumber numberWithInt:i],
#"pageNumber", nil];
AFOperation *imageOperation =
[[AFOperation alloc] initWithTarget:self
selector:#selector(savePageToDisk:)
object:arguments];
[imageOperation addObserver:self forKeyPath:#"isFinished" options:0 context:nil];
[imageOperation setUserInfo:arguments];
[operationArray addObject:imageOperation];
[imageOperation release];
}
[_imageQueue addOperations:operationArray waitUntilFinished:YES];
}
- (void)processingMagazine:(NSDictionary *)arguments {
// load pdf document from decrypted data
NSString *userID = [arguments objectForKey:#"userID"];
NSString *magazineID = [arguments objectForKey:#"itemID"];
[self loadPreviewPageWithMagazineID:magazineID userID:userID];
}
So each time to update UI I need to call
[_collectionCoverView performSelectorOnMainThread:#selector(setDownloadProgress:)
withObject:[NSNumber numberWithFloat:progress]
waitUntilDone:YES];
Is there any appropriate way to handle the UI?
I didn understand much from your code. But to accomplish what you want, you can add the UI update code at the end of your AFOperation method. I mean to say, UI will be updated automatically once some processing is done, if you add it in the operation method.
Also generally UI update happens in MainThread. SO there is nothing wrong in calling performSelectorInMainThread.
Related
I'm experimenting with Firebase's FDataSnapshot to pull in data and I would like it to write its data to my core data using MagicalRecord.
According to Firebases "best practice" blog I need to keep a reference to the "handle" so it can be cleaned up later on. Further, they mention to put the FDSnapshot code in viewWillAppear.
I am wanting a callback so that when its finished doing its thing to update core data.
But I'm really note sure how to do that; its doing two things and giving a return at the same time.
// In viewWillAppear:
__block NSManagedObjectContext *context = [NSManagedObjectContext MR_context];
self.handle = [self.ref observeEventType:FEventTypeValue withBlock:^(FDataSnapshot *snapshot) {
if (snapshot.value == [NSNull null])
{
NSLog(#"Cannot find any data");
}
else
{
NSArray *snapshotArray = [snapshot value];
// cleanup to prevent duplicates
[FCFighter MR_truncateAllInContext:context];
for (NSDictionary *dict in snapshotArray)
{
FCFighter *fighter = [FCFighter insertInManagedObjectContext:context];
fighter.name = dict[#"name"];
[context MR_saveToPersistentStoreWithCompletion:^(BOOL contextDidSave, NSError *error){
if (error)
{
NSLog(#"Error - %#", error.localizedDescription);
}
}];
}
}
}];
NSFetchRequest *fr = [[NSFetchRequest alloc] initWithEntityName:[FCFighter entityName]];
fr.sortDescriptors = #[[NSSortDescriptor sortDescriptorWithKey:#"name" ascending:YES]];
self.fighterList = (NSArray *) [context executeFetchRequest:fr error:nil];
[self.tableView reloadData];
In the above code, the core data reading does not wait for the firebase to complete.
Thus, my query -- how would I best combine a completion handler so that when it is complete to update core data, and reload the tableview.
Many thanks
This is a common issue when working with Asynchronous data.
The bottom line is that all processing of data returned from an async call (in this case, the snapshot) needs to be done inside the block.
Anything done outside the block may happen before the data is returned.
So some sudo code
observeEvent withBlock { snapshot
//it is here where snapshot is valid. Process it.
NSLog(#"%#", snapshot.value)
}
Oh, and a side note. You really only need to track the handle reference when you are going to do something else with it later. Other than that, you can ignore the handles.
So this is perfectly valid:
[self.ref observeEventType:FEventTypeValue withBlock:^(FDataSnapshot *snapshot) {
//load your array of tableView data from snapshot
// and/or store it in CoreData
//reload your tableview
}
NSOperationQueue *queue = [NSOperationQueue new];
NSInvocationOperation *operation;
for(int k=0; k<[imageArray count]; k++)
{
operation = [[NSInvocationOperation alloc] initWithTarget:self selector:#selector(loadData:) object:[imageArray objectAtIndex:k]];
[queue addOperation:operation];
[operation release];
}
using above code I called the method loadData to download some images.
-(void)loadData:(NSString*)newImage
{
[CATransaction begin];
[CATransaction setDisableActions:YES];
NSData * imageData = [[NSData alloc] initWithContentsOfURL: [NSURL URLWithString:newImage]];
NSString* appSuppPath = [NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES) objectAtIndex:0];
NSString* foofile = [appSuppPath stringByAppendingPathComponent:#"/PSDB"];
NSString * str =[NSString stringWithFormat:#"%#/%d.jpeg",foofile,x];
x++;
[imageData writeToFile:str atomically:YES];
[CATransaction commit];
}
The images were downloaded and saving. The problem was the low sized images were downloaded quickly. For example if the 6th image is the low sized image, then It downloaded first and saved as 1.jpeg. how can I make it as order.
From NSOperationQueue doc
An operation queue executes its queued operation objects based on their priority and readiness. If all of the queued operation objects have the same priority and are ready to execute when they are put in the queue—that is, their isReady method returns YES—they are executed in the order in which they were submitted to the queue. For a queue whose maximum number of concurrent operations is set to 1, this equates to a serial queue. However, you should never rely on the serial execution of operation objects. Changes in the readiness of an operation can change the resulting execution order.
if setMaxConcurrentOperationCount doesnt help, try to play with -isReady method inside your custom NSInvocationOperation
When using concurrent queues, you have no assurances regarding the order that they complete. But, you really should not care. Let them complete in the most efficient manner possible.
I would have thought, though, that you want the filename to correspond to the index k of the URL. So, I'd suggest you change the download process to just use k as you initiate the request, and retire the use of x, e.g.:
NSOperationQueue *queue = [NSOperationQueue new];
queue.maxConcurrentOperationCount = 5; // limit number of concurrent requests to some reasonable number
NSString* appSuppPath = [NSSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory, NSUserDomainMask, YES) objectAtIndex:0];
NSString* foofile = [appSuppPath stringByAppendingPathComponent:#"PSDB"];
for (int k = 0; k < [imageArray count]; k++)
{
[queue addOperationWithBlock:^{
NSData * imageData = [[NSData alloc] initWithContentsOfURL: [NSURL URLWithString:[imageArray objectAtIndex:k]]];
if (imageData)
{
NSString * str = [NSString stringWithFormat:#"%#/%d.jpeg", foofile, k];
[imageData writeToFile:str atomically:YES];
}
}];
}
This eliminates the asynchronous updating of your counter, x, eliminating the file numbering problem. (This also solves a thread-safety problem in your original code: What if two downloads completed at the same time, thus both using the same value of x?) This also ensures that your files are named properly.
Now, if you really want them to complete sequentially for other reasons, you could do this in a serial queue (e.g. maxConcurrentOperationCount = 1), but there is a significant performance penalty for doing that. It's much better to design your architecture so that the completion order is not important. E.g. if you just need to know when they're done, add a completion operation dependent upon the others finishing.
Certainly, the code change above solves a tactical problem, but if there are other reasons why you think you need them to finish in the order that you queued them, let us know, because there are probably solutions that won't necessitate that and let you enjoy the performance improvement of concurrent downloads.
I am fairly new to objective c and cocoa, however i have spent a lot of time with c++, and I have never run across this issue before.
My application needs to identify a mounted disk by name. Here is the code:
//this code will run whenever a new disk is mounted to the computer
-(void) scanForKindle:(NSNotification *) notification
{
NSMutableArray *mountedDisks = [[NSMutableArray alloc] init];
mountedDisks = [workspace mountedRemovableMedia];
NSMutableArray *ebookDevices = [[NSMutableArray alloc] init];
NSDictionary *fileAttributes = [[NSDictionary alloc] init];
int currentDisk;
for (currentDisk = 0; currentDisk < [mountedDisks count]; currentDisk++)
{
NSLog(#"Name: %#", [mountedDisks objectAtIndex:currentDisk]);
if ([mountedDisks objectAtIndex:currentDisk] == #"/Volumes/Kindle")
{
NSLog(#"Kindle has been identified");
}
}
}
I have gotten everything to work perfectly up to the if statement in the for loop. It simply wont run. any ideas why? I am sure this is a simple fix, but I cannot figure this out for the life of me.
Any help would be greatly appreciated!
That's because you are making pointer comparison between two different instances of NSStrings.
Do this instead -
if ([[mountedDisks objectAtIndex:currentDisk] isEqualToString:#"/Volumes/Kindle"])
{
NSLog(#"Kindle has been identified");
}
Use NSString's -isEqualToString: method to compare strings. == just compares the addresses of the strings.
I have an NSTableView that lists tags that are stored using Core Data. The default value for a tag is 'untitled' and I need each tag to be unique, so I have a validation routine that traps empty and non-unique values and that works fine. I don't want the user to be able to store the 'untitled' value for a tag, so I am observing the NSControlTextDidEndEditingNotification, which calls the following code:
- (void)textEndedEditing:(NSNotification *)note {
NSString *enteredName = [[[note userInfo] valueForKey:#"NSFieldEditor"] string];
if ([enteredName isEqualToString:defaultTagName]) {
NSString *dString = [NSString stringWithFormat:#"Rejected - Name cannot be default value of '%#'", defaultTagName];
NSString *errDescription = NSLocalizedStringFromTable( dString, #"Tag", #"validation: default name error");
NSString *errRecoverySuggestion = NSLocalizedStringFromTable(#"Make sure you enter a unique value for the new tag.", #"Tag", #"validation: default name error suggestion");
int errCode = TAG_NAME_DEFAULT_VALUE_ERROR_CODE;
NSArray *objArray = [NSArray arrayWithObjects:errDescription, errRecoverySuggestion, nil];
NSArray *keyArray = [NSArray arrayWithObjects:NSLocalizedDescriptionKey, NSLocalizedRecoverySuggestionErrorKey, nil];
NSDictionary *eDict = [NSDictionary dictionaryWithObjects:objArray forKeys:keyArray];
NSError *error = [[NSError alloc] initWithDomain:TAG_ERROR_DOMAIN code:errCode userInfo:eDict];
NSBeep();
[preferencesWindowsController presentError:error];
unsigned long index = [self rowWithDefaultTag];
[self selectRowIndexes:[NSIndexSet indexSetWithIndex:index] byExtendingSelection:NO];
// [self editColumn:0 row:index withEvent:nil select:YES];
}
}
- (unsigned long)rowWithDefaultTag {
__block unsigned long returnInt;
[managedTags enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
if ([[obj valueForKey:#"name"] isEqualToString:defaultTagName]) {
returnInt = idx;
*stop = YES;
}
}];
return returnInt;
}
With the 'editColumn' line commented out, the code works, so if the user accepts the default tag name without editing it, the error is built, displayed and the process finishes by leaving the appropriate row in the table highlighted.
However, I would like to take it that step further and place the user in edit mode. When I uncomment the 'editColumn' line, the behaviour is not at all what I expected - the tableView loses its blue focus box and the row that respresents the new tag is blank. If I click on the tableView, the row becomes visible. I've spent a lot of time on this and have got nowhere, so some help with this would be very much appreciated.
(Note: I tried using textDidEndEditing, which also didn't behave as I expected, but that is a separate issue!)
Answering my own question. Doh!
I already had a method which I used to put the user in edit mode when they clicked the button to add a new tag:
- (void)objectAdded:(NSNotification *)note {
if ([[note object] isEqual:self]) {
[self editColumn:0 row:[self rowWithDefaultTag] withEvent:nil select:YES];
}
}
Creating a notification to call this solves the problem and places the user in edit mode correctly. The important thing is not to try to do this on the existing runloop; so sending the notification as follows postpones delivery until a later runloop:
// OBJECTADDED is a previously defined constant.
NSNotification * note = [NSNotification notificationWithName:OBJECTADDED object:self];
[[NSNotificationQueue defaultQueue] enqueueNotification: note postingStyle: NSPostWhenIdle];
Problem solved. I wasted a lot of time trying to solve this - a classic example of getting too involved in the code and not looking at what I'm trying to do.
I've forgotten where I first saw this posted - whoever you are, thank you!
I'm contemplating how to offload the drawing of a very large Core Data tree structure to CATiledLayer. CATiledLayer seems to be awesome because it performs drawing on a background thread and then fades in tiles whenever they're drawn. However, because the information of the drawing comes from a Core Data context that is by design not thread safe, I'm running into race condition issues where the drawing code needs to access the CD context.
Normally, if I need to perform background tasks with Core Data, I create a new context in the background thread and reuse the existing model and persistent store coordinator, to prevent threading issues. But the CATiledLayer does all the threading internally, so I don't know when to create the context, and there needs to be some kind of context sharing, or I can't pass the right entities to the CATiledLayer to begin with.
Is there anyone with a suggestion how I can deal with this scenario?
Cheers,
Eric-Paul.
The easiest solution is to use the dispatch API to lock all of your data access onto a single thread, while still allowing the actual drawing to be multi-threaded.
If your existing managed object context can only be accessed on the main thread, then this is what you do:
- (void)drawInContext:(CGContextRef)context // I'm using a CATiledLayer subclass. You might be using a layer delegate instead
{
// fetch data from main thread
__block NSString *foo;
__block NSString *bar;
dispatch_sync(dispatch_get_main_queue(), ^{
NSManagedObject *record = self.managedObjecToDraw;
foo = record.foo;
bar = record.bar;
});
// do drawing here
}
This is a quick and easy solution, but it will lock your main thread while fetching the data, which is almost certainly going to create "hitches" whenever a new tile is loaded while scrolling around. To solve this, you need to perform all of your data access on a "serial" dispatch queue.
The queue needs to have it's own managed object context, and you need to keep this context in sync with the context on your main thread, which is (presumably) being updated by user actions. The easiest way to do this is to observe a notification that the context has changed, and throw out the one used for drawing.
Define an instance variable for the queue:
#interface MyClass
{
NSManagedObjectContext *layerDataAccessContext;
dispatch_queue_t layerDataAccessQueue;
}
#end
Create it in your init method:
- (id)init
{
layerDataAccessQueue = dispatch_queue_create("layer data access queue", DISPATCH_QUEUE_SERIAL);
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(contextDidChange:) name:NSManagedObjectContextDidSaveNotification object:nil]; // you might want to create your own notification here, which is only sent when data that's actually being drawn has changed
}
- (void)contextDidChange:(NSNotification *)notif
{
dispatch_sync(layerDataAccessQueue, ^{
[layerDataAccessContext release];
layerDataAccessContext = nil;
});
[self.layer setNeedsDisplay];
}
And access the context while drawing:
- (void)drawInContext:(CGContextRef)context
{
// fetch data from main thread
__block NSString *foo;
__block NSString *bar;
dispatch_sync(layerDataAccessQueue, ^{
NSManagedObject record = self.managedObjectToDraw;
foo = record.foo;
bar = record.bar;
});
// do drawing here
}
- (NSManagedObject *)managedObjectToDraw
{
if (!layerDataAccessContext) {
__block NSPersistentStoreCoordinator *coordinator;
dispatch_sync(dispatch_get_main_queue(), ^{
coordinator = [self persistentStoreCoordinator];
});
layerDataAccessContext = [[NSManagedObjectContext alloc] init];
[layerDataAccessContext setPersistentStoreCoordinator:coordinator];
}
NSFetchRequest *request = [[[NSFetchRequest alloc] init] autorelease];
NSEntityDescription *entity =
[NSEntityDescription entityForName:#"Employee"
inManagedObjectContext:layerDataAccessContext];
[request setEntity:entity];
NSPredicate *predicate =
[NSPredicate predicateWithFormat:#"self == %#", targetObject];
[request setPredicate:predicate];
NSError *error = nil;
NSArray *array = [layerDataAccessContext executeFetchRequest:request error:&error];
NSManagedObject *record;
if (array == nil || array.count == 0) {
// Deal with error.
}
return [array objectAtIndex:0];
}
I've given up trying to share managed object context instances between CATiledLayer draws and now just alloc/init a new context at every call of drawLayer:inContext: The performance hit is not noticable, for the drawing is already asynchronous.
If there's anyone out there with a better solution, please share!