I'm trying to give a solution.
First off, your code as it stands now will not run. Furthermore, although I think I have understood the principal problem, I'm not fully understanding the details - in particular all parameters and also how ANK works. Nonetheless, these details seem to be irrelevant for an example solution.
The Problem:
So, it seems you repeatedly have to invoke an asynchronous task which shall execute in serial (not in parallel), thereby consuming data from a stream until it reaches the end. Furthermore, you want to execute completion handlers on a well known execution context (say, the main thread, or a particular queue).
Additionally, it occurs to me, both your methods reloadPosts
and refreshPostsInPart
are asynchronous.
Note:
Each asynchronous method or operation shall have a means to signal completion to the call-site. In many cases this is realized with a completion handler, but there are other approaches, too.
A method invoking another method or operation which is asynchronous becomes itself asynchronous. Otherwise, if the calling method is synchronous this is an indication of a code smell.
So, taking a look at your methods:
reloadPosts
- It does not have a completion hander :(
- Strangely, it seems it incorporates the asynchronous task AND the completion handler.
refreshPostsInPart
- It does not have a completion handler. Uhm, stop - it isn't asynchronous! But wait, it actually is asynchronous but you forced it to become synchronous through using a semaphore that blocks the call-site's thread until it is finished: big code smell :)
A Possible Solution:
Luckily, your code can be restructured and greatly simplified.
First, a generic completion handler could be declared as follows:
typedef void (^completion_block_t)(id result, NSError* error);
Your method refreshPostsInPart
basically should implement the above Problem. WHEN the task is finished, it should signal the call-site this event through calling the completion handler. The call-site provides the completion handler and defines WHAT to do when that happens:
- (void) refreshPostsInPart:(StreamPart*)part completion:(completion_block_t);
Note: there is no return value! However, you get the result from the completion handler.
Now, since it is unclear to me what you actually want to accomplish, the subsequent solution becomes abstract - but I hope you know how you can "map" this abstract solution to your actual problem.
Furthermore, I'm going to use a third party library which implements the concept of a "Promise". A promise represents the eventual result of an asynchronous task - including an error if that happens. A promise is just another approach to signal completion (or error) to the call-site:
- (RXPromise*)refreshPostsInPart:(StreamPart*)part;
Note, there is no completion handler, instead the method returns a "Promise". The method is actually asynchronous and returns immediately as usual that is a Promise object. Though the promise is not yet "resolved" - that is, it doesn't contain a result yet - even not an error. The promise MSUT be eventually resolved by the asynchronous task. Once that happened, a call-site can obtain the result of the task.
With utilizing promises, you can remove your NSOperationQueues, NSOperations and also the blocks in your solution. RXPromise supports cancellation (not shown in this example) and Promises do also have a much more powerful concept to "chain" operations (also not shown in every detail here in this example). I would appreciate if you read the docs, and possibly consult the web for further information ;)
When that promise gets eventually resolved by the asynchronous task it has been either fulfilled with the result or rejected with an error. Then your call-site likely want to do something with the result. You accomplish this with registering success and failure handlers:
[self refreshPostsInPart:part]
.then(^id(id result){
// On Success:
NSLog(@"This is the result: %@", result);
return nil;
}, ^id (NSError* error){
// On Failure:
NSLog(@"Error: %@", error);
return nil;
});
That is, you "setup" your success and/or failure handler with that then
expression:
[self refreshPostsInPart:part].then(<success-handler>, <failure-handler>);
which is the short form of:
RXPromise* resultPromise = [self refreshPostsInPart:part];
resultPromise.then(<success-handler>, <error-handler>);
Note that - here - the completion handlers will be executed on some private execution context. However, in RXPromise you can explicitly define what queue to use where the handlers get executed, using thenOn
, for example:
resultPromise.thenOn(dispatch_queue_get_main_queue(),
^id(id result){
// do something with result on the main queue
return nil;
}, nil /*error handler not used*/);
Now, we can take a look at the implementation of the method refreshPostsInPart:
:
Suppose, your class StreamPart
will be represented as a NSArray
in this solution which mimics a rather simple "stream" of objects.
The solution also utilizes a class method from the RXPromise library repeat
which greatly simplifies an "asynchronous loop":
repeat
is declared as follows:
typedef RXPromise* (^rxp_nullary_task)();
+ (RXPromise*) repeat:(rxp_nullary_task)block;
It asynchronously executes the block in a continuous loop until the block returns nil
or the returned promise will be rejected.
Note, repeat
is itself asynchronous, thus it returns a promise.
So a canonical implementation for a repeat
would look like:
RXPromise* allFinishedPromise = [RXPromise repeat:^RXPromise*{
if (no more input) {
return nil; // terminate the asynchronous loop
}
return [input asyncTask]; // asynchronously execute the next task with input
}];
and possible usage is:
allFinishedPromise.then(^id(id result){
// in case of success, the result of repeat is always @"OK"
},
^id(NSError* error){
// if an error occurred, this is the error of the failing task
});
So, we also need that "task" which is basically the code which executes per iteration, and this is an asynchronous task (thus, it returns a promise):
-(RXPromise*) asyncTask;
This basically implements your reloadPosts
. I'll omit this implementation for now.
Note that your original implementation contained the completion handler code. We do that not. Instead we register a success and failure handler as usual.
Furthermore you likely want to continue with some particular code which gets executed thereafter once a handler has been finished. Maybe your handler itself is an asynchronous method! You can do such things in a concise manner, for example:
Suppose, you have implemented asyncTask
(which basically implements the task part of your reloadPosts
method), and then when that finished want to execute another asynchronous method, and then when that also finished want to print the result:
[self asyncTask]
.then(^id (id result){
// take result of asyncTask and pass it through another asynchronous task (which returns a Promise:
return [self anotherAsyncTaskWithInput:result];
}, nil)
.then(^id (id result) {
// here: result is the result of "anotherAsyncTaskWithInput:"
NSLog(@"Final Result: %@", result);
}, nil);
Now, that you know how to "chain" asynchronous tasks, we can finally write the sample implementation of the method refreshPostsInPart:
:
- (RXPromise*) refreshPostsWithArray(NSArray* inputs)
{
const NSUInteger count = [inputs count];
__block NSUInteger i = 0;
return [RXPromise repeat:^RXPromise*{
// Check termination condition:
if (i >= count) {
return nil; // stream at EOS
}
RXPromise* finalResult = [inputs[i++] asyncTask]
.then(^id(id result){
// do something with result (that is your completion handler part of `reloadPosts `
return nil; // note: intermediate result will not be used in repeat!
}, nil);
return finalResult;
}];
}
Note: I'm taking an NSArray
as example for a StreamPart
. We just need to know when the input is consumed, so that we can return nil
(instead a Promise) in order to terminate the repeat
loop. I think you know how to map this to your StreamPart class.
Finally, I copy a functional Foundation console example. This requires to link against the RXPromise library. Feel free to experiment with it.
Running console sample
#import <Foundation/Foundation.h>
#import <RXPromise/RXPromise.h>
#import <RXPromise/RXPromise+RXExtension.h>
// As an example add a category for NSString to simulate an asynchronous task
@implementation NSString (Example)
- (RXPromise*) asyncTask
{
RXPromise* promise = [[RXPromise alloc] init];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
int count = 10;
while (count--) {
usleep(100*1000);
}
if ([self isEqualToString:@"X"]) {
[promise rejectWithReason:@"Bad Input"];
}
else {
[promise fulfillWithValue:[self capitalizedString]];
}
});
return promise;
}
@end
RXPromise* performTasksWithArray(NSArray* inputs)
{
const NSUInteger count = [inputs count];
__block NSUInteger i = 0;
return [RXPromise repeat:^RXPromise*{
if (i >= count) {
return nil;
}
return [inputs[i++] asyncTask].then(^id(id result){
NSLog(@"%@", result);
return nil; // intermediate result not used in repeat
}, nil);
}];
}
int main(int argc, const char * argv[])
{
@autoreleasepool {
NSArray* inputs = @[@"a", @"b", @"c", @"X", @"e", @"f", @"g"];
[performTasksWithArray(inputs)
.then(^id(id result){
NSLog(@"Finished: %@", result);
return nil;
}, ^id(NSError* error){
NSLog(@"Error occured: %@", error);
return nil;
}) runLoopWait];
}
return 0;
}
Note: the current input simulates an error: @"X" causes the asyncTask to fail!
Console log:
2014-02-26 23:06:44.412 Sample7[1410:1003] A
2014-02-26 23:06:45.422 Sample7[1410:2403] B
2014-02-26 23:06:46.434 Sample7[1410:1003] C
2014-02-26 23:06:47.578 Sample7[1410:2403] Error occured: Error Domain=RXPromise Code=-1000 "The operation couldn’t be completed. Bad Input" UserInfo=0x10010ba00 {NSLocalizedFailureReason=Bad Input}