I am using the MKNetworkKit library to make REST calls to a server. This particular snippet sets is called when a user wants to make a photo favorite or not favorite. It is pretty straight forward objective-c block code if we are only setting favorite flag for a single photo. However I have impelmented a multiselect mechanism in the GUI so that a user can select several photos and favorite them all at once.
The function I wrote to do this works just fine, but it just doesn't feel very clean to me.
What I dislike:
Tracking all of the callbacks with a block counter. I am wondering if there is a more elegant way to handle this.
The same code exists in both completion blocks. However that is just how MKNetworkKit is used (one block for success, one for error). I suppose if I made this an instance method, I could handle it by calling another i-method, but that seems just as messy to do all the setup. I'd like to keep this as a handy class method utility.
Suggestions?
+(BOOL)updateAssets:(NSArray*)assets
isFavorite:(BOOL)isFavorite
completion:(MyAssetCompletion)completion // (BOOL success)
{
assert(assets);
if (assets == nil || assets.count == 0) return NO;
__block BOOL bError = NO;
__block NSInteger counter = 0; // Use a counter to track number of completed REST calls
for(MyAsset *asset in assets){
MyUpdateAssetForm *form = [[MyUpdateAssetForm alloc] init];
form.isPrivate = isFavorite ? #(1) : #(0);
[[MyRESTEngine sharedInstance] updateAssetWithUUID:asset.UUID
withForm:form
completionBlock:^{
counter++;
if(counter == assets.count){
completion(bError == NO);
}
} errorBlock:^(NSError *error, NSString *additionalInfo) {
bError = bError || YES;
counter++;
if(counter == assets.count){
completion(bError == NO);
}
}];
}
return YES;
}
You can try this (untested):
(By the way, your current implementation is not thread safe as the counter might be incremented from more then one thread in a non-atomic manner)
#see NSConditionLock
- (BOOL) updateAssets:(NSArray*)assets
isFavorite:(BOOL)isFavorite
completion:(bool_block_t)completion
{
assert(assets && [assets count]);
__block NSMutableArray* failed = [NSMutableArray new];
__block NSConditionLock* lock = [[NSConditionLock alloc] initWithCondition:[assets count]];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),
^{
[lock lockWhenCondition:0];
completion([failed count] == 0);
});
NSNumber* val = (isFavorite ? #1 : #0);
for (MyAsset* asset in assets) {
MyUpdateAssetForm* form = [[MyUpdateAssetForm alloc] init];
form.isPrivate = val;
[self updateAssetWithUUID:asset.UUID
withForm:form
completionBlock:^{[lock unlockWithCondition:[lock condition] - 1];}
errorBlock:^(NSError *error, NSString *additionalInfo)
{
[lock lock];
[failed addObject:asset];
[lock unlockWithCondition:[lock condition] - 1];
}];
}
return YES;
}
Related
I am working on a turn based iOS game and trying to populate my list of games the player is participating in.
for (unsigned i = 0; i < [matches count]; i++)
{
// Only load data for games in progress.
// NOTE: Might want to handle finished games later.
if ([matches[i] status] != GKTurnBasedMatchStatusEnded)
{
// Send off another block to retrieve the match's data.
[(GKTurnBasedMatch*)matches[i] loadMatchDataWithCompletionHandler: ^(NSData *matchData, NSError *error)
{
// Prepare the game.
Game* game;
if (matchData.length == 0)
{
// If the match data is empty, this is a new game. Init from scratch.
game = [[Game alloc] init];
}
else
{
// Otherwise, unpack the data and init from it.
game = [NSKeyedUnarchiver unarchiveObjectWithData:matchData];
}
game.match = matches[i];
// Load the displayNames for the players.
bool lastIndex = i == ([matches count] - 1);
[self loadPlayerIdentifiersForGame:game intoArray:blockGames lastIndex:lastIndex];
}];
}
}
Unfortunately, I am having an issue where I can't tag each block with its index. That is, i is always 0 by the time the block executes. Is there a way that I can make sure the block knows what i WAS at the time it was launched?
I sidestepped the issue where my UITableView wouldn't reload if the last game was over by doing this instead:
GKTurnBasedMatch* match;
for (int j = ([matches count] - 1); j >= 0; j --)
{
match = matches[j];
if (match.status != GKTurnBasedMatchStatusEnded)
break;
}
bool lastIndex = (matches[i] == match);
[self loadPlayerIdentifiersForGame:game intoArray:blockGames lastIndex:lastIndex];
-(void)someMethod {
...
for (unsigned i = 0; i < [matches count]; i++)
{
// Only load data for games in progress.
// NOTE: Might want to handle finished games later.
if ([matches[i] status] != GKTurnBasedMatchStatusEnded)
[self loadMatch:i of:matches];
}
}
-(void) loadMatch:(int)i of:(NSArray *)matches {
// Send off another block to retrieve the match's data.
[(GKTurnBasedMatch*)matches[i] loadMatchDataWithCompletionHandler: ^(NSData *matchData, NSError *error)
{
// Prepare the game.
Game* game;
if (matchData.length == 0)
{
// If the match data is empty, this is a new game. Init from scratch.
game = [[Game alloc] init];
}
else
{
// Otherwise, unpack the data and init from it.
game = [NSKeyedUnarchiver unarchiveObjectWithData:matchData];
}
game.match = matches[i];
// Load the displayNames for the players.
bool lastIndex = i == ([matches count] - 1);
[self loadPlayerIdentifiersForGame:game intoArray:blockGames lastIndex:lastIndex];
}];
}
the simplest way is to pass a third parameter containing the tag..
And I suggest to use typedef.. to better write code (and let autocompletion work for you..)
typedef void (^CompletionBlock)(NSInteger tag, NSData * data, NSError *err);
and use CompletionBlock when defining loadMatchDataWithCompletionHandler.
I am trying to pull the turn based games in which a player is participating in order to populate my tableView.
This is my function to pull their games:
- (void) loadMatchDataWithArray:(NSMutableArray*)currentGames Flag:(bool*)returned
{
NSMutableArray* __block blockGames = currentGames;
bool* __block blockReturn = returned;
[GKTurnBasedMatch loadMatchesWithCompletionHandler:^(NSArray *matches, NSError *error)
{
if (matches)
{
for (int i = 0; i < matches.count; i++)
{
[(GKTurnBasedMatch*)matches[i] loadMatchDataWithCompletionHandler: ^(NSData *matchData, NSError *error)
{
int size = [matchData length];
if (size != 0)
{
Game* game = [NSKeyedUnarchiver unarchiveObjectWithData:matchData];
[blockGames addObject:game];
}
else
{
Game* game = [[Game alloc] init];
[blockGames addObject:game];
game.activePlayer = [GKLocalPlayer localPlayer];
}
*blockReturn = true;
}];
}
}
else
{
*blockReturn = true;
}
}];
}
And this is where I call it:
- (void)viewDidLoad
{
[super viewDidLoad];
[[self tableView]
setBackgroundView:[[UIImageView alloc]
initWithImage:[UIImage imageNamed:#"iPhoneBackground-568h"]]];
bool* returned = false;
[[GKMatchHelper sharedInstance] loadMatchDataWithArray:currentGames Flag:returned];
while (!returned);
[self.tableView reloadData];
// Uncomment the following line to preserve selection between presentations.
// self.clearsSelectionOnViewWillAppear = NO;
// Uncomment the following line to display an Edit button in the navigation bar for this view controller.
// self.navigationItem.rightBarButtonItem = self.editButtonItem;
}
Sadly, this is just giving me a blank black screen and never returns. Is there a way that I can detect when my block comes back and display a loading spinner until then, at which point I would reload the table?
EDIT:
I have revised my code and brought the function inside my MainMenuViewController, and now it builds but never displays the data.
- (void) loadMatchData
{
NSMutableArray* __block blockGames = currentGames;
MainMenuViewController* __weakSelf = self;
[GKTurnBasedMatch loadMatchesWithCompletionHandler:^(NSArray *matches, NSError *error)
{
if (matches)
{
for (int i = 0; i < matches.count; i++)
{
[(GKTurnBasedMatch*)matches[i] loadMatchDataWithCompletionHandler: ^(NSData *matchData, NSError *error)
{
int size = [matchData length];
if (size != 0)
{
Game* game = [NSKeyedUnarchiver unarchiveObjectWithData:matchData];
[blockGames addObject:game];
}
else
{
Game* game = [[Game alloc] init];
[blockGames addObject:game];
game.activePlayer = [GKLocalPlayer localPlayer];
}
[__weakSelf.tableView reloadData];
}];
}
}
[__weakSelf.tableView reloadData];
}];
[__weakSelf.tableView reloadData];
}
And now in my ViewDidLoad I just call:
[self loadMatchData];
Oh dear. Do NOT halt the program execution with "while" loops!
Why not simply call [self.tableView reloadData] at the end of your block?
So,
Remove the last 2 lines in the viewDidLoad method
Replace *blockReturn = true; with [self.tableView reloadData] (you might need to keep a weak reference to 'self' to avoid retain cycles)
Never ever use while (this and that) to wait for an operation to complete. A non-responsive UI is bad and it will cause the users to abandon your app.
I use fast enumeration and in the enumeration block I send network requests asynchronously.
So what happens is the enumerateObjectsUsingBlock: just call the block super fast and let the enumeration block finish after some time.
This leads to different results, because some requests finish faster than other. So it's not sorted as I wanted.
Is there any way to set the block to freeze, and after the asynchronous network requests is completed, to just tell it to go to the next one?
Here is some code
NSArray *sites = [self.fetchedResultsController.fetchedObjects copy];
NSLog(#"sites - %#",sites);
[sites enumerateObjectsUsingBlock:^(Sites *site, NSUInteger idx, BOOL *stop) {
NSLog(#"site name - %#,",site.name);
[[Wrapper sharedWrapper] sendRequestTo:site completionBlock:{
NSLog(#"site name - %#",site.name);
}];
}];
Thanks!
Is there any way to set the block to freeze, and after the
asynchronous network requests is completed, to just tell it to go to
the next one?
NSArray *sites = [self.fetchedResultsController.fetchedObjects copy];
NSLog(#"sites - %#",sites);
[sites enumerateObjectsUsingBlock:^(Sites *site, NSUInteger idx, BOOL *stop) {
NSLog(#"site name - %#,",site.name);
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
[[Wrapper sharedWrapper] sendRequestTo:site completionBlock:{
NSLog(#"site name - %#",site.name);
dispatch_semaphore_signal(semaphore);
}];
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
}];
^ Not really the ideal way to do this but if you want to synchronously iterate while waiting for an async request to finish before moving forward, the above will do it via GCD. There are other ways to to where you can iterate and increment a dispatch_group while waiting for all the groups to be left after the async tasks complete such as:
dispatch_group_t downloadGroup = dispatch_group_create();
dispatch_group_enter(downloadGroup);
[self fetchStuffInBackground:background withCompletion:^(NSArray *stuff, NSError *error) {
NSLog(#"leaving stuff");
dispatch_group_leave(downloadGroup);
}];
dispatch_group_enter(downloadGroup);
[self fetchAOtherStuffInBackground:background withCompletion:^(NSArray *stuff, NSError *error) {
NSLog(#"leaving other stuff");
dispatch_group_leave(downloadGroup);
}];
dispatch_group_enter(downloadGroup);
[self fetchLastStuffInBackground:background withCompletion:^(NSArray *lastStuff, NSError *error) {
NSLog(#"leaving last stuff");
dispatch_group_leave(downloadGroup);
}];
}
}
dispatch_group_notify(downloadGroup, dispatch_get_main_queue(), ^{
if (callback) {
callback(error);
}
});
I wanted to achieve the same thing but keep using blocks to simplify my code, instead of having the hassle of passing parameters through a recursive method. I came up with this NSArray category:
NS_ASSUME_NONNULL_BEGIN
#interface NSArray(MH)
- (void)mh_asyncEnumerateObjectsUsingBlock:(void (^)(id obj, NSUInteger idx, BOOL* stop, dispatch_block_t next))block;
#end
NS_ASSUME_NONNULL_END
#implementation NSArray(MH)
- (void)mh_asyncEnumerateObjectsUsingBlock:(void (^)(id _Nonnull obj, NSUInteger idx, BOOL* stop, dispatch_block_t next))block{
__block NSUInteger index = 0;
__block BOOL stop = NO;
void (^next)();
__block __weak typeof(next) weakNext;
weakNext = next = ^void() {
void (^strongNext)() = weakNext;
// check if finished
if(stop || index == self.count){
return;
}
id obj = self[index];
index++;
block(obj, index - 1, &stop, strongNext);
};
next();
}
#end
It's used like this:
NSArray* a = #[#"Malc", #"Bob", #"Jim"];
[a mh_asyncEnumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL *stop, dispatch_block_t next) {
// simulate an async network call
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(#"%#", obj);
next();
});
}];
Output:
2016-01-04 22:41:04.631 Malc
2016-01-04 22:41:05.632 Bob
2016-01-04 22:41:06.720 Jim
As you can see, this demo outputs each string in the array with 1 second delay. You can use it by doing your network call inside the block and then calling next when it's done. If you hit an error and would like to cancel just set *stop = YES; before calling next(), the same as you would do with normal enumeration.
NSArray *sites = [self.fetchedResultsController.fetchedObjects copy];
NSLog(#"sites - %#",sites);
[sites mh_asyncEnumerateObjectsUsingBlock:^(Site *site, NSUInteger idx, BOOL *stop, dispatch_block_t next){
NSLog(#"site name - %#,",site.name);
[[Wrapper sharedWrapper] sendRequestTo:site completionBlock:{
if(error){ // your completion block should have an error param!!!
*stop = YES;
}
NSLog(#"site name - %#",site.name);
next();
}];
}];
Is there any way to set the block to freeze, and after the asynchronous network requests is completed, to just tell it to go to the next one?
You can achieve that result by reorganizing your code: instead of using enumeration, just execute the async request from the completion block, one at the time:
- (void) doRequestAsync:(NSArray*)sites index:(NSUInteger)index {
if (index >= [sites count]) return;
NSString* site = [sites objectAtIndex:index];
[[Wrapper sharedWrapper] sendRequestTo:site completionBlock:{
NSLog(#"site name - %#",site.name);
[self doRequestAsync:sites index:++index];
}];
}
Alternatives to this are modifying your Wrapper class so that it uses async networking (but use it on a secondary thread, then, to avoid blocking the UI).
Or you might implement the Async Completion Token pattern so to be able to reorder the responses when they are received.
I am facing an issue in updating to core data using batch updating on a background thread. In the below code I am using main thread to notify user with a progress view and a string which calls a method in appdelegate. But if I am getting bad access error in the NSEntity line in random number of data where I have thousands of objects to update.if I uncomment the NSLOG i have indicated below there is no error, or if i comment the main thread no error, or if I dont update through batch instead if I use bulk update then also no error. IF i comment autorelease pool also the error is appearing. Will some body help me on this please.
Thanks in advance,
Cheers,
Shravan
NSAutoreleasePool *tempPool = [[NSAutoreleasePool alloc] init];
NSUInteger iterator = 1;
for (int i = 0; i < totalNo; i++) {
NSDictionary *alertResult = [[alertResultList objectAtIndex:i] retain];
if (alertResult == nil) {
continue;
}
//managedObjectContext = [appDelegate.managedObjectContext retain];
NSLog(#"Object Count:%u", [[managedObjectContext insertedObjects]count]);
AlertResult *result = (AlertResult *)[NSEntityDescription
insertNewObjectForEntityForName:#"AlertResult"
inManagedObjectContext:managedObjectContext];
[result setUserName:#"A"];
iterator++;
//When count reaches max update count we are saving and draining the pool and resetting the pool
if (iterator == kUploadCount) {
if ([self update] == NO) {
// If unable to update Alert results in the Core Data repository, return
// a custom status code.
statusCode = -1;
}
[managedObjectContext reset];
[tempPool drain];
tempPool = [[NSAutoreleasePool alloc] init];
iterator = 0;
}
//Adding code to change the display string for the lock view to notify user
float count1 = (float)(counter/totalAlerts);
counter = counter + 1.0f;
NSString *dispStr = [NSString stringWithFormat:#"%f",count1];//[NSString stringWithFormat:#"Loading %d out of %d alerts",(i+1),totalAlerts];
NSString *dispMess = [NSString stringWithFormat:#"Alerts %d of %d",(i+1),totalNo];
[self performSelectorOnMainThread:#selector(changeLockScreenMessageWith:) withObject:[NSArray arrayWithObjects:dispStr,dispMess, nil] waitUntilDone:YES];
//NSLog(#"count"); /* If I uncomment this line code runs fine */
[alertResult release];
alertResult = nil;
}
//If count is inbetween the update limit we are updating and we are draining the pool
if (iterator != 0) {
if ([self update] == NO) {
// If unable to update Alert results in the Core Data repository, return
// a custom status code.
statusCode = -1;
}
[managedObjectContext reset];
//[tempPool drain];
}
//Out side the previous method
- (BOOL)update {
NSError *error;
if (![managedObjectContext save:&error]) {
NSLog(#"%#", [error userInfo]);
return NO;
}
return YES;
}
The most likely cause of the kind of crash you're describing is using your managedObjectContext across threads. managedObjectContext is not thread-safe. You must create a new MOC for each thread. I assume managedObjectContext is an ivar; you should never access your ivars directly like this (except in init and dealloc). Always use an accessor to handle memory management for you.
The reason NSLog makes it crash is because NSLog dramatically changes the timing of this function, and you have a race condition.
I am trying to add data to CoreData. It works fine when I build from Xcode to the phone but when I try to start the app directly from iPhone it crashes on first save to the Context.
I read a text file that is synced via iTunes File Sharing, the file is pretty big (~350 000 lines). The values I get from the file is added to two different arrays (barcodes and productNames). The arrays are later batched through and the sent to the function where I save the data.
From the array loop:
[...]
words = [rawText componentsSeparatedByString:#";"];
int loopCounter = 0;
int loopLimit = 20000;
int n = 0;
int wordType;
NSEnumerator *word = [words objectEnumerator];
NSLog(#"Create arrays");
while(tmpWord = [word nextObject]) {
if ([tmpWord isEqualToString: #""] || [tmpWord isEqualToString: #"\r\n"]) {
// NSLog(#"%#*** NOTHING *** ",tmpWord);
}else {
n++;
wordType = n%2;
if (wordType == kBarcode) {
[barcodes addObject: tmpWord];
}else if (wordType == kProduct) {
[productNames addObject: tmpWord];
}
// Send to batch //
loopCounter ++;
if (loopCounter == loopLimit) {
loopCounter = 0;
NSLog(#"adding new batch");
[self addBatchOfData];
[barcodes release];
[productNames release];
barcodes = [[NSMutableArray arrayWithCapacity:20000] retain];
productNames = [[NSMutableArray arrayWithCapacity:20000] retain];
}
}
[...]
And then the save-function:
-(void)addBatchOfData {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
NSError *error;
NSUInteger loopLimit = 5000;
NSUInteger loopCounter = 0;
NSString *ean;
NSString *designation;
for (int i=0; i<[barcodes count];i++ ) {
ean = [barcodes objectAtIndex:i];
designation = [productNames objectAtIndex:i];
Product *product = (Product *)[NSEntityDescription insertNewObjectForEntityForName:#"Product" inManagedObjectContext:importContext];
[product setDesignation:designation];
[product setBarcode:ean];
loopCounter ++;
if (loopCounter == loopLimit) {
NSLog(#"Save CoreData");
[importContext save:&error];
[importContext reset];
[pool drain];
pool = [[NSAutoreleasePool alloc] init];
loopCounter = 0;
}
}
// Save any remaining records
if (loopCounter != 0) {
[importContext save:&error];
[importContext reset];
}
[pool drain];
}
It's really irritating that it works fine when I build from Xcode. Hopefully there is a setting that I missed or something...
EDIT: Forgot to mention that I don't get passed the Default-screen and I don't have any logs. Can it have something to do with the provisioning?
Offload your file loading in a background thread and let the phone start up your main window and view. iOS will kill your app if you do not present a view in a timely manor (this is what you are seeing).
I have to do something like this for my xml -> CoreData converter code. I just present the user with a view notifying them of what is going on and a progress bar (I use https://github.com/matej/MBProgressHUD).
something like:
self.hud = [[MBProgressHUD alloc] initWithView:window];
// Set determinate mode
hud.mode = MBProgressHUDModeDeterminate;
hud.delegate = self;
hud.labelText = #"Converting Data File";
[self.window addSubview:hud];
// Show the HUD while the provided method executes in a new thread
[hud showWhileExecuting:#selector(convertToCoreDataStoreTask) onTarget:self withObject:nil animated:YES];
You just have to make sure that you use a separate NSManagedObjectContext in the new thread.
I would suggest that you implement this delegate method and then try to see what is going on with memory.
when running in the simulator, you have no memory constraints, but when running in the phone you do
- (void)applicationDidReceiveMemoryWarning:(UIApplication *)application
{
}
I think I find the solution to my question.
What I was doing was that I started all the heavy data crunch in the "- (void) viewDidLoad {". When I changed it to start the crunch after I clicked a button in the app, it worked just fine.
Right now it's just finding out where the start the data crunch, any suggestions?