Best way to implement time-sensitive dispatch queue - ios

So I have a service where I can ask for profile-json of a user by passing in the profileID in the request body. If I need 10 peoples profiles, I pass in 10 profileID, and the server will get back with all 10 profile information. Max count = 20.
So I have added an optimization algorithm that groups a bunch of profileIDs based on user scrolling really fast, or user opening up bunch of profiles in the app, etc... Basically my logic is that I have a dispatch_queue that waits roughly 0.6 seconds and in that time I have an array that collects all profileIDs different views in the app will ask for. After 0.6 seconds, whatever is queued up in the array, i include that in the request body, and send it to server.
Any better strategies/design patterns that I could use here?
My implementation looks like this.
----> call in the main thread <--------
CXAssert([NSThread isMainThread], #"profileInfoForUsers should be main thread");
if (!profileIDCacheWaitingToRequest) {
profileIDCacheWaitingToRequest = [NSMutableArray array];
}
if (!profileIDArraySubmittedToTheServer) {
profileIDArraySubmittedToTheServer = [NSMutableArray array];
}
if (!_profile_batch_process_queue) {
_profile_batch_process_queue = dispatch_queue_create("com.gather.chatx.profile.batch.process", DISPATCH_QUEUE_SERIAL);
}
NSMutableArray *filteredProfileIDCache = [NSMutableArray array];
for (NSString *profileID in users) {
if (profileID == NULL) {
continue;
}
if (![profileIDArraySubmittedToTheServer containsObject:profileID]) {
CXDebugLog(#"Not sent to server yet");
if (![profileIDCacheWaitingToRequest containsObject:profileID]) {
CXDebugLog(#"Not added to queue yet yet");
[filteredProfileIDCache addObject:[profileID copy]];
} else {
CXDebugLog(#"Already present in the queue, will fire soon");
}
} else {
CXDebugLog(#"Skipping profile download call for %#", profileID);
}
}
if (filteredProfileIDCache.count == 0) {
CXDebugLog(#"No need for submitting profile to server as we are already waiting on server to return..");
completion (nil, nil);
return;
}
for (NSString *pid in filteredProfileIDCache) {
if (profileIDCacheWaitingToRequest.count <= GROUP_PROFILE_MAX_COUNT) {
[profileIDCacheWaitingToRequest addObject:pid];
} else {
CXDebugLog(#"wait next turn. hope the user makes this call.");
continue;
}
}
CXProdLog(#"Queuing profiles for download...");
dispatch_async(_profile_batch_process_queue, ^{
// this block will keep executing once its a serial queue..
/////// SLEEP this thread, and wait for 0.6 seconds
sleep(0.6);
dispatch_async(dispatch_get_main_queue(), ^{
if (profileIDCacheWaitingToRequest.count == 0) {
completion (nil, nil);
return ;
}
NSArray *profileIDsToRequest = [profileIDCacheWaitingToRequest copy];
[profileIDArraySubmittedToTheServer addObjectsFromArray:profileIDsToRequest];
CXProdLog(#"Fetching profiles from server with count %lu", (unsigned long)profileIDsToRequest.count);
if (profileIDsToRequest.count > GROUP_PROFILE_MAX_COUNT) {
CXDebugLog(#"We are exceeding the count here");
NSMutableArray *array = [NSMutableArray arrayWithArray:profileIDsToRequest];
NSUInteger totalNumberOfObjectsToRemove = profileIDsToRequest.count - GROUP_PROFILE_MAX_COUNT;
for (int i = 0; i < totalNumberOfObjectsToRemove; i ++) {
[array removeLastObject];
}
profileIDsToRequest = array;
}
[CXDownloader downloadProfileForUsers:profileIDsToRequest sessionKey:self.sessionKey completion:^(NSDictionary *profileData, NSError *error) {
dispatch_async(dispatch_get_main_queue(), ^{
[profileIDArraySubmittedToTheServer removeAllObjects];
if (profileData) {
....
}
else {
....
}
});
}];
[profileIDCacheWaitingToRequest removeAllObjects];
});
});

Related

Singleton property returns different values depending on invocation

Background
In my app, I have class called FavoritesController that manages objects that the user's marked as favorites, and this favorite status is then used throughout the app. The FavoritesController is designed as a singleton class as there are a number of UI elements throughout the app that needs to know the 'favorite status' for objects in different places, also network requests need to be able to signal that a favorite needs to be invalidated if the server says so.
This invalidation part happens when the server responds with a 404 error, indicating that the favorite object must be removed from the user's favorites. The network fetch function throws an error, which triggers the FavoritesController to remove the object and then send a notification to interested parties that they need to refresh.
The problem
When using a unit test to check the quality of the 404 implementation, all methods are triggered as intended – the error is thrown and caught, the FavoritesController deletes the object and sends the notification. In some instances though, the deleted favorite is still there – but it depends on from where the query is done!
If I query inside the singleton the deletion went OK, but if I query from a class that makes use of the singleton, the deletion didn't happen.
Design details
The FavoritesController property favorites uses an ivar with all accesses #synchronized(), and the value of the ivar is backed by a NSUserDefaults property.
A favorite object is an NSDictionary with two keys: id and name.
Other info
One weird thing which I fail to understand why it happens: in some deletion attempts, the name value for the favorite object gets set to "" but the id key retains its value.
I've written unit tests that add an invalid favorite and checks that it gets removed on first server query. This test passes when starting with empty set of favorites, but fails when there is an instance of 'semi-deleted' object as above (that retains its id value)
The unit test now consistently passes, but in live usage the failure to delete remains. I suspect that this is due to NSUserDefaults not saving to disk immediately.
Steps I've tried
Making sure that the singleton implementation is a 'true' singleton, i.e. sharedController always returns the same instance.
I thought there was some sort of 'capture' problem, where a closure would keep its own copy with outdated favorites, but I think not. When NSLogging the object ID it returns the same.
Code
FavoritesController main methods
- (void) serverCanNotFindFavorite:(NSInteger)siteID {
NSLog(#"Server can't find favorite");
NSDictionary * removedFavorite = [NSDictionary dictionaryWithDictionary:[self favoriteWithID:siteID]];
NSUInteger index = [self indexOfFavoriteWithID:siteID];
[self debugLogFavorites];
dispatch_async(dispatch_get_main_queue(), ^{
[self removeFromFavorites:siteID completion:^(BOOL success) {
if (success) {
NSNotification * note = [NSNotification notificationWithName:didRemoveFavoriteNotification object:nil userInfo:#{#"site" : removedFavorite, #"index" : [NSNumber numberWithUnsignedInteger:index]}];
NSLog(#"Will post notification");
[self debugLogFavorites];
[self debugLogUserDefaultsFavorites];
[[NSNotificationCenter defaultCenter] postNotification:note];
NSLog(#"Posted notification with name: %#", didRemoveFavoriteNotification);
}
}];
});
}
- (void) removeFromFavorites:(NSInteger)siteID completion:(completionBlock) completion {
if ([self isFavorite:siteID]) {
NSMutableArray * newFavorites = [NSMutableArray arrayWithArray:self.favorites];
NSIndexSet * indices = [newFavorites indexesOfObjectsPassingTest:^BOOL(NSDictionary * entryUnderTest, NSUInteger idx, BOOL * _Nonnull stop) {
NSNumber * value = (NSNumber *)[entryUnderTest objectForKey:#"id"];
if ([value isEqualToNumber:[NSNumber numberWithInteger:siteID]]) {
return YES;
}
return NO;
}];
__block NSDictionary* objectToRemove = [[newFavorites objectAtIndex:indices.firstIndex] copy];
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(#"Will remove %#", objectToRemove);
[newFavorites removeObject:objectToRemove];
[self setFavorites:[NSArray arrayWithArray:newFavorites]];
if ([self isFavorite:siteID]) {
NSLog(#"Failed to remove!");
if (completion) {
completion(NO);
}
} else {
NSLog(#"Removed OK");
if (completion) {
completion(YES);
}
}
});
} else {
NSLog(#"Tried removing site %li which is not a favorite", (long)siteID);
if (completion) {
completion(NO);
}
}
}
- (NSArray *) favorites
{
#synchronized(self) {
if (!internalFavorites) {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
self->internalFavorites = [self.defaults objectForKey:k_key_favorites];
});
if (!internalFavorites) {
internalFavorites = [NSArray array];
}
}
return internalFavorites;
}
}
- (void) setFavorites:(NSArray *)someFavorites {
#synchronized(self) {
internalFavorites = someFavorites;
[self.defaults setObject:internalFavorites forKey:k_key_favorites];
}
}
- (void) addToFavorites:(NSInteger)siteID withName:(NSString *)siteName {
if (![self isFavorite:siteID]) {
NSDictionary * newFavorite = #{
#"name" : siteName,
#"id" : [NSNumber numberWithInteger:siteID]
};
dispatch_async(dispatch_get_main_queue(), ^{
NSArray * newFavorites = [self.favorites arrayByAddingObject:newFavorite];
[self setFavorites:newFavorites];
});
NSLog(#"Added site %# with id %ld to favorites", siteName, (long)siteID);
} else {
NSLog(#"Tried adding site as favorite a second time");
}
}
- (BOOL) isFavorite:(NSInteger)siteID
{
#synchronized(self) {
NSNumber * siteNumber = [NSNumber numberWithInteger:siteID];
NSArray * favs = [NSArray arrayWithArray:self.favorites];
if (favs.count == 0) {
NSLog(#"No favorites");
return NO;
}
NSIndexSet * indices = [favs indexesOfObjectsPassingTest:^BOOL(NSDictionary * entryUnderTest, NSUInteger idx, BOOL * _Nonnull stop) {
if ([[entryUnderTest objectForKey:#"id"] isEqualToNumber:siteNumber]) {
return YES;
}
return NO;
}];
if (indices.count > 0) {
return YES;
}
}
return NO;
}
Singleton implementation of FavoritesController
- (instancetype) init {
static PKEFavoritesController *initedObject;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
initedObject = [super init];
self.defaults = [NSUserDefaults standardUserDefaults];
});
return initedObject;
}
+ (instancetype) sharedController
{
return [self new];
}
Unit testing code
func testObsoleteFavoriteRemoval() {
let addToFavorites = self.expectation(description: "addToFavorites")
let networkRequest = self.expectation(description: "network request")
unowned let favs = PKEFavoritesController.shared()
favs.clearFavorites()
XCTAssertFalse(favs.isFavorite(313), "Should not be favorite initially")
if !favs.isFavorite(313) {
NSLog("Adding 313 to favorites")
favs.add(toFavorites: 313, withName: "Skatås")
}
let notification = self.expectation(forNotification: NSNotification.Name("didRemoveFavoriteNotification"), object: nil) { (notification) -> Bool in
NSLog("Received notification: \(notification.name.rawValue)")
return true
}
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
NSLog("Verifying 313 is favorite")
XCTAssertTrue(favs.isFavorite(313))
addToFavorites.fulfill()
}
self.wait(for: [addToFavorites], timeout: 5)
NSLog("Will trigger removal for 313")
let _ = SkidsparAPI.fetchRecentReports(forSite: 313, session: SkidsparAPI.session()) { (reports) in
NSLog("Network request completed")
networkRequest.fulfill()
}
self.wait(for: [networkRequest, notification], timeout: 10)
XCTAssertFalse(favs.isFavorite(313), "Favorite should be removed after a 404 error from server")
}
To give context around my answers, this is what the code in question looked like when suggesting the change:
- (NSArray *)favorites {
#synchronized(internalFavorites) {
if (!internalFavorites) {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
internalFavorites = [self.defaults objectForKey:k_key_favorites];
});
if (!internalFavorites) {
internalFavorites = [NSArray array];
}
}
}
return internalFavorites;
}
I was suspicious of the check if (!internalFavorites) { that followed #synchronized(internalFavorites) because that meant that there was an expectation of #synchronized being passed nil, which results in a noop.
This meant multiple calls to to favorites or setFavorites could happen in funny ways since they wouldn't actually be synchronized. Giving #sychronized an actual object to synchronize on was crucial for thread safety. Synchronizing on self is fine, but for a particular class, you have to be careful not to synchronize too many things on self or you'll be bound to create needless blocking. Providing simple NSObjects to #sychronized is a good way to narrow the scope of what you're protecting.
Here's how you can avoid using self as your lock.
- (instancetype)init {
static PKEFavoritesController *initedObject;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
initedObject = [super init];
self.lock = [NSObject new];
self.defaults = [NSUserDefaults standardUserDefaults];
});
return initedObject;
}
+ (instancetype)sharedController {
return [self new];
}
- (NSArray *)favorites {
#synchronized(_lock) {
if (!internalFavorites) {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
self->internalFavorites = [self.defaults objectForKey:k_key_favorites];
});
if (!internalFavorites) {
internalFavorites = [NSArray array];
}
}
}
return internalFavorites;
}
Regarding the abnormalities between test runs, definitely calling synchronize on the NSUserDefaults will help because calls to change the defaults are asynchronous, which means other threads are involved. There are like 3 layers of caching as well, and specifically for the purposes of running tests synchronize should ensure that things are completely and cleanly committed before Xcode pulls the plug on the test run. The documentation very abruptly insists that it's not a necessary call, but if it truly weren't necessary it wouldn't exist :-). On my first iOS projects, we always called synchronize after every defaults change... so, I think that documentation is more aspirational on the Apple engineers' parts. I'm glad this intuition helped you.

iOS/AFNetworking 3.0: Complete multiple requests in order

I've the following code below (example code) that sends an API GET request multiple times.
- (void)listOfPeople:(NSArray *)array {
for (int i = 0; i < array.count; i++) {
Person *person = [array objectAtIndex:i];
[personClient getPersonData:person.fullName onSuccess:^(id result) {
// change data here
} onFailure:^(NSError *error) {
}];
}
}
The code doesn't work very well because the API requests finishes in a different order every time. I need to complete each api request in order. I believe I need to wait until either the completion block or the failure block is finished before continuing the for loop. Can someone point me in the right direction unless there is a better way to accomplish this task. I've tried dispatch group, but it didn't complete each request in order.
Get rid of the for loop, and instead make a recursive function that calls itself from the completion handler to get the next Person. That way when each call completes, it will make the call to get the next one.
Something like this:
- (void)getPersonFromArray:(NSArray *)array atIdx:(NSInteger)idx {
if (idx < array.count)
{
Person *person = [array objectAtIndex:idx];
[personClient getPersonData:person.fullName onSuccess:^(id result)
{
// Do something useful with Person here...
// ...
[self getPersonFromArray:array atIdx(idx + 1)];
} onFailure:^(NSError *error) {
// Handle errors here
// ...
}];
}
}

iOS multi-thread ensure async block return data

I am trying to use multi thread to achieve a serial of http request, first I want to upload several photos to a third party api, and would like all the returned response all collected, and then fire a final http request. But what I did with following code can not get it work.
//With valid token, I can upload to a third-party server a list of Photos
//after I have each of the photo uploaded, a "key" is responsed, denoting the name of the Photo
//I would like to collect all the "key", and post to my own server to record them in database
- (void)uploadMultiplePhotoAssets:(NSArray *)assets Token:(NSString *)token
{
dispatch_group_t group = dispatch_group_create();
//I would like to use these arrays to store the result of the following request in each dispatch
__block NSMutableArray *successUploadedImageKeys = [NSMutableArray array];
__block NSMutableArray *failedAssetIndex = [NSMutableArray array];
for(int i= 0; i < assets.count; i++)
{
dispatch_group_async(group,dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^ {
[ThirdPartyApi uploadPHAsset:assets[i] Token:token complete:^(NSString *key) {
if(key)
{ //if key is not nil, the request is successful
NSMutableDictionary *dict = [[NSMutableDictionary alloc] init];
[dict setObject:key forKey:#"key"];
[dict setObject:#(i) forKey:#"index"];
[successUploadedImageKeys addObject:[dict copy]];
}
else
{
//if request failed, record which one failed
[failedAssetIndex addObject:#(i)];
}
}];
});
}
//I would like to handle all the result after each thread is performed, and all the "complete" blocks are also finished
dispatch_group_notify(group,dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^ {
NSLog(#"successUploadedImageKeys is %#, failedAssetIndex is %#", successUploadedImageKeys, failedAssetIndex);
[MyApi postToMyBackend:successUploadedImageKeys];
});
}
If I use NSOperation, it is also the same, all the complete block are not waited before the final http request is fired.
In C, if coding for MPI, there is a method to wait all the thread to be at the same status. What is it in Obj-C to achieve this? How can I wait for the async response to arrive and then do something with all the response together?
How about good old completion handlers? (up to the reader to decide to dispatch any of the work onto a background thread if needed)
-(void) uploadAsset: (id) asset successKeys: successUploadedImageKeys failedAssets: failedAssetIndex completionHandler: (void(^)(void)) completion {
[ThirdPartyApi uploadPHAsset:assets[i] Token:token complete:^(NSString *key) {
if(key)
{ //if key is not nil, the request is successful
NSMutableDictionary *dict = [[NSMutableDictionary alloc] init];
[dict setObject:key forKey:#"key"];
[dict setObject:#(i) forKey:#"index"];
[successUploadedImageKeys addObject:[dict copy]];
}
else
{
//if request failed, record which one failed
[failedAssetIndex addObject:#(i)];
}
completionHandler(completion);
}];
}
-(void) uploadAssets: (NSMutableSet*) assets token:(NSString *)token successKeys: successUploadedImageKeys failedAssets: failedAssetIndex completionHandler: (void(^)(void)) completion {
id nextAsset = assets.anyObject;
[self uploadAsset: nextAsset successKeys:successUploadedImageKeys failedAssets:failedAssetIndex completionHandler:^{
[assets removeObject:nextAsset];
if (assets.count) {
[self uploadAssets:assets token:token successKeys:successUploadedImageKeys failedAssets:failedAssetIndex completionHandler:completion];
} else {
completion();
}
}];
}
-(void) uploadMultiplePhotoAssets:(NSArray *)assets token:(NSString *)token {
NSMutableArray *successUploadedImageKeys = [NSMutableArray array];
NSMutableArray *failedAssetIndex = [NSMutableArray array];
[self uploadAssets: [NSMutableSet setWithArray:assets] token:(NSString *)token successKeys: successUploadedImageKeys failedAssets: failedAssetIndex completionHandler: (void(^)(void)) {
// done
NSLog(#"successUploadedImageKeys is %#, failedAssetIndex is %#", successUploadedImageKeys, failedAssetIndex);
[MyApi postToMyBackend:successUploadedImageKeys];
}];
}

GCD serial queue and race condition

I have two methods which run on a serial queue. Each method return a copy of some class. I'm trying to achieve thread safety solution while also mainting data integrity.
for example:
-(Users *) getAllUsers
{
__block copiedUsers;
dispatch_sync(_backgroundQueue, ^{
copiedUsers = [self.users copy]; // return copy object to calling thread.
});
return copiedUsers;
}
-(Orders *) getAllOrders
{
__block copiedOrders;
dispatch_sync(_backgroundQueue, ^{
copiedOrders = [self.Orders copy]; // return copy object to calling thread.
});
return copiedOrders;
}
In addition to this two methods, I have a worker class that add/remove users and orders, all done via a serial queue backgroundQueue.
If in the main thread I call getAllUsers and then getAllOrders right after the other my data integrity isn't safe because between the two calls the worker class might have changed the model.
my question is how can I make to the caller a nice interface that allows multiple methods to run atomically?
Model is only updated from backgroundQueue serial queue.
Client talks to model via a method that receives a block that runs in the background queue.
In addition, not to freeze main thread, I create another queue and run a block that talks with the gateway method.
P.S - attention that dispatch_sync is called only in runBlockAndGetNeededDataSafely to avoid deadlocks.
Code sample:
aViewController.m
ManagerClass *m = [ManagerClass new];
dispatch_queue_t q = dispatch_queue_create("funnelQueue", DISPATCH_QUEUE_SERIAL);
dispatch_block_t block_q = ^{
__Users *users;
__Orders *orders;
[manager runBlockAndGetNeededDataSafely:^
{
users = [manager getUsers];
orders = [manager getOrders];
dispatch_async(dispatch_get_main_queue(),
^{
// got data safely - no thread issues, copied objects. update UI!
[self refreshViewWithUsers:users
orders:orders];
});
}];
}
dispatch_async(q, block_q);
Manager.m implementation:
-(void) runBlockInBackground:(dispatch_block_t) block
{
dispatch_sync(self.backgroundQueue, block);
}
-(Users *) getAllUsers
{
return [self.users copy];
}
-(Orders *) getAllOrders
{
return [self.Orders copy];
}
To answer your question about how to checking the current queue:
First when you create the queue, give it a tag:
static void* queueTag = &queueTag;
dispatch_queue_t queue = dispatch_queue_create("a queue", 0);
dispatch_queue_set_specific(queue, queueTag, queueTag, NULL);
and then run a block like this:
-(void)runBlock:(void(^)()) block
{
if (dispatch_get_specific(queueTag) != NULL) {
block();
}else {
dispatch_async(self.queue, block);
}
}
Your example doesn't work. I suggest to use completion callback. You should have an option to know when the worker finish his job to return to value.
- (void)waitForCompletion:(BOOL*)conditions length:(int)len timeOut:(NSInteger)timeoutSecs {
NSDate *timeoutDate = [NSDate dateWithTimeIntervalSinceNow:timeoutSecs];
BOOL done = YES;
for (int i = 0; i < len; i++) {
done = done & *(conditions+i);
}
do {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:timeoutDate];
if([timeoutDate timeIntervalSinceNow] < 0.0)
break;
//update done
done = YES;
for (int i = 0; i < len; i++) {
done = done & *(conditions+i);
}
} while (!done);
}
-(void) getAllUsers:(void(^)(User* user, NSError* error))completion
{
dispatch_async(_backgroundQueue, ^{
BOOL condition[2] = [self.userCondition, self.orderCondition];
[self waitForCompletion: &condition[0] length:2 timeOut:60];
if (completion) {
completion([self.users copy], nil);
}
});
}

downloading data from several pffile's at once asynchronosly

If I have an array of Message objects, each with a PFile containing data, is it possible to download the data for every single message by queuing them up asynchronously like so:
for (int i = 0; i < _downloadedMessages.count; i++) {
PFObject *tempMessage = (PFObject *)[_downloadedMessages objectAtIndex:i];
[[tempMessage objectForKey:#"audio"] getDataInBackgroundWithBlock:^(NSData *data, NSError *error) {
[self persistNewMessageWithData:data];
}];
}
This seems to cause my app to hang, even though this should be done in the background...
Using the solution below:
NSMutableArray* Objects = ...
[self forEachPFFileInArray:Objects retrieveDataWithCompletion:^BOOL(NSData* data, NSError*error){
if (data) {
PFObject *tempObj = (PFObject *)Object[someIndex...];
[self persistNewMessageWithData:data andOtherInformationFromObject:tempObj];
return YES;
}
else {
NSLog(#"Error: %#", error);
return NO; // stop iteration, optionally continue anyway
}
} completion:^(id result){
NSLog(#"Loop finished with result: %#", result);
}];
What you are possibly experiencing is, that for a large numbers of asynchronous requests which run concurrently, the system can choke due to memory pressure and due to network stalls or other accesses of resources that get exhausted (including CPU).
You can verify the occurrence of memory pressure using Instruments with the "Allocations" tool.
Internally (that is, in the Parse library and the system) there might be a variable set which sets the maximum number of network requests which can run concurrently. Nonetheless, in your for loop you enqueue ALL requests.
Depending of what enqueuing a request means in your case, this procedure isn't free at all. It may cost a significant amount of memory. In the worst case, the network request will be enqueued by the system, but the underlying network stack executes only a maximum number of concurrent requests. The other enqueued but pending requests hang there and wait for execution, while their network timeout is already running. This may lead to cancellation of pending events, since their timeout expired.
The simplest Solution
Well, the most obvious approach solving the above issues would be one which simply serializes all tasks. That is, it only starts the next asynchronous task when the previous has been finished (including the code in your completion handler). One can accomplish this using an asynchronous pattern which I name "asynchronous loop":
The "asynchronous loop" is asynchronous, and thus has a completion handler, which gets called when all iterations are finished.
typedef void (^loop_completion_handler_t)(id result);
typedef BOOL (^task_completion_t)(PFObject* object, NSData* data, NSError* error);
- (void) forEachObjectInArray:(NSMutableArray*) array
retrieveDataWithCompletion:(task_completion_t)taskCompletionHandler
completion:(loop_completion_handler_t)completionHandler
{
// first, check termination condition:
if ([array count] == 0) {
if (completionHandler) {
completionHandler(#"Finished");
}
return;
}
// handle current item:
PFObject* object = array[0];
[array removeObjectAtIndex:0];
PFFile* file = [object objectForKey:#"audio"];
if (file==nil) {
if (taskCompletionHandler) {
NSDictionary* userInfo = #{NSLocalizedFailureReasonErrorKey: #"file object is nil"}
NSError* error = [[NSError alloc] initWithDomain:#"RetrieveObject"
code:-1
userInfo:userInfo];
if (taskCompletionHandler(object, nil, error)) {
// dispatch asynchronously, thus invoking itself is not a recursion
dispatch_async(dispatch_get_global(0,0), ^{
[self forEachObjectInArray:array
retrieveDataWithCompletion:taskCompletionHandler
completionHandler:completionHandler];
});
}
else {
if (completionHandler) {
completionHandler(#"Interuppted");
}
}
}
}
else {
[file getDataInBackgroundWithBlock:^(NSData *data, NSError *error) {
BOOL doContinue = YES;
if (taskCompletionHandler) {
doContinue = taskCompletionHandler(object, data, error);
}
if (doContinue) {
// invoke itself (note this is not a recursion")
[self forEachObjectInArray:array
retrieveDataWithCompletion:taskCompletionHandler
completionHandler:completionHandler];
}
else {
if (completionHandler) {
completionHandler(#"Interuppted");
}
}
}];
}
}
Usage:
// Create a mutable array
NSMutableArray* objects = [_downloadedMessages mutableCopy];
[self forEachObjectInArray:objects
retrieveDataWithCompletion:^BOOL(PFObject* object, NSData* data, NSError* error){
if (error == nil) {
[self persistNewMessageWithData:data andOtherInformationFromObject:object];
return YES;
}
else {
NSLog(#"Error %#\nfor PFObject %# with data: %#", error, object, data);
return NO; // stop iteration, optionally continue anyway
}
} completion:^(id result){
NSLog(#"Loop finished with result: %#", result);
}];

Resources