I want to fetch data from server with multiple calls inside for loop. I'm passing different parameter each time. I know it is possible to fetch data like, I'm fetching in code below :
for (NSDictionary *feedItem in [feed objectForKey:#"content"]) {
// url with feedItem data.
NSURL *url = ....
[UrlMethod GetURL:url success:^(NSDictionary *placeData) {
if (placeData) {
dispatch_async(dispatch_get_main_queue(), ^{
// adding object to table data source array
[dataSourceArray addObject:[placeData objectForKey:#"data"]];
// reloading table view.
[self.tableView reloadData];
});
}
} failure:^(NSError *error) {
}];
}
The problem is, Whenever I add data to dataSourceArry, It is not adding sequentially. It is adding according to response of API calls. Please let me know, If it is not clear.
In your case, I would allocate a mutable array first and set [NSNull null] at each position:
NSInteger count = [[feed objectForKey:#"content"] count];
NSMutableArray *dataSourceArray = [NSMutableArray arrayWithCapacity:count];
for (NSInteger i = 0; i < count; ++i) {
[dataSourceArray addObject:[NSNull null]];
}
Then, I would use something called dispatch groups (see more here http://commandshift.co.uk/blog/2014/03/19/using-dispatch-groups-to-wait-for-multiple-web-services/):
__block NSError *apiCallError = nil; // Just to keep track if there was at least one API call error
NSInteger index = 0;
// Create the dispatch group
dispatch_group_t serviceGroup = dispatch_group_create();
for (NSDictionary *feedItem in [feed objectForKey:#"content"]) {
// Start a new service call
dispatch_group_enter(serviceGroup);
// url with feedItem data.
NSURL *url = ...
[UrlMethod GetURL:url success:^(NSDictionary *placeData) {
if (placeData) {
dispatch_async(dispatch_get_main_queue(), ^{
// Add data to Data Source
// index should be the correct one, as the completion block will contain a snapshot of the corresponding value of index
dataSourceArray[index] = [placeData objectForKey:#"data"];
}
dispatch_group_leave(serviceGroup);
} failure:^(NSError *error) {
apiCallError = error;
dispatch_group_leave(serviceGroup);
}];
index++;
}
dispatch_group_notify(serviceGroup, dispatch_get_main_queue(),^{
if (apiCallError) {
// Make sure the Data Source contains no [NSNull null] anymore
[dataSourceArray removeObjectIdenticalTo:[NSNull null]];
}
// Reload Table View
[self.tableView reloadData];
});
Hope it works for you.
This might be of help for you,
//keep dictionary property which will store responses
NSMutableDictionary *storeResponses = [[NSMutableDictionary alloc]init];
//Somewhere outside function keep count or for loop
NSInteger count = 0;
for (NSDictionary *feedItem in [feed objectForKey:#"content"]) {
//Find out index of feddItem
NSInteger indexOfFeedItem = [[feed objectForKey:#"content"] indexOfObject:feedItem];
NSString *keyToStoreResponse = [NSString stringWithFormat:#"%d",indexOfFeedItem];
// url with feedItem data.
NSURL *url = ....
[UrlMethod GetURL:url success:^(NSDictionary *placeData) {
if (placeData) {
//instead of storing directly to array like below
// adding object to table data source array
[dataSourceArray addObject:[placeData objectForKey:#"data"]];
//follow this
//increase count
count++;
[storeResponses setObject:[placeData objectForKey:#"data"] forKey:keyToStoreResponse];
// reloading table view.
if(count == [feed objectForKey:#"content"].count)
{
NSMutableArray *keys = [[storeResponses allKeys] mutableCopy]; //or AllKeys
//sort this array using sort descriptor
//after sorting "keys"
for (NSString *key in keys)
{
//add them serially
[dataSourceArray addObject:[storeResponses objectForKey:key]];
}
dispatch_async(dispatch_get_main_queue(), ^{
[self.tableView reloadData];
});
}
}
} failure:^(NSError *error) {
}];
}
Edit : The answer I have given is directly written here,you might face compilation errors while actually running this code
Don't reload your table each time in the loop. After the loop finishes fetching data , do a sorting on your datasourcearray to get the desired result and then reload table.
This is because you're calling web-services asynchronously so it's not give guarantee that it's give response in sequence as you have made request!
Now solutions for that :
You should write your api like it's give all data at a time. So,
You not need to make many network call and it will improve
performance also!
Second thing you can make recursive kind of function, I mean make another request from completion handler of previous one. In this case once you got response then only another request will be initialize but in this case you will have to compromise with performance!! So first solution is better according to me!
Another thing you can sort your array after you get all the responses and then you can reload your tableView
Try the following :-
for (NSDictionary *feedItem in [feed objectForKey:#"content"]) {
// url with feedItem data.
NSURL *url = ....
[UrlMethod GetURL:url success:^(NSDictionary *placeData) {
if (placeData) {
// adding object to table data source array
[dataSourceArray addObject:[placeData objectForKey:#"data"]];
// reloading table view.
dispatch_sync(dispatch_get_main_queue(), ^{
[self.tableView reloadData];
});
});
} failure:^(NSError *error) {
}];
}
Related
I have the following code where I am calling self.tableView reloadData inside the loop which is a bad idea. If I put it outside the loop then it would not work as expected since it will be called before updating the noOfGroceryItems field.
How can I improve it?
-(void) populateShoppingLists {
[_groceriesService getAllShoppingLists:^(NSArray *results, NSError *error) {
if(error) {
NSLog(#"%#",error.localizedDescription);
}
for(CKRecord *record in results) {
ShoppingList *shoppingList = [[ShoppingList alloc] initWithRecord:record];
// get the number of grocery items in the shopping list
[_groceriesService getItemsByShoppingList:shoppingList result:^(NSArray *results, NSError *error) {
shoppingList.noOfGroceryItems = results.count;
[_shoppingLists addObject:shoppingList];
// THIS IS BAD IDEA
**dispatch_async(dispatch_get_main_queue(), ^{
_shoppingLists = [_shoppingLists sort:#"title" ascending:YES];
[self.tableView reloadData];
});**
}];
}
}];
}
Implementation for getItemsShoppingList method:
-(void) getItemsByShoppingList:(ShoppingList *)shoppingList result:(GetItemsByShoppingList) getItemsByShoppingList
{
CKQuery *query = [[CKQuery alloc] initWithRecordType:#"GroceryItems" predicate:[NSPredicate predicateWithFormat:#"ShoppingList == %#",shoppingList.record]];
[_privateDB performQuery:query inZoneWithID:nil completionHandler:^(NSArray *results, NSError *error) {
getItemsByShoppingList(results,error);
}];
}
You could add a conditional to reload the table if and only if the last shopping list has been fetched, ex:
[_groceriesService getAllShoppingLists:^(NSArray *results, NSError *error) {
if(error) {
NSLog(#"%#",error.localizedDescription);
}
// Variable to count the number
// of records processed
__block int recordsProcessed = 0;
for(CKRecord *record in results) {
ShoppingList *shoppingList = [[ShoppingList alloc] initWithRecord:record];
// (I've changed this second results variable from results
// to results2 in order to distinguish between the two
// "results" variables.)
// get the number of grocery items in the shopping list
[_groceriesService getItemsByShoppingList:shoppingList
result:^(NSArray *results2, NSError *error) {
shoppingList.noOfGroceryItems = results2.count;
[_shoppingLists addObject:shoppingList];
dispatch_async(dispatch_get_main_queue(), ^{
_shoppingLists = [_shoppingLists sort:#"title" ascending:YES];
// Increment recordsProcessed to indicate another
// record has been processed
recordsProcessed ++;
// If all the records have been processed,
// reload the table (using the outer block's
// results variable, not the inner block's result2
// variable).
if (recordsProcessed == results.count) {
[self.tableView reloadData];
}
});
}];
}
}];
Update: And a second solution. Assuming that _shoppingLists starts out empty, you can simply compare results with the number of elements put into _shoppingLists, ex:
[_groceriesService getAllShoppingLists:^(NSArray *results, NSError *error) {
if(error) {
NSLog(#"%#",error.localizedDescription);
}
for(CKRecord *record in results) {
ShoppingList *shoppingList = [[ShoppingList alloc] initWithRecord:record];
// (I've changed this second results variable from results
// to results2 in order to distinguish between the two
// "results" variables.)
// get the number of grocery items in the shopping list
[_groceriesService getItemsByShoppingList:shoppingList
result:^(NSArray *results2, NSError *error) {
shoppingList.noOfGroceryItems = results2.count;
[_shoppingLists addObject:shoppingList];
dispatch_async(dispatch_get_main_queue(), ^{
_shoppingLists = [_shoppingLists sort:#"title" ascending:YES];
// If the number of shopping lists stored
// equals the number of records processed,
// reload the table (using the outer block's
// results variable, not the inner block's result2
// variable).
if (_shoppingLists.count == results.count) {
[self.tableView reloadData];
}
});
}];
}
}];
If you put a dispatch_async after the for loop, you're guaranteed that the block will be dispatched on the main queue, after the loops completes (and your data is updated).
Why not just dispatch the reloadData call once after all your model are processed and updated?
Instead of fast enumerating the results array, you could enumerate with a block, that gives you the index of the current object. if the index is [results count] - 1, trigger the reload.
Of course you should not name two arrays results if the inner one does hide the outer, especially if you intend to call other peoples code «not a very good solution.»
I want to save an array to the NSUserDefaults every time a user interacts with the rows in a tableview (deletes them, for instance)
However, I'm encountering the following issue:
Imagine the following use case:
User deletes row -> row is deleted from datasource, datasource is saved to disk
User deletes another row -> row is deleted from datasource, app crashes because datasource of the first action is still being enumerated
How do I solve this problem? It's important that the app has live feedback to the user, so any checks that the user has to wait for are not preferred.
This is the code that I use to save the datasource to the disk:
if(allowSavePaymentArray){
dispatch_async(kAsyncQueue, ^{
allowSavePaymentArray = NO;
__block int loopCount = 0;
NSMutableArray *archiveArray = [NSMutableArray arrayWithCapacity:dataSourceArray.count];
for (NSMutableDictionary *object in dataSourceArray) {
// PFFile isn't easy to encode, but UIImage is, so whenever we encounter a PFFile, we convert it to UIImage
id imageFile = [payment objectForKey:#"img"];
if([imageFile isKindOfClass:[PFFile class]]){
[imageFile getDataInBackgroundWithBlock:^(NSData *imageData, NSError *error) {
if (!error) {
UIImage *image = [UIImage imageWithData:imageData];
[payment setObject:image forKey:#"img"]; // the PFFile is now replaced for an UIImage
NSData *paymentEncodedObject = [NSKeyedArchiver archivedDataWithRootObject:payment];
[archiveArray addObject:paymentEncodedObject];
loopCount++;
if(loopCount == [paymentArray count]){ // when done looping, save it all
NSUserDefaults *userData = [NSUserDefaults standardUserDefaults];
[userData setObject:archiveArray forKey:#"payments"];
allowSavePaymentArray = YES;
}
}
}];
} else {
loopCount++;
NSData *paymentEncodedObject = [NSKeyedArchiver archivedDataWithRootObject:payment];
[archiveArray addObject:paymentEncodedObject];
if(loopCount == [paymentArray count]){ // when done looping
NSUserDefaults *userData = [NSUserDefaults standardUserDefaults];
[userData setObject:archiveArray forKey:#"payments"];
allowSavePaymentArray =YES;
}
}
}
});
}
Your issue is that you are using fast enumeration on a background thread and you have the likely case that the array being enumerated on the background thread will be modified on the main (or some other) thread.
The fix is simple enough. Create a copy of the array:
__block int loopCount = 0;
NSMutableArray *archiveArray = [NSMutableArray arrayWithCapacity:dataSourceArray.count];
NSArray *tempArray = [dataSourceArray copy]; // make a copy
for (NSMutableDictionary *object in tempArray) {
You may use a serial queue, this will execute one process at the time.
dispatch_queue_t kAsyncQueue;
kAsyncQueue = dispatch_queue_create("com.example.MyQueue", NULL);
Here the documentation from apple:
https://developer.apple.com/library/mac/documentation/Performance/Reference/GCD_libdispatch_Ref/index.html#//apple_ref/c/func/dispatch_queue_create
When you finished working with your array on your background queue, spin off the block on main thread to make sure that you have everything going in certain order.
I have this code, but when I log the mediaDictionaryArray, I get null. Does the receiver array have to be initialized with a value first or can I add objects to an empty array? Does [NSArray array] vs. [[NSArray alloc]init] have anything to do with it?
Adding dictionary from API call that happens i times. Asynch call will return the dictionary - can't be sure if NSMutableArray will work in catchJSONArray since asynch nature of call will make the array of indeterminate size which will make it hard to use later on.
Updated with relevant bit.
for (int i = 0; i<[array count]; i++) {
NSString *getString = array[i];
NSLog(#"getstring %#", getString);
[client GET:getString parameters:nil success:^(NSURLSessionDataTask *task, id responseObject) {
{
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)task.response;
if (httpResponse.statusCode == 200) {
dispatch_async(dispatch_get_main_queue(), ^{
_locationMediaArray = (NSArray*)responseObject[#"data"];
[self catchJSONArray:_locationMediaArray];
then here is method with the array issue
-(void)catchJSONArray:(NSArray*)array{
NSArray *catchJSONArray = [NSArray array];
_mediaDictionaryArray = [catchJSONArray arrayByAddingObjectsFromArray:array];
NSLog(#"mediaDictionaryArray %#", _mediaDictionaryArray);
}
arrayByAddingObjectsFromArray returns a new array containing your objects, as an NSArray can not be changed once created.
If you want to change an existing array, you should be using an NSMutableArray.
The best way you could do this is:
_mediaDictionaryArray=[NSArray arrayWithArray:otherArray];
That will create a new array with the contents of otherArray and assign it to _mediaDictionary.
No need to alloc init your array just pass the refrence of your other array. If
_mediaDictionaryArray is mutable array then use below:-
_mediaDictionaryArray=[array mutableCopy];
If it is non mutable array then use below
_mediaDictionaryArray=[array copy];
Hi I need to download over 2000 records from azure , the maximum you can download is 1000 at the time , so I need to use a completion handler to download 200 at the time.
They posted this code as an example but I don't know how to use.
If I copy this to Xcode there is an error
(bool)loadResults() - Error " Expect Method Body "
Returning data in pages
Mobile Services limits the amount of records that are returned in a single response. To control the number of records displayed to your users you must implement a paging system. Paging is performed by using the following three properties of the MSQuery object:
BOOL includeTotalCount
NSInteger fetchLimit
NSInteger fetchOffset
In the following example, a simple function requests 20 records from the server and then appends them to the local collection of previously loaded records:
- (bool) loadResults() {
MSQuery *query = [self.table query];
query.includeTotalCount = YES;
query.fetchLimit = 20;
query.fetchOffset = self.loadedItems.count;
[query readWithCompletion:(NSArray *items, NSInteger totalCount, NSError *error) {
if(!error) {
//add the items to our local copy
[self.loadedItems addObjectsFromArray:items];
//set a flag to keep track if there are any additional records we need to load
self.moreResults = (self.loadedItems.count < totalCount);
}
}];
}
thanks for your help.
If you are getting Error " Expect Method Body " then you copied it into your code incorrectly and there is a formatting issue.
If you want to load data with paging in a single call, I would do something like this:
in your .h file declare
typedef void (^CompletionBlock) ();
#property (nonatomic, strong) NSMutableArray *results;
in your .m file
- (void)loadData
{
self.results = [[NSMutableArray alloc] init];
MSClient *client = [MSClient clientWithApplicationURLString:#"YOUR_URL" applicationKey:#"YOUR_KEY"]
MSTable *table = [client tableWithName:#"YOUR_TABLE"];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"YOUR_SELECT_FILTER"];
MSQuery *query = [[MSQuery alloc] initWithTable:table predicate:predicate];
//note the predicate is optional. If you want all rows skip the predicate
[self loadDataRecursiveForQuery:query withCompletion:^{
//do whatever you need to do once the data load is complete
}];
}
- (void)loadDataRecursiveForQuery:(MSQuery *)query withCompletion:(CompletionBlock)completion
{
query.includeTotalCount = YES;
query.fetchLimit = 1000; //note: you can adjust this to whatever amount is optimum
query.fetchOffset = self.results.count;
[query readWithCompletion:(NSArray *items, NSInteger totalCount, NSError *error) {
if(!error) {
//add the items to our local copy
[self.results addObjectsFromArray:items];
if (totalCount > [results count]) {
[self loadDataRecursiveForQuery:query withCompletion:completion];
} else {
completion();
}
}
}];
}
Note: I haven't tested this code, but it should work more or less.
I have a complex JSON file that I need to parse into a CoreData table. Currently, I capture the data into an NSArray with this format and the following 6 elements:
2013-08-29 10:54:04.930 iTrackTest[1542:c07] athleteRecords[0]: #SchoolID
2013-08-29 10:54:04.930 iTrackTest[1542:c07] athleteRecords[1]: #LastName
2013-08-29 10:54:04.930 iTrackTest[1542:c07] athleteRecords[2]: #Gender
2013-08-29 10:54:04.931 iTrackTest[1542:c07] athleteRecords[3]: SchType
2013-08-29 10:54:04.931 iTrackTest[1542:c07] athleteRecords[4]: #FirstName
2013-08-29 10:54:04.931 iTrackTest[1542:c07] athleteRecords[5]: #IDAthlete
First question, it appears that SchType is a k-dimensional NSArray of NSDictionaries. Is that true?
I have been capturing simpler, single-tiered JSON files using code from Paul Hegarty of Stanford:
dispatch_async(fetchQ, ^{
NSArray *athleteRecords;
athleteRecords = [AthleticNetDataFetcher retrieveDataForAthleteWithID:athleteID];
NSLog(#"In %#: athleteRecords has %d records",NSStringFromClass([self class]), [athleteRecords count]);
NSLog(#"NSArray with athleteRecords: %#", athleteRecords);
[document.managedObjectContext performBlock:^{
int iCount=0;
for (NSDictionary *athleteInfo in athleteRecords) {
[self resultsWithAthleteInfoForAthleteWithID:athleteInfo inManagedObjectContext:document.managedObjectContext];
NSLog(#"athleteRecords[%d]: %#", iCount, athleteInfo);
iCount++;
}
[document saveToURL:document.fileURL forSaveOperation:UIDocumentSaveForOverwriting completionHandler:NULL];
}];
});
I need data elements from each node for every record in my CoreData table. For example, SchoolName from School node, IDSeason from Season node, and all elements from Results node would be written to a single CoreData table row (record).
Do I need to resort to dot notation and abandon the iteration through the NSArray or do I need to capture multiple NSArrays each with data further down the nodes? Having a hard time getting my head around this.
Thanks!
Not sure why I had such a difficult time getting my head around this, but Hot Licks got me on the right track.
Here is what I learned that might be helpful to others:
If you have multiple NSDictionaries embedded within an array, it is much simpler to parse these sub-dictionaries in other methods.
+(void)parseDivisionBranches:(NSDictionary *)schTypeDictionary usingStudentInfoFrom:(NSDictionary *)myAthleteInfo
intoManagedDoc: (UIManagedDocument *)document
{
NSArray* schoolDivisions = [self wrapDictionaryInArrayIfNecessary:[schTypeDictionary valueForKeyPath:#"School.SchoolDivision"]];
for (NSDictionary* schoolDivision in schoolDivisions) {
[MarksFromMeets parseDictionaryWithXcMarksForAthlete:(NSString*) [myAthleteInfo objectForKey:#"athlete_ID"]
fromDictionary:(NSDictionary *)schoolDivision
intoThisManagedDoc:(UIManagedDocument *)document];
}
}
In instances where only a single NSDictionary is passed at a particular level of the tree, it is simpler to embed that NSDictionary inside an NSArray so that you can use the same code to extract data; therefore, I always check to see if have an NSArray or NSDict.
+ (NSArray*) wrapDictionaryInArrayIfNecessary:(NSObject*)dictionaryMasquaradingAsAnArray
{
NSMutableArray* newArray = [[NSMutableArray alloc] init];
if([dictionaryMasquaradingAsAnArray isKindOfClass:[NSArray class]]) {
newArray = [dictionaryMasquaradingAsAnArray copy];
}else if([dictionaryMasquaradingAsAnArray isKindOfClass:[NSDictionary class]]) {
[newArray addObject:dictionaryMasquaradingAsAnArray];
}else {
NSString *className = NSStringFromClass([dictionaryMasquaradingAsAnArray class]);
NSLog(#"ERROR - dictionaryMasquaradingAsAnArray of %# class", className);
newArray = nil;
}
return newArray;
}
Then parse each sub-dictionary in turn by calling the method associated with the branch of the data tree, in this case:
+ (void)parseDictionaryWithXcMarksForAthlete:(NSString*)withAthleteID
fromDictionary:(NSDictionary *)dictionary
intoThisManagedDoc:(UIManagedDocument *)document
{
NSArray* seasons = [self wrapDictionaryInArrayIfNecessary:[dictionary valueForKeyPath:#"Season"]];
BOOL* parsedSeasonData;
for (NSDictionary* season in seasons) {
parsedSeasonData = [self parseDictionaryWithSeasonsListings:(NSString*)withAthleteID
fromDictionary:(NSDictionary *)season
intoThisManagedDoc:(UIManagedDocument *)document];
}
}
At some nodes, I had to capture data and pass it along down the chain for use later when I would ultimately write a record to CoreData. Again, thanks to Hot Licks and hope this helps others.