I have NSOperation with AFHTTPClient request. In end of operation i need to perform another N operations with requests and wait that requests will be finished to mark main operation as finished
#interface MyOperation : OBOperation
#end
#implementation MyOperation
- (id)init
{
if (self = [super init]) {
self.state = OBOperationReadyState;
}
return self;
}
- (void)start
{
self.state = OBOperationExecutingState;
AFHTTPClient *client = [[AFHTTPClient alloc] initWithBaseURL:[NSURL URLWithString:#"http://google.com"]];
[client getPath:#"/"
parameters:nil
success:^(AFHTTPRequestOperation *operation, id responseObject) {
NSOperationQueue *queue = [NSOperationQueue new];
queue.maxConcurrentOperationCount = 1;
NSMutableArray *ops = [NSMutableArray array];
for (int i = 1; i < 10; i++) {
MyInnerOperation *innerOp = [[MyInnerOperation alloc] initWithNumber:#(i)];
[ops addObject:innerOp];
}
[queue addOperations:ops waitUntilFinished:YES];
self.state = OBOperationFinishedState;
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
self.state = OBOperationFinishedState;
NSLog(#"error");
}];
}
#end
Link to OBOperation source at end of question. It's a simple class that add useful methods to control NSOperation flow
Sample of Inner Operation:
#interface MyInnerOperation : OBOperation
- (id)initWithNumber:(NSNumber *)number;
#end
#implementation MyInnerOperation
- (id)initWithNumber:(NSNumber *)number
{
if (self = [super init]) {
_number = number;
self.state = OBOperationReadyState;
}
return self;
}
- (void)start
{
self.state = OBOperationExecutingState;
NSLog(#"begin inner operation: %#", _number);
AFHTTPClient *client = [[AFHTTPClient alloc] initWithBaseURL:[NSURL URLWithString:#"http://google.com"]];
[client getPath:#"/"
parameters:nil
success:^(AFHTTPRequestOperation *operation, id responseObject) {
NSLog(#"inner operation success: %#", _number);
self.state = OBOperationFinishedState;
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
self.state = OBOperationFinishedState;
NSLog(#"inner operation error: %#", _number);
}];
}
#end
So if i begin my operation:
MyOperation *op = [MyOperation new];
[_queue addOperation:op];
I see in console begin inner operation: 1 and that's all! My app totally freeze (even UI)
After some exploration i decide that freeze caused by [queue addOperations:ops waitUntilFinished:YES];. If i don't wait for finish, my inner operations work as expected, but MyOperation finished before child operations will be completed.
So now i have workaround with dependent block operation:
NSBlockOperation *endOperation = [NSBlockOperation blockOperationWithBlock:^{
self.state = OBOperationFinishedState;
}];
NSMutableArray *ops = [NSMutableArray arrayWithObject:endOperation];
for (int i = 1; i < 10; i++) {
MyInnerOperation *innerOp = [[MyInnerOperation alloc] initWithNumber:#(i)];
[ops addObject:innerOp];
[endOperation addDependency:innerOp];
}
[queue addOperations:ops waitUntilFinished:NO];
But i still totally don't understand what's real problem of this freeze. Any explanation will be very useful.
OBOperaton class source: https://dl.dropboxusercontent.com/u/1999619/issue/OBOperation.h https://dl.dropboxusercontent.com/u/1999619/issue/OBOperation.m
Whole project: https://dl.dropboxusercontent.com/u/1999619/issue/OperationsTest.zip
The reason you're deadlocking is that AFNetworking dispatches the completion blocks to the main queue. Therefore, waitUntilFinished in that first success handler will block the main queue until the subordinate requests finish. But those subordinate requests cannot finish because they need to dispatch their completion blocks to the main queue, which the first operation is still blocking.
Clearly, you never want to block the main queue anyway, but you receive a deadlock if you block the main queue waiting for operations, which, themselves, need the main queue.
Related
I'm having problem replacing this code.
-(NSMutableArray *) GetPrices: {
NSError *error;
NSURLResponse *response;
NSData *tw_result = [NSURLConnection
sendSynchronousRequest:urlRequest
returningResponse:&response error:&error];
The problem I have is that the function that calls this code process the url and then returns data to a method that calls it.
Previously I used this like so.
ViewController calls a function to gather data by creating an operation queue (so that the UI & main thread are available)
NSOperationQueue *myQueue = [NSOperationQueue new];
NSInvocationOperation *operation = [[NSInvocationOperation alloc]
initWithTarget:self selector:#selector(loadDataWithOperation) object:nil];
[myQueue addOperation:operation];
[operation release];
[myQueue release];
The function in the operation queue calls the method to get data on an object and that method then runs the synchronous URLrequest.
-(void)loadDataWithOperation {
self.sectionPriceArray = [self.myObject GetPrices];
So myObject would return a price array.
I have tried using NSSession but but I can't figure out how to pass the result back as the method terminates prior to getting tw_result from the completion Handler.
Any thoughts I have to do this in Objective C as I don't have permission from the client to convert to swift.
EDIT of Question with more details:
Inside my GetPrices method I have tried
NSURLSession *session = [NSURLSession sharedSession];
[[session dataTaskWithRequest:urlRequest
completionHandler:^(NSData *tw_result,
NSURLResponse *response,
NSError *error) {
result = [[NSString alloc] initWithData:tw_result encoding:NSUTF8StringEncoding];
NSArray *resultArray = [result componentsSeparatedByString:#"\n"];
}) resume];
But I cannot figure out how to make this work one level up at the calling level.
As #maddy mentioned you're going to want to use a completion block for your getPrices method instead of a return -- returns + async don't mix.
This would be the general form to convert your getPrices method to:
- (void)_getPricesWithCompletion:(void(^)(NSMutableArray *sectionPriceArray))priceCompletion;
This site: http://goshdarnblocksyntax.com has some of the common block syntax declaration usages.
Typically you'd call this async method and then set your iVar in the completion block and reload your associated UI elements after receiving the new data. Something along these lines:
[self _getPricesWithCompletion:^(NSMutableArray *sectionPriceArray) {
self.sectionPriceArray = sectionPriceArray;
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
// reload any UI elements dependent on your sectionPriceArray here
}];
}];
Now in the example code you show, it seems like you're using an NSOperationQueue to queue up different operations. Things can get a bit more complicated here. Subsequent queue'd operations won't wait on your async operations to finish before executing. So for example, if you have an operation after the getPrices operation which utilizes the result of the fetch of the prices, the iVar will almost definitely not contain the correct data at that point. In this case you'd need to use some sort of semaphore to handle waiting for the async operation to complete before continuing to the operation which depends upon it.
Here's an example of what I mean:
NotProperlyWaiting.h
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
#interface NotProperlyWaiting : NSObject
#property (strong, nullable) NSMutableArray *sectionPriceArray;
- (void)callOperations;
- (void)fakeServerCallWithCompletion:(void(^)(NSData *tw_result, NSURLResponse *response, NSError *error))completion;
#end
NotProperlyWaiting.m
#import "NotProperlyWaiting.h"
#interface NotProperlyWaiting()
- (void)_getPricesWithCompletion:(void(^)(NSMutableArray *sectionPriceArray))priceCompletion;
- (void)_printPricesArray;
#end
#implementation NotProperlyWaiting
- (instancetype)init {
self = [super init];
if (self) {
_sectionPriceArray = [NSMutableArray array];
}
return self;
}
- (void)callOperations {
// setup our completion block to be passed in (this is what will eventually set the self.sectionPricesArray
void (^pricesCompletion)(NSMutableArray *) = ^ void (NSMutableArray *sectionPricesArrayFromCompletion){
self.sectionPriceArray = sectionPricesArrayFromCompletion;
};
NSOperationQueue *myQueue = [NSOperationQueue new];
NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self selector:#selector(_getPricesWithCompletion:) object:pricesCompletion];
NSInvocationOperation *printOperation = [[NSInvocationOperation alloc] initWithTarget:self selector:#selector(_printPricesArray) object:nil];
[myQueue addOperation:operation];
[myQueue addOperation:printOperation];
}
- (void)_getPricesWithCompletion:(void(^)(NSMutableArray *sectionPricesArray))priceCompletion {
[self fakeServerCallWithCompletion:^(NSData *tw_result, NSURLResponse *response, NSError *error) {
// check the error or response or whatever else to verify that the data is legit from your server endpoint here
// then convert the data to your mutable array and pass it through to our completion block
NSString *stringData = [[NSString alloc] initWithData:tw_result encoding:NSUTF8StringEncoding];
NSMutableArray *tempPricesArray = [NSMutableArray arrayWithArray:[stringData componentsSeparatedByString:#"\n"]];
// now our completion block passing in the result prices array
priceCompletion(tempPricesArray);
}];
}
- (void)_printPricesArray {
NSLog(#"NotWaiting -- Prices array : %#", self.sectionPriceArray);
}
// this is a fake version of NSURLSession
- (void)fakeServerCallWithCompletion:(void(^)(NSData *tw_result, NSURLResponse *response, NSError *error))completion {
NSString *fakeServerResponse = #"FirstThing\nSecondThing\nThirdThing";
NSData *fakeData = [fakeServerResponse dataUsingEncoding:NSUTF8StringEncoding];
NSURLResponse *fakeResponse = [[NSURLResponse alloc] init];
NSError *fakeError = [NSError errorWithDomain:#"FakeErrorDomain" code:33 userInfo:nil];
// never call sleep in your own code, this is just to simulate the wait time for the server to return data
sleep(3);
completion(fakeData,fakeResponse,fakeError);
}
NS_ASSUME_NONNULL_END
ProperlyWaiting.h (Subclass of NotProperlyWaiting.h to re-use callOperation and fakeServerCallWithCompletion:)
#import "NotProperlyWaiting.h"
NS_ASSUME_NONNULL_BEGIN
#interface ProperlyWaiting : NotProperlyWaiting
#end
NS_ASSUME_NONNULL_END
ProperlyWaiting.m
#import "ProperlyWaiting.h"
#interface ProperlyWaiting()
- (void)_getPricesWithCompletion:(void(^)(NSMutableArray *sectionPricesArray))priceCompletion;
- (void)_printPricesArray;
#property dispatch_semaphore_t semaphore;
#end
#implementation ProperlyWaiting
- (void)callOperations {
self.semaphore = dispatch_semaphore_create(0);
[super callOperations];
}
// identical implementations to NotProperlyWaiting, but this time we'll use a semaphore to ensure the _printPricesArray waits for the async operation to complete before continuing
- (void)_getPricesWithCompletion:(void(^)(NSMutableArray *sectionPricesArray))priceCompletion {
[self fakeServerCallWithCompletion:^(NSData *tw_result, NSURLResponse *response, NSError *error) {
// check the error or response or whatever else to verify that the data is legit from your server endpoint here
// then convert the data to your mutable array and pass it through to our completion block
NSString *stringData = [[NSString alloc] initWithData:tw_result encoding:NSUTF8StringEncoding];
NSMutableArray *tempPricesArray = [NSMutableArray arrayWithArray:[stringData componentsSeparatedByString:#"\n"]];
// now our completion block passing in the result prices array
priceCompletion(tempPricesArray);
// signal our semaphore to let it know we're done
dispatch_semaphore_signal(self.semaphore);
}];
}
- (void)_printPricesArray {
// wait for the semaphore signal before continuing (so we know the async operation we're waiting on has completed)
dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
NSLog(#"Waiting -- Prices array : %#", self.sectionPriceArray);
}
#end
With example calls of the class like this:
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
NotProperlyWaiting *notWaiting = [[NotProperlyWaiting alloc] init];
[notWaiting callOperations];
ProperlyWaiting *waiting = [[ProperlyWaiting alloc] init];
[waiting callOperations];
}
The output in the log will be:
NotWaiting -- Prices array : (
)
And then 3 seconds later:
Waiting -- Prices array : (
FirstThing,
SecondThing,
ThirdThing
)
Some additional links to helpful documentation related to this topic:
https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/WorkingwithBlocks/WorkingwithBlocks.html
https://developer.apple.com/documentation/dispatch/1452955-dispatch_semaphore_create
I'm profiling (Leaks) my app (ARC) and it's showing several memory leaks which I can't figure out.
One object (ArchivingTasksManager) kicks off a method that creates many NSOperations within a for in loop, and adds them to a queue. Each NSOperation is a custom subclass (DownloadImageOperation) that I instantiate with an NSURL.
In ArchivingTasksManager:
- (void)archiveAllImages
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self beginBackgroundTask];
[self archiveImages];
});
}
- (void)archiveImages
{
self.queue.suspended = true;
self.queue.maxConcurrentOperationCount = 1;
for (Highlight *highlight in self.list.highlights) {
for (Experience *experience in highlight.experiences) {
for (NSURL *imageURL in [experience allImageURLsOfType:ExperienceImageType640x640]) {
DownloadImageOperation *experienceImageDownload = [[DownloadImageOperation alloc] initWithImageURL:imageURL];
[self.queue addOperation:experienceImageDownload];
}
NSURL *authorURL = [NSURL URLWithString:experience.author.image_250x250Url];
if (authorURL) {
DownloadImageOperation *authorImageDownload = [[DownloadImageOperation alloc] initWithImageURL:authorURL];
[self.queue addOperation:authorImageDownload];
}
}
MapSnapshotOperation *mapSnapshot = [[MapSnapshotOperation alloc] initWithHighlight:highlight];
[self.queue addOperation:mapSnapshot];
}
[self.queue addObserver:self
forKeyPath:NSStringFromSelector(#selector(operationCount))
options:NSKeyValueObservingOptionInitial context:nil];
self.totalOperations = self.queue.operationCount;
self.queue.suspended = false;
}
In DownloadImageOperation:
- (instancetype)initWithImageURL:(NSURL *)imageURL;
{
self = [super init];
if (!self) {
return nil;
}
_imageURL = imageURL;
_targetURL = [[ArchivingOperation class] localImageURLFromRemoteImagePath:imageURL.absoluteString];
NSURLSessionConfiguration *config = [NSURLSessionConfiguration ephemeralSessionConfiguration];
_session = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:nil];
_downloadTask = [_session downloadTaskWithURL:imageURL];
return self;
}
Here's what Instruments shows me in ArchivingTasksManager:
And here's what Instruments shows me in DownloadImageOperation:
I have an app which downloads some files from the server in few threads. The problems is that it is giving a heavy load to the CPU (hitting to 80%). What can be done to make it better? I made similar app on Windows with C#, and the cpu usage never goes above 5%.
EDIT: This code has been changed after getting some suggestions below. The problem now is, that the download never reaches 100% when I set [queue setMaxConcurrentOperationCount:6]. If I change the asynchronous NSURLConnection back to sendSynchronous call it works, when I change the above OperationCount to 1, also works.
This is how I add NSOperations to the queue (may be large, like 800).
int chunkId = 0;
for (DownloadFile *downloadFile in [download filesInTheDownload])
{
chunkId = 0;
for (DownloadChunk *downloadChunk in [downloadFile chunksInTheFile])
{
DownloadChunkOperation *operation = [[DownloadChunkOperation alloc] initWithDownloadObject:download
downloadFile:downloadFile downloadChunk:downloadChunk andChunkId:chunkId];
[queue addOperation:operation];
chunkId++;
}
}
#import "DownloadChunkOperation.h"
#import "Download.h"
#import "DownloadFile.h"
#import "DownloadChunk.h"
#interface DownloadChunkOperation()
#property(assign) BOOL isExecuting;
#property(assign) BOOL isFinished;
#end
#implementation DownloadChunkOperation
#synthesize download = _download;
#synthesize downloadFile = _downloadFile;
#synthesize downloadChunk = _downloadChunk;
#synthesize isFinished = _isFinished;
#synthesize isExecuting = _isExecuting;
- (id) initWithDownloadObject:(Download *)download downloadFile:(DownloadFile *)downloadFile downloadChunk:(DownloadChunk *)downloadChunk andChunkId:(uint32_t)chunkId
{
self = [super init];
if (self) {
self.download = download;
self.downloadFile = downloadFile;
self.downloadChunk = downloadChunk;
self.chunkId = chunkId;
}
return self;
}
- (void) start
{
if ([self isCancelled]) {
[self setIsFinished:YES];
[self setIsExecuting:NO];
return;
}
[self setIsExecuting:YES];
[self setIsFinished:NO];
[self.downloadChunk setChunkState:cDownloading];
downloadPath = [[NSString stringWithFormat:#"%#/%#", [self.download downloadFolder], [self.download escapedTitle]] stringByExpandingTildeInPath];
NSURL *fileURL = [[NSURL alloc] initWithString:[self.downloadFile filePath]];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:fileURL];
NSString *range = [NSString stringWithFormat:#"bytes=%lli-%lli", [self.downloadChunk startingByte], [self.downloadChunk endingByte]];
[request setValue:range forHTTPHeaderField:#"Range"];
connection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:NO];
// IMPORTANT! The next line is what keeps the NSOperation alive for the during of the NSURLConnection!
[connection scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
[connection start];
if (connection) {
NSLog(#"connection established!");
do {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
} while (!self.isFinished);
} else {
NSLog(#"couldn't establish connection for: %#", fileURL);
}
}
- (BOOL) isConcurrent
{
return YES;
}
- (void) connection:(NSURLConnection *)_connection didReceiveResponse:(NSURLResponse *)response
{
receivedData = [[NSMutableData alloc] init];
}
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
// Not cancelled, receive data.
if (![self isCancelled]) {
[receivedData appendData:data];
self.download.downloadedBytes += [data length];
return;
}
// Cancelled, tear down connection.
[self setIsExecuting:NO];
[self setIsFinished:YES];
[self.downloadChunk setChunkState:cConnecting];
[self->connection cancel];
}
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
[self setIsExecuting:NO];
[self setIsFinished:YES];
NSLog(#"Connection failed! Error - %# %#",
[error localizedDescription],
[[error userInfo] objectForKey:NSURLErrorFailingURLStringErrorKey]);
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
NSString *chunkPath = [downloadPath stringByAppendingFormat:#"/%#.%i", [self.downloadFile fileName], self.chunkId];
NSError *saveError = nil;
[receivedData writeToFile:chunkPath options:NSDataWritingAtomic error:&saveError];
if (saveError != nil) {
NSLog(#"Download save failed! Error: %#", [saveError description]);
}
else {
NSLog(#"file has been saved!: %#", chunkPath);
}
[self setIsExecuting:NO];
[self setIsFinished:YES];
[self.downloadChunk setChunkState:cFinished];
if ([self.download downloadedBytes] == [self.download size])
[[NSNotificationCenter defaultCenter] postNotificationName:#"downloadFinished" object:self.download];
}
#end
You should not create threads yourself. Use dedicated API like NSOperationQueue or even GCD directly for this purpose. They know better about hardware limits, virtual cores, etc. and support priority settings.
You shouldn't use +sendSynchronousRequest: either. Wrapping your -downloadChunk method in a dispatch call as suggested by charith won't help you improve performance, as +sendSynchronousRequest: blocks the thread until new data comes in and forces GCD to spawn new threads.
Use the asynchronous API of NSURLConnection using delegate callbacks. You can also wrap your NSURLConnection code inside a NSOperation subclass and use NSOperationQueue to manage the downloads: Using NSURLConnections
If you don't want to write the NSOperation subclass yourself, you can also use a 3rd party framework like AFNetworking.
Try with GCD blocks and global queues. This is the apple recommended way now for concurrency ex:
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(globalQueue, ^{
[self downloadChunk:objDownload];
});
My app is working this way :
- create an album and take pictures
- send them on my server
- get an answer / complementary information after picture analysis.
I have some issue with the sending part. Here is the code
#interface SyncAgent : NSObject <SyncTaskDelegate>
#property NSOperationQueue* _queue;
-(void)launchTasks;
#end
#implementation SyncAgent
#synthesize _queue;
- (id)init
{
self = [super init];
if (self) {
self._queue = [[NSOperationQueue alloc] init];
[self._queue setMaxConcurrentOperationCount:1];
}
return self;
}
-(void) launchTasks {
NSMutableArray *tasks = [DataBase getPendingTasks];
for(Task *t in tasks) {
[self._queue addOperation:[[SyncTask alloc] initWithTask:t]];
}
}
#end
and the SyncTask :
#interface SyncTask : NSOperation
#property (strong, atomic) Task *_task;
-(id)initWithTask:(Task *)task;
-(void)mainNewID;
-(void)mainUploadNextPhoto:(NSNumber*)photoSetID;
#end
#implementation SyncTask
#synthesize _task;
-(id)initWithTask:(Task *)task {
if(self = [super init]) {
self._task = task;
}
return self;
}
-(void)main {
NSLog(#"Starting task : %#", [self._task description]);
// checking if everything is ready, sending delegates a message etc
[self mainNewID];
}
-(void)mainNewID {
__block SyncTask *safeSelf = self;
[[WebAPI sharedClient] createNewPhotoSet withErrorBlock:^{
NSLog(#"PhotoSet creation : error")
} andSuccessBlock:^(NSNumber *photoSetID) {
NSLog(#"Photoset creation : id is %d", [photoSetID intValue]);
[safeSelf mainUploadNextPhoto:photoSetID];
}];
}
-(void)mainUploadNextPhoto:(NSNumber*) photoSetID {
//just admit we have it. won't explain here how it's done
NSString *photoPath;
__block SyncTask *safeSelf = self;
[[WebAPI sharedClient] uploadToPhotosetID:photoSetID withPhotoPath:photoPath andErrorBlock:^(NSString *photoPath) {
NSLog(#"Photo upload error : %#", photoPath);
} andSuccessBlock:^(NSString *photoPath) {
NSLog(#"Photo upload ok : %#", photoPath);
//then we delete the file
[safeSelf mainUploadNextPhoto:photoSetID];
}];
}
#end
Every network operations are done using AFNetworking this way :
-(void)myDummyDownload:(void (^)(NSData * data))successBlock
{
AFHTTPClient* _httpClient = [[AFHTTPClient alloc] initWithBaseURL:[NSURL URLWithString:#"http://www.google.com/"]];
[_httpClient registerHTTPOperationClass:[AFHTTPRequestOperation class]];
NSMutableURLRequest *request = [_httpClient requestWithMethod:#"GET" path:#"/" nil];
[request setValue:#"application/x-www-form-urlencoded" forHTTPHeaderField:#"Content-Type"];
AFHTTPRequestOperation *operation = [_httpClient HTTPRequestOperationWithRequest:(NSURLRequest *)request
success:^(AFHTTPRequestOperation *operation, id data) {
if(dataBlock)
dataBlock(data);
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"Cannot download : %#", error);
}];
[operation setShouldExecuteAsBackgroundTaskWithExpirationHandler:^{
NSLog(#"Request time out");
}];
[_httpClient enqueueHTTPRequestOperation:operation];
}
My problem is : my connections are made asynchronously, so every task are launched together without waiting fo the previous to finish even with [self._queue setMaxConcurrentOperationCount:1] in SyncAgent.
Do I need to perform every connection synchronously ? I don't think this is a good idea, because a connection never should be done this way and also because I might use these methods elsewhere and need them to be performed in background, but I cannot find a better way. Any idea ?
Oh and if there is any error/typo in my code, I can assure you it appeared when I tried to summarize it before pasting it, it is working without any problem as of now.
Thanks !
PS: Sorry for the long pastes I couldn't figure out a better way to explain the problem.
EDIT: I found that using a semaphore is easier to set up and to understand : How do I wait for an asynchronously dispatched block to finish?
If you have any control over the server at all, you should really consider creating an API that allows you to upload photos in an arbitrary order, so as to support multiple simultaneous uploads (which can be quite a bit faster for large batches).
But if you must do things synchronized, the easiest way is probably to enqueue new requests in the completion block of the requests. i.e.
// If [operations length] == 1, just enqueue it and skip all of this
NSEnumerator *enumerator = [operations reverseObjectEnumerator];
AFHTTPRequestOperation *currentOperation = nil;
AFHTTPRequestOperation *nextOperation = [enumerator nextObject];
while (nextOperation != nil && (currentOperation = [enumerator nextObject])) {
currentOperation.completionBlock = ^{
[client enqueueHTTPRequestOperation:nextOperation];
}
nextOperation = currentOperation;
}
[client enqueueHTTPRequestOperation:currentOperation];
The following code works for me, but I am not sure of the drawbacks. Take it with a pinch of salt.
- (void) main {
NSCondition* condition = [[NSCondition alloc] init];
__block bool hasData = false;
[condition lock];
[[WebAPI sharedClient] postPath:#"url"
parameters:queryParams
success:^(AFHTTPRequestOperation *operation, id JSON) {
//success code
[condition lock];
hasData = true;
[condition signal];
[condition unlock];
}
failure:^(AFHTTPRequestOperation *operation, NSError *error) {
//failure code
[condition lock];
hasData = true;
[condition signal];
[condition unlock];
}];
while (!hasData) {
[condition wait];
}
[condition unlock];
}
I am writing a Core Data ContextManager for a larger iOS application. A ContextManager provides an NSManagedContext that is automatically updated when other ContextManagers save their NSMangedContext to the persistent data store.
I have a unit test (TestContextManager) that creates two contexts, adds an object to one, and tests to see if the object appears in the other context. It doesn't. Why does the last test fail?
Here's the code for a ContextManager and the failing unit test. The last assert in the unit test fails. Every other assert passes. As you can see, the ContextManager relies upon getting a change notification from a different ContextManager and using mergeChangesFromContextDidSaveNotification to update itself. Notice that everything happens on the same thread for this test.
I know the NSManagedObjectContextDidSaveNotification is being sent and received correctly. I know the NSManagedObjectContextDidSaveNotification has the correct data in its userInfo dictionary.
I have also run this unit test as an application test on an actual device using an SQLite persistent store -- the same assert fails.
Thanks in advance!
ContextManager:
#import "ContextManager.h"
#implementation ContextManager
#synthesize context;
#pragma mark - Custom code
- (void)save {
NSError *error = nil;
if (self.context != nil) {
if ([self.context hasChanges] && ![self.context save:&error]) {
NSAssert1(FALSE, #"Unable to save the managed object context. UserInfo:\n%#", [error userInfo]);
}
}
return;
}
- (void)mergeChanges:(NSNotification *)notification {
if (notification.object != self.context) {
[self.context mergeChangesFromContextDidSaveNotification:notification];
}
return;
}
#pragma mark - Overridden NSObject methods
#pragma mark Creating, copying, and deallocating object
- (id)initWithPersistentStoreCoordinator:(NSPersistentStoreCoordinator *)persistentStoreCoordinator {
self = [super init];
if (self) {
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(mergeChanges:) name:NSManagedObjectContextDidSaveNotification object:nil];
self.context = [[[NSManagedObjectContext alloc] init] autorelease];
[self.context setPersistentStoreCoordinator:persistentStoreCoordinator];
}
return self;
}
- (void)dealloc {
[context release];
[super dealloc];
return;
}
#end
TestContextManager:
#import "TestContextManager.h"
#import "ContextManager.h"
#import "CoreDataManager.h"
#define TEST_MANAGED_OBJECT #"AManagedObject"
#implementation TestContextManager
- (void)testContextManager {
CoreDataManager *coreDataManager = [[CoreDataManager alloc] init];
coreDataManager.storeType = NSInMemoryStoreType;
ContextManager *contextManagerA = [coreDataManager provideContextManager];
if (!contextManagerA) STFail(#"CoreDataManager did not provide a context manager.");
NSManagedObjectContext *contextA = contextManagerA.context;
if (!contextA) STFail(#"ContextManager did not provide a managed object context.");
// setA1 has 0 objects (or whatever is initially there).
NSSet *setA1 = [contextManagerA.context registeredObjects];
[NSEntityDescription insertNewObjectForEntityForName:TEST_MANAGED_OBJECT inManagedObjectContext:contextManagerA.context];
// setA2 has 1 object.
NSSet *setA2 = [contextManagerA.context registeredObjects];
STAssertTrue([setA2 count] == [setA1 count]+1, #"Context provided by ContextManager is not accepting new objects.");
[contextManagerA save];
ContextManager *contextManagerB = [coreDataManager provideContextManager];
[NSEntityDescription insertNewObjectForEntityForName:TEST_MANAGED_OBJECT inManagedObjectContext:contextManagerB.context];
[contextManagerB save];
NSSet *setA3 = [contextManagerA.context registeredObjects];
// setA3 should have 2 objects <=== THIS TEST FAILS
STAssertTrue([setA3 count] == [setA1 count]+2, #"Context is not updating new objects.");
[coreDataManager release];
return;
}
#end
Did you actually set up the ContextManager to observe the notification for saving a managedObjectContext? You don't show that here so I just wanted to cover the simplest case.
Sorry, I should have made this a comment on Erik's post.
Thanks to littleknown for answering my question. Clearly I need to do some reading on what registeredObjects actually returns. I guess the good news here is that the actual code works -- the unit test was bad...
Here's the unit test that correctly exercises the unit under test AND passes:
#import "TestContextManager.h"
#import "ContextManager.h"
#import "CoreDataManager.h"
#define TEST_MANAGED_OBJECT #"AManagedObject"
#implementation TestContextManager
- (void)testContextManager {
CoreDataManager *coreDataManager = [[CoreDataManager alloc] init];
coreDataManager.storeType = NSInMemoryStoreType;
ContextManager *contextManagerA = [coreDataManager provideContextManager];
if (!contextManagerA) STFail(#"CoreDataManager did not provide a context manager.");
NSManagedObjectContext *contextA = contextManagerA.context;
if (!contextA) STFail(#"ContextManager did not provide a managed object context.");
NSEntityDescription *entityDescriptionA = [NSEntityDescription entityForName:TEST_MANAGED_OBJECT inManagedObjectContext:contextA];
// make A1 request on an empty context (0 objects)
NSFetchRequest *requestA1 = [[NSFetchRequest alloc] init];
[requestA1 setEntity:entityDescriptionA];
NSError *errorA1 = nil;
NSArray *arrayA1 = [contextA executeFetchRequest:requestA1 error:&errorA1];
if (arrayA1 == nil) STFail(#"Fetch request A1 failed.");
if ([arrayA1 count] != 0) STFail(#"Context A1 is not empty at start of test.");
// add an object to context A and make request A2
[NSEntityDescription insertNewObjectForEntityForName:TEST_MANAGED_OBJECT inManagedObjectContext:contextManagerA.context];
NSFetchRequest *requestA2 = [[NSFetchRequest alloc] init];
[requestA2 setEntity:entityDescriptionA];
NSError *errorA2 = nil;
NSArray *arrayA2 = [contextA executeFetchRequest:requestA2 error:&errorA2];
if (arrayA2 == nil) STFail(#"Fetch request A2 failed.");
if ([arrayA2 count] != 1) STFail(#"Context A2 did not successfully add an object.");
// add an object to context B and make request B1
ContextManager *contextManagerB = [coreDataManager provideContextManager];
NSManagedObjectContext *contextB = contextManagerB.context;
NSEntityDescription *entityDescriptionB = [NSEntityDescription entityForName:TEST_MANAGED_OBJECT inManagedObjectContext:contextB];
[NSEntityDescription insertNewObjectForEntityForName:TEST_MANAGED_OBJECT inManagedObjectContext:contextManagerB.context];
NSFetchRequest *requestB1 = [[NSFetchRequest alloc] init];
[requestB1 setEntity:entityDescriptionB];
NSError *errorB1 = nil;
NSArray *arrayB1 = [contextB executeFetchRequest:requestB1 error:&errorB1];
if (arrayB1 == nil) STFail(#"Fetch request B1 failed.");
if ([arrayB1 count] != 1) STFail(#"Context B1 did not successfully add an object.");
// save contextB
[contextManagerB save];
// check if contextA was updated
NSFetchRequest *requestA3 = [[NSFetchRequest alloc] init];
[requestA3 setEntity:entityDescriptionA];
NSError *errorA3 = nil;
NSArray *arrayA3 = [contextA executeFetchRequest:requestA3 error:&errorA3];
if (arrayA3 == nil) STFail(#"Fetch request A3 failed.");
if ([arrayA3 count] != 2) STFail(#"Context A did not update correctly.");
[requestA1 release];
[requestA2 release];
[requestB1 release];
[requestA3 release];
[coreDataManager release];
return;
}
#end