I am using this helper class: Secure-NSUserDefaults
I'm running into a problem where the validationhash differs from the stored hash for a NSDictionary when I modify the latter and try saving it again.
Here is my code:
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
BOOL valid = NO;
NSDictionary * modDict = [defaults secureDictionaryForKey:aKey valid:&valid];
if (!valid) {
//handle
}
for (NSString * modKey in modDict) {
NSMutableArray * modArray = [[modDict objectForKey:modKey] mutableCopy];
NSString * newValue = #"newValue";
[modArray replaceObjectAtIndex:0 withObject:newValue];
NSMutableDictionary *newModDict = [modDict mutableCopy];
[newModDict setObject:modArray forKey:modKey];
[defaults setSecureObject:newModDict forKey:aKey];
[defaults synchronize];
}
When I run this code once, kill the app, and start it again, all works as it should, and the validationhash matches. It always matches correctly on the first run, in fact. In other words: my code saves out the secure dictionary and the hash matches on the subsequent app run just fine (once).
BUT - if I run the same code a second time without killing the app between runs, the validationhash is different, and the &valid check fails.
I was able to trace down the problem to this line:
[newModDict setObject:modArray forKey:modKey];
If I comment it out, it works fine all the time, but then of course, the modification I am trying to make isn't made at all, I simply copy the Dictionary "as is".
I'm struggling to figure out what I am doing wrong or how to modify and re-save the NSDictionary so that the hash matches.
Thanks in advance for any help!
LOL! Funny how sometimes writing down a question in a concise way triggers new ideas. It just occurred to me what I had to do, and just now solved it like this:
[newModDict setObject:[modArray copy] forKey:modKey];
i.e. I had to make the edited NSArray non-mutable first.
Maybe someone will benefit from the answer. I'm too embarrassed to mention how long I worked on this last night.
Related
In the login procedures in my App i am looping through a server response and save some data in NSUserDefaults:
NSMutableArray *unitTypeArray = [[NSMutableArray alloc] init];
[unitTypeArray addObject:#{#"Name":#"Item",
#"Description":#"Item",
#"ID":#"0",
#"Time":#"2014-12-29 12:00:00",
#"AddedByUserID":#"2",
#"Depth":#"0",
#"Height":#"0",
#"Width":#"0",
#"softLimit":#"10"}];
for (NSMutableDictionary *unitType in response) {
[unitType setValue:#10 forKey:#"softLimit"];
[unitTypeArray addObject:unitType];
}
[[NSUserDefaults standardUserDefaults] setObject:unitTypeArray forKey:#"unitTypes"];
Later i am retrieving this Array:
self.unitTypeArr = [[self.preferences objectForKey:#"unitTypes"] mutableCopy];
but i am getting a NSDictionary back. This was perfectly working in iOS < 9.0 but since iOS 9.0 my app is crashing. Is there a fix for that or do i have to convert the NSDictionary to NSArray?
you are saving unitTypeArray into NSUserDefault. right? as it's type NSMutableArray; So, you will get Array from preference. Why you expecting NSDictionary?
Retrieve Data by this way
NSMutableArray *unitTypeArr = [[NSUserDefaults standardUserDefaults] objectForKey:#"unitTypes"];
I've found a way to get it going. The first code block in my question was only run if the data hasn't been retrieved before. I can't understand why, but if i run this every time on login, it is working. Even though it contains exactly the same data.
When the following code executes, it logs "0" even though the indexPath.row selected is "1".
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
NSInteger location = indexPath.row;
[userDefaults setInteger:location forKey:#"savedlocations"];
NSInteger location2 = [userDefaults integerForKey:#"savedlocation"];
NSLog(#"l: %ld", location);
You've got several things that need to be fixed:
You're saving with the key savedlocations, but retrieving it with savedlocation. Note that the first is plural.
You're logging location, not location2, which is what's being pulled from NSUserDefaults. location2 will always be 0, because of the point above.
Edit: I had a third point here about calling synchronize, but it turns out that that is irrelevant to this situation, as explained by rmaddy in the comments below.
Other than those quick fixes, though, your configuration ought to work.
You are looking for the key "savedlocation" instead of "savedlocations".
EDIT:
0 is being returned by the method, which is what is returned for an NSInteger that can't be found. My bad on the weird wording.
I'm attempting to build an app which will track a route, then store the route in parse.com so I can overlay the route taken by a user using MKpolyline.
I'm very new to Objective-c and IOS development, so please excuse my ignorance!!
I'm stuck when I try to save the route taken, then send/save the location array so I can rebuild the MKpolyline on the next view controller which is opened when the user completes the activity.
I'm not sure whether to save the location array to NSUserDefaults or save it to core data. At the moment I am converting the Array to an NSValue and the saving it to NSUserDefaults like so:
count = [self.locations count];
CLLocationCoordinate2D coordinates[count];
for (NSInteger i = 0; i < count; i++) {
coordinates[i] = [(CLLocation *)self.locations[i] coordinate];
NSValue *locationValue = [NSValue valueWithMKCoordinate:coordinates[i]];
[_locationsArray addObject:locationValue];
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
[defaults setObject:totalDistance forKey:#"totalDistance"];
[defaults setObject:_locationsArray forKey:#"mapOverlay"];
// [defaults setDouble:_totalTime forKey:#"totalTime"];
[defaults setObject:avgSpeedToBeSaved forKey:#"averageSpeed"];
[defaults setObject:totalCalories forKey:#"totalCalories"];
[defaults synchronize];
Is this the right way to do this? And how do I rebuild the locations Array.
If anyone could point me in the right direction it would be greatly appreciated.
I've now changed my code to what was suggested by manecosta to rebuild the CLLocationCoordinates to create an MKPolyline, but my issue now is that the array is Null from where I start to convert into an NSValue. I am unable to figure out why this is, is there something wrong with the way I'm building LocationsArray in the first place?
Yes, I guess you're doing it right and to rebuild just do the opposite, which should be something like:
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSArray *locationsArray = [defaults objectForKey:#"mapOverlay"];
CLLocationCoordinate2D coordinates[[locationsArray count]];
NSInteger i = 0;
for(NSValue *locationValue in locationsArray){
coordinates[i] = [locationValue MKCoordinateValue];
i++;
}
About the fact that you're using User Defaults to store tons of data. I don't really know what is correct, but I'll tell you that I've previously used it to store the cache of my app which were quite big arrays and dictionaries and it never failed me.
You can get object from NSUserDefaults by using objectForKey:
As you have [defaults setObject:totalDistance forKey:#"totalDistance"];.
Now if you want to get the totalDistance back from the NSUserDefaults` then you can use following code:
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSArray *arrayLocation = [defaults objectForKey:#"totalDistance"];
But a piece of advise for you is not to use NSUserDefaults for your project. NSUserDefaults should only be used to store little amount of data (It's just a personal opinion.). Your locationArray is going to be quite large. Right?
A nice comparison between the two options NSUserDefaults & CoreData
What are the limitations of NSUserDefaults?
I'm posting answer here (from above link - in case the question gets unavailable in future time - who knows !!)
NSUserDefaults offers a trivial learning curve and thread safe
implementation.
Otherwise I've found Core Data superior in every way. Especially with
regards to configuring default values and migration routines.
EDIT: I actually haven't tried but it seems to be this way..
Tell me if it does not work.
To get back NSArray from NSValue
NSValue *value = [defaults objectForKey:#"mapOverlay"];
NSArray *arrayTemp;
[value getValue:&arrayTemp];
can you help me? I´m searching here for a while and testet many things... no solution found!
I have set a NSMutableArray in the .h File:
#interface ViewController : UIViewController
{
NSMutableArray *Transactions;
}
In the .m file in the ViewDidLoad Method I initialized it and load the Array from the UserDefaults:
Transactions = [NSMutableArray array];
or
Transactions = [NSMutableArray new];
Transactions = [defaults objectForKey:#"transactions"];
Later i add a new Dictionary to it:
[insert setObject:confirmPaymentStatus forKey:#"status"];
[insert setObject:confirmPaymentAmount forKey:#"amount"];
[insert setObject:confirmPaymentDiscription forKey:#"description"];
[insert setObject:timestamp forKey:#"time"];
NSLog(#"%#",insert);
[Transactions addObject:insert];
NSLog(#"Transactions Array:\n%#",Transactions);
[defaults setObject:Transactions forKey:#"transactions"];
[defaults synchronize];
The insert Dictonary is full of data and then i got this from the Log:
2014-02-05 20:56:53.691 PPEasyPay Pro Pro[21907:60b] {
amount = "0.91";
description = "Polaris 123123";
status = COMPLETED;
time = "2014-02-05T11:56:51.248-08:00";
}
2014-02-05 20:56:53.692 PPEasyPay Pro Pro[21907:60b] Transactions Array:
(null)
It sounds like [defaults objectForKey:#"transactions"] is returning null, and therefore you're assigning your array to null. From there, you add an object to your non existent array, and since the array doesn't exist, nothing actually happens. Instead, you should create your array, and only add the objects from the other array to it if they exist.
NSArray *newStuff = [defaults objectForKey:#"transactions"];
transactions = [NSMutableArray new];
if (newStuff) {
[transactions addObjectsFromArray:newStuff];
}
Side note, your instances should be camelCase starting with a lowercase letter.
Transactions = [NSMutableArray new];
Transactions = [defaults objectForKey:#"transactions"];
When you do that, you are creating a new NSMutableArray, then that array is being discarded when you assign the result of [defaults objectForKey:#"transactions"] to the same variable.
I'd be willing to guess that the result of [defaults objectForKey:#"transactions"] is nil, which is why your array is nil.
Also, by convention, the Transactions variable should begin with a lowercase letter, or even an underscore followed by a lowercase letter.
The problem is that Transactions itself was nil to start with. You then say:
[Transactions addObject:insert];
But this has no effect: sending addObject: to nil still leaves it as nil.
What you need to do is this: after you fetch Transactions from the defaults, look to see if it is nil. If it is, set it to an empty mutable array instead.
Also, please note that this code is utterly stupid:
Transactions = [NSMutableArray array];
Transactions = [defaults objectForKey:#"transactions"];
You set Transactions to an empty mutable array, but then you immediately throw away that empty mutable array and replace it by whatever comes from the defaults (i.e. nil).
Transactions = [NSMutableArray array];
or
Transactions = [NSMutableArray new];
Transactions = [defaults objectForKey:#"transactions"];
You have several problems here. The first is that what you've written is the object equivalent of this:
int i;
i = 5;
i = 3;
What's i? Well, 3 of course. What does the i = 5 line do? Absolutely nothing. (Except possibly take CPU time, depending on optimization.)
The second problem is that [defaults objectForKey:] is going to return an immutable object.
From Apple's docs for objectForKey::
Special Considerations
The returned object is immutable, even if the value you originally set was mutable.
The reason this works without error is that objectForKey: returns an id; you can assign an id to any object variable without an error. But this is like a C pointer cast, not a type coercion. You haven't made the return value a NSMutableArray, you've just shown the compiler you don't care what it is.
What you want is this:
NSArray *readOnlyTransactions = [defaults objectForKey:#"transactions"];
if (readOnlyTransactions == nil) {
readOnlyTransactions = #[];
}
_transactions = [readOnlyTransactions mutableCopy];
You'll notice I renamed your instance variable to _transactions. This is the convention for instance variables, and you should probably adapt to it.
You can do this more succinctly with the ?: operator, if you're comfortable with using it:
_transactions = [([defaults objectForKey:#"transactions"] ?: #[]) mutableCopy];
What this does:
Read [defaults objectForKey:#"transactions"].
If that returns nil, use an empty list instead.
Make a mutable copy of it.
Assign the thing to _transactions.
In my app I am storing a list of sets of data in NSUserDefaults. When it needs to be accessed, I create create an array (each key in NSUserDefaults contains and array of data) and use the data. It works great. Yay! Unfortunately, when I display the data in a UITableView, deleting one set of data ( which == one row in the UITableView ) is not working properly. This is how I am going about doing that:
if(buttonIndex == actionSheet.destructiveButtonIndex)
{
NSUserDefaults* defaults = [NSUserDefaults standardUserDefaults];
int iii = [defaults integerForKey:#"currentMap"];
NSMutableDictionary* dicOne = (NSMutableDictionary*)[[NSUserDefaults standardUserDefaults] dictionaryRepresentation];
for(int lll = iii; lll < [[defaults objectForKey:#"counterKey"] intValue]; lll++)
{
if(lll != [[defaults objectForKey:#"counterKey"] intValue] - 1)
[dicOne setObject:[dicOne objectForKey:[NSString stringWithFormat:#"%i", (iii + 1)]] forKey:[NSString stringWithFormat:#"%i", iii]];
else
[dicOne removeObjectForKey:[NSString stringWithFormat:#"%i", iii]];
}
[defaults setPersistentDomain:dicOne forName:[[NSBundle mainBundle] bundleIdentifier]];
[defaults synchronize];
int counter = [defaults integerForKey:#"counterKey"];
counter--;
[defaults setInteger:counter forKey:#"counterKey"];
[defaults synchronize];
[self performSelector:#selector(done:) withObject:nil afterDelay:.3];
}
The idea behind this code is to start a for loop at the index of the selected row to be deleted ( variable 'iii'). This method moves through the dictionary, moving each index down a level.
Imagine the dictionary containing five sets of data (five keys), labeled 0, 1, 2, 3, and 4. When 2 gets marked for deletion, this code moves through the dictionary, assigning the data contained in key 3 to key 2, the data in key 4 to key 3, then deletes the remaining copy in key three. The counterKey then gets decremented (as this is the number I use to keep track of how many keys exist, so I can tell the UITableView how many cells it needs to create). Or at least that is what I think it should do.
But it doesn't do what I think it should do; when a cell gets marked for deletion, what really happens is that the data for that key shows up as NULL, and the keys do not 'slide' like I think they should.
The done: method dismisses the information view (that contains the delete button and information about the selected row) and returns to the view that holds the UITableView.
Does this concept make sense, and if so, why doesn't it work? Thanks for your time.
***Edit:
Thank you, jrturton, using an NSArray worked, but sometimes deleting it (or trying) crashes with this error:
* Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: '-[__NSCFArray removeObjectAtIndex:]: mutating method sent to immutable object'
* First throw call stack:
(0x14c7052 0x1658d0a 0x146fa78 0x146f9e9 0x14c109f 0xd381 0x8cba1f 0x14c8ec9 0x5105c2 0x51055a 0x5b5b76 0x5b603f 0x5b52fe 0x535a30 0x535c56 0x51c384 0x50faa9 0x24adfa9 0x149b1c5 0x1400022 0x13fe90a 0x13fddb4 0x13fdccb 0x24ac879 0x24ac93e 0x50da9b 0x1ef9 0x1e75)
Which is odd, because there is not a single NSArray used in that class; all of them are mutable.
This is the line of the crash (I am reasonably sure):
NSMutableArray* outerArray = (NSMutableArray*)[defaults objectForKey:#"mapSaveDataKey"];
[outerArray removeObjectAtIndex:iii];
Fix:
Replace:
[defaults objectForKey:#"mapSaveDataKey"];
With:
[defaults mutableArrayValueForKey:#"mapSaveDataKey"];
What you are doing seems unnecessarily complicated. Why not just have a single array stored in your user defaults, under a key (e.g #"data"), then remove or add items to that array? This way you don't have to maintain separate counts or anything like that. You can use objectAtIndex: to get hold of item 4, for example, and when you delete item 2, the items above it will move down a notch anyway.
Update - as Matthew Gillingham points out in the comments, I did mean a mutable array. As youve discovered, you can't alter the number or position of elements in an NSArray.
Note also that arrayForKey: will return an NSArray even if you stored a mutable array in the first place, so when getting the value from defaults you will have to do the following:
NSMutableArray *myData = [NSMutableArray arrayWithArray:[defaults arrayForKey:#"data"]];
Where defaults is your user defaults object.