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.
Related
I got problem with saving and comparing data. First I download data and save my data (array,dict)to userDefaults. After redownloading I need to compare if my new array have some new data which I haven't saved in userDefaults. So its mean I should find the data that is not the same inside my old data and add them to my new array.
NSMutableDictionary* tmpDict = [NSMutableDictionary new];
NSMutableDictionary* copyDict = [NSMutableDictionary new];
NSMutableArray *dataGroupsArr = [NSMutableArray new];
NSMutableDictionary *dataGroupsDict = [NSMutableDictionary new];
dataGroupsDict[#"name"] = #"FirstGroup"; // I dont need groups at the moment
NSMutableArray *datas = [[NSMutableArray alloc]init];
FOR ........ (parser)
{
NSMutableDictionary *data = [[NSMutableDictionary alloc]init];
data[#"dataID"] = [#"some data from parser"];
[datas addObject:data];
}];
dataGroupsDict[#"datas"] = datas;
[dataGroupsArr addObject:dataGroupsDict];
tmpDict[#"dataGroups"] = dataGroupsArr;
After I save data Im trying to load them from userdefualts
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
copyDict = [userDefaults objectForKey:#"dataDownloadArr"];
//data never added
if(copyDict == nil){
[userDefaults setObject:tmpDict forKey:#"dataDownloadArr"];
[userDefaults synchronize];
} else {
for (int i = 0; i< [copyDict[#"dataGroups"][0][#"datas"]count]; i++){
NSLog(#"%#", copyDict[#"dataGroups"][0][#"datas"][i][#"dataID"]);
}
}
Now I don't know how to compare data, and if there is new data in my new array how to add it to old one.
Did you try to use NSData instance method isEqualToData?
Two data objects are equal if they hold the same number of bytes, and
if the bytes at the same position in the objects are the same.
https://developer.apple.com/documentation/foundation/nsdata/1409330-isequaltodata
I was also facing same issue what I did is I asked the backend developer to send a value as modified_date and create a dictionary containing dataArray and modified_date. So after re-downloading you can just check for modified_date and replace the dataArray instead of comparing every array element.
I am building something like an "instagram" app with posts with pictures. I am using Firebase to store/retrieve data.
My issue comes because the posts are shown kind of in a random way. For example, in cellForRowAtIndexPath, the first post is shown at indexPath[0], the next post is [1], but the next one which should be at [2], it shows at [0].
I will copy paste the most relevant code for the issue:
Here is the class where I retrieve the Scores:
////// Arrays to store the data
nameArray = [[NSMutableArray alloc]init];
scoreArray = [[NSMutableArray alloc]init];
photomealArray = [[NSMutableArray alloc]init];
keysArray = [[NSMutableArray alloc]init]; // Reference to know what post should be scored.
// Reference to the Database
self.storageRef = [[FIRStorage storage] reference];
FIRDatabaseReference *rootRef= [[FIRDatabase database] referenceFromURL:#"https://XXXXXXX.firebaseio.com/"];
// Query all posts
[[rootRef child:#"Posts"] observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot * _Nonnull snapshot) {
if(snapshot.exists){
NSDictionary *dict = snapshot.value;
for (NSString *key in dict) {
// Query all users to show the Name of the person who posts
[[rootRef child:#"Users"] observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot * _Nonnull Usersnapshot) {
if(Usersnapshot.exists){
NSDictionary *Userdict = Usersnapshot.value;
for (NSString *Userkey in Userdict) {
NSString *Users_UserID = [Userdict[Userkey] valueForKey:#"UserID"];
NSString *UserID = [dict[key] valueForKey:#"UserID"];
// If the userID matches with the person who posts, then add it to the array
if([Users_UserID isEqualToString:UserID]) [nameArray addObject:[NSString stringWithFormat:#"%#",[Userdict[Userkey] valueForKey:#"Name"]]];
}
}
}];
UIImage *mealPhoto = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:[dict[key] valueForKey:#"PostPhoto"]]]];
NSString *Score = [dict[key] valueForKey:#"Score"];
NSString *PostKeys = [dict[key] valueForKey:#"KeyforPost"];
if([Score isEqual:#"null"]) [scoreArray addObject:#"0"]; else [scoreArray addObject:Score];
[photomealArray addObject:mealPhoto];
[keysArray addObject:PostKeys];
}
}
} withCancelBlock:^(NSError * _Nonnull error) {
NSLog(#"%#", error.localizedDescription);
}];
If I quit and relaunch the app, the new post will be stored in whatever index is "empty" starting from [0], which is what it is expected.
This is how the didSelectRowAtIndexPath looks like:
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath*)indexPath{
FeedTableViewCell *cell =[tableView cellForRowAtIndexPath:indexPath];
if(cell.selectionStyle != UITableViewCellSelectionStyleNone){
if([score.text intValue] >= -1 && [score.text intValue] <= 1 && ![score.text isEqual: #""]){
NSString *keyforcell = [keysArray objectAtIndex:indexPath.row];
NSLog(#"key a usar :%#", keyforcell);
ref = [[FIRDatabase database] referenceFromURL:#"https://XXXXX.firebaseio.com"];
[[[[self.ref child:#"Posts"] child:keyforcell] child:#"Score"] setValue:[NSNumber numberWithInt:[score.text intValue]]];
[[[[self.ref child:#"Posts"] child:keyforcell] child:#"Rated"] setValue:#"YES"];
[scoreArray insertObject:[NSNumber numberWithInt:[score.text intValue]] atIndex:indexPath.row];
cell.selectionStyle = UITableViewCellSelectionStyleNone;
}
}
I have tried to play with observeSingleEventOfType and observeEventType because maybe this has to do with the fact that the single one is only called once. Also I've thought that viewDidLoad() is triggered after the cell is selected, but It shouldn't. Also, I think it has to do with the NSMutableArrays, but I am not completely sure
Can anyone help me out ?
UPDATE : I got to know that neither Firebase nor NSDictionary returns/stored sorted data, so my guess is that my issue has something to do with sorting the items. I got to know about orderbykey and orderbyvalue but they don't seem to work.
It is not clear to see what your issue is, but you should not forget there is no arrays in Firebase Database. The only collection type is dictionary, and dictionaries has no key order.
You should consider a sorting system on your objects, instead of directly adding them to arrays.
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) {
}];
}
I query to DynamoDB. The result is stored in queryResult array. With every new query to database the value of queryResult array is being changed (replaced). The problem comes when I want to populate my table view controller (the list of added friends) with all potential results.
I want to create new array that adds every new received result, but unfortunately its value gets overwritten with every new query result. For example, if for the 1st time I want to add "Adam" and next time "Michael" I want an array with values: (Adam, Michael). Now, Michael overwrites Adam. I ask you how can I force an array to store every newly added result - not to overwrite the existing one. Later I would like to loop through that array to populate tables rows. I would appreciate every hint, advice. Thank you very much.
- (IBAction)addButton:(UIButton *)sender {
//To store credentials typed by user.
NSString *userName = self.nameTypedTextField.text;
NSString *phoneNumber = self.phoneNumberTextField.text;
//To query for typed credentials.
AWSDynamoDBQueryExpression *query = [AWSDynamoDBQueryExpression new];
query.hashKeyAttribute = #"userPhoneNumber";
query.hashKeyValues = phoneNumber;
//To receive typed credentials.
AWSDynamoDBObjectMapper *objectMapper = [AWSDynamoDBObjectMapper defaultDynamoDBObjectMapper];
[[objectMapper query:[Users class] expression:query] continueWithBlock:^id(AWSTask *task) {
if (task.error) NSLog(#"The request failed. Error: [%#]", task.error);
if (task.exception) NSLog(#"The request failed. Exception: [%#]", task.exception);
if (task.result) {
//I receive query result.
AWSDynamoDBPaginatedOutput *paginatedOutput = task.result;
NSArray *queryResult = [[NSArray alloc] initWithArray:paginatedOutput.items];
NSLog(#"Query result: %#", queryResult);
//I wanted to create array that adds every object that is being stored in queryResult array. It doesnt work - it simply replaces its value with new result.
NSMutableArray *array = [[NSMutableArray alloc] init];
[array addObjectsFromArray:queryResult];
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
//Must be converted to NSData becouse of: Attempt to set a non-property-list object as an NSUserDefualts.
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:queryResult];
[userDefaults setObject:data forKey:#"User Data"];
[[NSUserDefaults standardUserDefaults] synchronize];
}
return nil;
}];
}
EDIT:
You need to save your very first query result first. arrayByAddingObjectFromArray requires noempty array (previousQueryResult) to add the results from the other array (queryResult). Otherwise you will end up with null statement and you wont be able to stack up the results. That issue occurs only on the very first application launch.
if (task.result) {
//To receive query result.
AWSDynamoDBPaginatedOutput *paginatedOutput = task.result;
NSArray *queryResult = [[NSArray alloc] initWithArray:paginatedOutput.items];
NSLog(#"Query result: %#", queryResult);
//Get the previously stored query results.
NSData *previousData = [[NSUserDefaults standardUserDefaults] dataForKey:#"User Data"];
NSArray *previousQueryResults = [NSKeyedUnarchiver unarchiveObjectWithData:previousData];
//If previousQueryResults is empty (due to first applcation launch).
if ([previousQueryResults count] == 0) {
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:queryResult];
[[NSUserDefaults standardUserDefaults]setObject:data forKey:#"User Data"];
[[NSUserDefaults standardUserDefaults] synchronize];
} else {
//Get the previously stored query results.
NSData *previousData2 = [[NSUserDefaults standardUserDefaults] dataForKey:#"User Data"];
NSArray *previousQueryResults2 = [NSKeyedUnarchiver unarchiveObjectWithData:previousData2];
//Add the new query results to what was previously stored.
NSArray *updatedArray = [previousQueryResults2 arrayByAddingObjectsFromArray:queryResult];
NSLog(#"Updated array: %#", updatedArray);
//Store the combined query results to user defaults.
NSData *dataUpdated = [NSKeyedArchiver archivedDataWithRootObject:updatedArray];
[[NSUserDefaults standardUserDefaults]setObject:dataUpdated forKey:#"User Data"];
[[NSUserDefaults standardUserDefaults] synchronize];
}
}
On each task completion, you are initializing a new array (which has no items in it) and adding only the new query results in it. This is why it seems like it is "overwriting" the results. When a task finishes, you should get the previous results from user defaults and add the new query results to that. I have updated your code to show how that can be done:
if (task.result) {
//I receive query result.
AWSDynamoDBPaginatedOutput *paginatedOutput = task.result;
NSArray *queryResult = [[NSArray alloc] initWithArray:paginatedOutput.items];
NSLog(#"Query result: %#", queryResult);
// Get the previously stored query results
NSData *previousData = [[NSUserDefaults standardUserDefaults] dataForKey:#"User Data"];
NSArray *previousQueryResults = [NSKeyedUnarchiver unarchiveObjectWithData:previousData];
// Add the new query results to what was previously stored
NSArray *array = [previousQueryResults arrayByAddingObjectsFromArray:queryResults];
// Store the combined query results to user defaults
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:array];
[[NSUserDefaults standardUserDefaults] setObject:data forKey:#"User Data"];
[[NSUserDefaults standardUserDefaults] synchronize];
}
I am trying to set up three NSMutableArray to use in UITableView.
Here is my code:
for (PFObject *object in objects) {
PFUser *user = (PFUser *) object[#"user"];
[ [user objectForKey:#"image"] getDataInBackgroundWithBlock:^(NSData *data, NSError *error) {
if (!error)
{
//Add Comment text
[_commentsArray insertObject:[object objectForKey:#"comment"] atIndex:i];
//Add comment Id
[_commentIDArray insertObject:object.objectId atIndex:i];
//Add user image
[_picsArray insertObject:[UIImage imageWithData:data] atIndex:i ];
if (i == [objects count]-1)
{
[self.tableView reloadData];
}
}
else
{
NSLog(#"Errrror == %ld",(unsigned long)[_picsArray count]);
}
i++;
}];
}
In the PFQuery I am ordering it:
[query orderByDescending:#"createdAt"];
But as far as I can understand image in first row is large. So it takes time to download it. So it goes to second loop. Try to download image. Size is small. Download finished. Add to array. Now download for first image is finished. Add to array but to second place. How can manage it so it add items one by one in the order?
Check this:
// initially, add place holder
for (int i=0; i<objects.count; i++) {
[_commentsArray addObject:#""];
[_commentIDArray addObject:#""];
[_picsArray addObject:#""];
}
for (PFObject *object in objects) {
PFUser *user = (PFUser *) object[#"user"];
[ [user objectForKey:#"image"] getDataInBackgroundWithBlock:^(NSData *data, NSError *error) {
if (!error)
{
NSInteger orderIndex = [objects indexOfObject:object];
//Replace Comment text
[_commentsArray replaceObjectAtIndex:[object objectForKey:#"comment"] atIndex:orderIndex];
//Replace comment Id
[_commentIDArray replaceObjectAtIndex:object.objectId atIndex:orderIndex];
//Replace user image
[_picsArray replaceObjectAtIndex:[UIImage imageWithData:data] atIndex:orderIndex ];
if (i == [objects count]-1)
{
[self.tableView reloadData];
}
}
else
{
NSLog(#"Errrror == %ld",(unsigned long)[_picsArray count]);
}
i++;
}];
}
Rather than downloading image and create array to populate tableview, you have to just create array of PFObjects and use it with SDWebImage for Asynchronous image downloading without any issue or blocking UI.
I'm guessing that the question is really about not expending effort to download images beyond the scroll position while the visible rows are still being fetched.
The solution to that problem is to load images lazily, when they're needed to configure a cell in cellForRowAtIndexPath:. There's plenty of generic content available about this idea. For a parse-specific solution, see PFImageView here.
The gist is that image view will take care of loading and caching an image from the file. It will do this asynchronously, so there will be a low perceived lag. Just give the file to the image view and let it do the rest...
Your cellForRowAtIndexPath will look something like this:
// just guessing that your "objects" array is the table's datasource
PFObject *object = self.objects[indexPath.row];
PFUser *user = (PFUser *) object[#"user"];
// put a PFImageView in the cell (in this case with a tag==32)
PFImageView *imageView = (PFImageView *)[cell viewWithTag:32];
imageView.image = [UIImage imageNamed:#”placeholder.png”];
imageView.file = [user objectForKey:#"image"]; // assuming this is a file attribute
[imageView loadInBackground];
You have a problem that you try to do order based adding, where your blocks fire asynchronously so it can be in random order.
You should change to a dictionary or any other keyed data structure and use keys for your comments and pics (e.g. use comment id as the key).
Also double check if the callback of the block is executed on the main queue or any serial queue, because if it's not you need to add locks.
I had the same problem, my images were downloaded but not appearing in the order it should, my table view images and the titles were not matching.
To solve that, I created a column at my class in Parse.com that hold exclusively nameForImages, then each downloaded image is saved using this name.
The nameForImages had to be the same used for the column title, for example:
Title = Pepperoni and Four Cheese | nameForImage =
PepperoniAndFourCheese
Title - Muzzarella and Spinach | nameForImage = MuzzarellaAndSpinach
Etc...
This trick fit to solve my problem because the name of the image and the title appearing in the cell were short and had no special caracters.
I hope it helps or light a solution, good luck.