I'm trying to Update a Dictionary, saved in the UserDefaults.
However, it just doesn't want to update the Object at the given Index. I have no idea why. The Object with it's values stay the same.
Code:
- (void) saveUser
{
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSMutableArray *users = [[NSMutableArray alloc] initWithArray:[defaults objectForKey:#"users"]];
BOOL userAlreadyAdded = FALSE;
NSUInteger count = 0;
for (NSMutableDictionary *user in users) {
if ([[user objectForKey:#"username"] isEqualToString:[defaults objectForKey:#"username"]]) {
NSLog(#"User found: %#", user);
[user setObject:sid forKey:#"sess"];
[user setObject:mid forKey:#"seca"];
[users replaceObjectAtIndex:count withObject:user];
userAlreadyAdded = TRUE;
}
count++;
}
if (!userAlreadyAdded) {
NSMutableDictionary *userToAdd = [[NSMutableDictionary alloc] initWithObjectsAndKeys:
[defaults objectForKey:#"username"], #"username",
sid, #"sess",
mid, #"seca",
nil];
[users addObject:userToAdd];
}
NSLog(#"USERS TO SAVE: %#", users);
[defaults setObject:users forKey:#"users"];
[defaults synchronize];
}
Values returned from NSUserDefaults are immutable, even if you set a
mutable object as the value. For example, if you set a mutable string
as the value for "MyStringDefault", the string you later retrieve
using stringForKey: will be immutable.
try removing the object and adding it again.
For each user create a Mutable Dictionary from the one you have like you already did with 'users' (initWithArray).
NSMutableDictionary* mutableUser = [NSMutableDictionary dictionaryWithDictionary:user];
Also I'm not entirely sure you're safe to presume that the object at index of your count is the object you expect because the for in loop enumerates differently. I would use setObjectForKey, not replaceAtIndex.
Related
I'm trying to use NSUserDefaults to save two variables after app shutdown. One being firstBoot and the other showAgain. Prepare for bad code-
- (void)viewDidLoad
{
[super viewDidLoad];
NSUserDefaults *defaults1 = [NSUserDefaults standardUserDefaults]; //Here is where it is supposed to be loading the firstBoot variable to check whether or not it is the first boot
NSInteger boot = [defaults1 integerForKey:#"firstBoot"];
firstBoot = boot; //Sort of pointless variable swapping
if (firstBoot == 0) //Detects if isn't first boot, checks whether or not showAgain is yes or no (1=yes, 0=no)
{
NSUserDefaults *defaults1 = [NSUserDefaults standardUserDefaults];
NSInteger boot = [defaults1 integerForKey:#"firstBoot"];
NSUserDefaults *defaults2 = [NSUserDefaults standardUserDefaults];
NSInteger show = [defaults2 integerForKey:#"showAgain"];
firstBoot = boot; //More var swapping
showAgain = show; //^^
}
else
{
firstBoot = 1; //If it is the first boot, or the variable wasn't 0, it sets them both to 1.
showAgain = 1;
}
if ((firstBoot == 1) || (showAgain == 1)) //Checks if its the firstBoot or showAgain is set to yes
{
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:#"Check the 'About' tab for help!" //Displays alert
message:#""
delegate:self
cancelButtonTitle:#"Don't show again"
otherButtonTitles:#"OK", nil];
[alert show];
}
else
{
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:#"firstboot and showagain != 1" //Displays alert if it isn't the first boot, or isn't 1 and showAgain isn't set to yes(1).
message:#""
delegate:self
cancelButtonTitle:#"wrong"
otherButtonTitles:#"OK", nil];
[alert show];
}
}
Alert-
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex {
if (buttonIndex == [alertView cancelButtonIndex])
{
showAgain = 0; //If user clicks "Don't show again", set showAgain to off(0) and tell it it isn't the first boot anymore
firstBoot = 0;
NSUserDefaults *defaults1 = [NSUserDefaults standardUserDefaults];
[defaults1 setInteger:0 forKey:#"firstBoot"]; //Hopefully stores firstBoot as 0
[defaults1 synchronize];
NSUserDefaults *defaults2 = [NSUserDefaults standardUserDefaults];
[defaults2 setInteger:0 forKey:#"showAgain"]; //Again storing showAgain as 0
[defaults2 synchronize];
}
}
The first alert should pop up the very first time you run the app, while the second one is a debugger placeholder, and should run every time after the first time. For me, the second one is running first, telling me that firstBoot and showAgain != 1.
NSInteger boot = [defaults1 integerForKey:#"firstBoot"]; will return 0 until you have saved a value in your defaults. 0, NO or nil are returned for keys that don't have any values stored.
The correct way to handle this is to register default values before you try to retrieve these user defaults.
// register default values. until you save your own value "firstBoot" will return YES
// you should put these two lines in the `application:didFinishLaunchingWithOptions:`
// method of the AppDelegate. Add all userdefaults that should have default values there
NSDictionary *defaultUserDefaults = #{ #"firstBoot" : #YES };
[[NSUserDefaults standardUserDefaults] registerDefaults:defaultUserDefaults];
// use BOOL instead of integer
BOOL firstBoot = [[NSUserDefaults standardUserDefaults] boolForKey:#"firstBoot"];
if (firstBoot) {
NSLog(#"first launch");
[[NSUserDefaults standardUserDefaults] setBool:NO forKey:#"firstBoot"];
[[NSUserDefaults standardUserDefaults] synchronize];
}
else {
NSLog(#"has been launched before");
}
How would firstBoot be set to YES on the first boot? Who would have set it?
Call registerDefaults: at the start of the application for all properties that should have values different from 0 before they have been explicitely set. For example, with a key firstBoot = YES. Alternatively, use a property "notFirstBoot" where the return value for missing properties ("NO") is what you actually want.
And there is really no need to call standardUserDefaults again and again.
In my OS X app, I'm trying to save and retrieve the tag of a radio button. The error occurs on the line marked "<-HERE" in setPreferenceRotor. There is a valid tag coming in.
// PreferenceController.h
extern NSString * const myCellKey;
extern NSString * const myMatrixChangedNotification;
#interface PreferenceController:NSWindowController
{
IBOutlet NSMatrix *matrixRotor;
}
- (IBAction)setRotorTag:(id)sender;
+ (NSInteger)preferenceRotorTag;
+ (void)setPreferenceRotor:(NSInteger)matrixTag;
#end
// PreferenceController.m
NSString * const myMatrixChangedNotification = #"myRotorChanged";
#implementation PreferenceController
- (void)windowDidLoad
{
[super windowDidLoad];
[matrixRotor selectCellWithTag:[PreferenceController preferenceRotorTag]];
}
+ (NSInteger)preferenceRotorTag
{
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
NSData *tagAsData = [defaults objectForKey:myCellKey];
return [NSKeyedUnarchiver unarchiveObjectWithData:tagAsData];
}
+ (void)setPreferenceRotor:(NSInteger)matrixTag
{
//NSInteger mt = matrixTag;
NSData *tagAsData = [NSKeyedArchiver archivedDataWithRootObject:matrixTag]; **//<-HERE**
[[NSUserDefaults standardUserDefaults]setObject:tagAsData forKey:myCellKey];
}
You are passing a primitive (non-object) value, of type NSInteger from variable matrixTag, to a method, archivedDataWithRootObject:, which expects an object reference value. That method happily tries to use the value (which is probably the integer 16, 0x10) as an object reference, and kaboom...
Your thinking looks correct, you know you cannot store non-object values in user defaults, and so you must wrap them as objects first. It is just your way of doing so that is wrong. What you need here is to create an instance of NSNumber from your integer. You could write:
NSNumber *tagAsNumber = [NSNumber numberWithInteger:matrixTag];
[[NSUserDefaults standardUserDefaults] setObject:tagAsNumber forKey:myCellKey];
However this pattern is common enough that a shortcut is provided:
[[NSUserDefaults standardUserDefaults] setInteger:matrixTag forKey:myCellKey];
and this will create the NSNumber object for you. There is also a corresponding integerForKey: method which will unwrap the integer for you when reading.
In my json file I have a title, subtitle, and url.
I sort the title to set the items alphabetically, but the url isn't sorted with the title and I don't know why.
This is what i've done:
NSDictionary *allDataDictionary = [NSJSONSerialization JSONObjectWithData:jsonData options:NSJSONReadingMutableContainers error:nil];
NSArray *arrayOfItems = [allDataDictionary objectForKey:#"items"];
for (NSDictionary *diction in arrayOfItems) {
NSString *titles = [diction objectForKey:#"title"];
NSString *station = [diction objectForKey:#"url"];
[jsonArray addObject:titles];
[jsonStations addObject:station];
// SORT JSON
NSArray *sortedArray;
sortedArray = [jsonArray sortedArrayUsingComparator:^NSComparisonResult(NSString *title1, NSString *title2)
{
if ([title1 compare:title2] > 0)
return NSOrderedDescending;
else
return NSOrderedAscending;
}];
[jsonArray setArray:sortedArray];
When I press the first item in the tableView, I get get the url from a total diffrent title.
What should I do to get the title to match the url in the tableView?
First of all this seems like a strange way of sorting, you should use a dictionary instead of 2 arrays otherwise things get messy very quickly.
Secondly you need to pass your sortedArray to the table instead of the jsonArray currently it seems to be just trying to reset the jsonArray.
I would create one method to handle it like this (I have stripped some of your sorting script to simplify this)
-(NSArray *)sortContentWithJSONData {
NSDictionary *allDataDictionary = [NSJSONSerialization JSONObjectWithData:jsonData options:NSJSONReadingMutableContainers error:nil];
NSArray *arrayOfItems = [allDataDictionary objectForKey:#"items"];
NSArray *sortedArray = [NSSortDescriptor sortDescriptorWithKey:#"title" ascending:false];
NSMutableArray *outputArray = [[NSMutableArray alloc] init];;
for (NSDictionary *diction in arrayOfItems) {
NSString *titles = [diction objectForKey:#"title"];
NSString *station = [diction objectForKey:#"url"];
[outputArray addObject:[NSDictionary dictionaryWithObjectsAndKeys:titles, #"title", station, #"station", nil]]
}
return [outputArray sortedArrayUsingDescriptors:[NSArray arrayWithObject:sortedArray]];
}
Then you could set a global array and access it in your table view using the following...
NSArray *tableContent = [self sortContentWithJSONData];
Hope that clears things up a bit :)
I am writing a dictionary application, and i am trying to import raw data from strings, one string per word. Amongst other things, the raw input strings contain the names of the parts of speech the corresponding word belongs to. In my datamodel I have a separate entity for Words and PartOfSpeech, and i want to create one entity of the type PartOfSpeech for each unique part of speech there may be in the input strings, and establish the relationships from the Words to the relevant pars of speech. The PartOfSpeech entity has just one Atribute, name, and one-to-many relationship to the Word:
My first implementation of getting unique PartOfSpeech entities involved caching them in a mutable array and filtering it each time with a predicate. It worked, but it was slow. I decided to speed it up a bit by caching the PartsOfSpeech in an NSDictionary, and now when i try and save the datastore after the import, i get the error "Cannot save objects with references outside of their own stores.". It looks like the problem is in the dictionary, but how can i solve it?
Here is the code that worked:
(in both sniplets managedObjectContext is an ivar, and processStringsInBackground: method runs on a background thread using performSelectorInBackground:withObject: method)
- (void) processStringsInBackground:(NSFetchRequest *)wordStringsReq {
NSError *err = NULL;
NSFetchRequest *req = [[NSFetchRequest alloc] init];
[req setEntity:[NSEntityDescription entityForName:#"PartOfSpeech" inManagedObjectContext:managedObjectContext]];
err = NULL;
NSMutableArray *selectedPartsOfSpeech = [[managedObjectContext executeFetchRequest:req error:&err] mutableCopy];
NSPredicate *p = [NSPredicate predicateWithFormat:#"name like[c] $name"];
// NSPredicate *formNamePredicate = [NSPredicate predicateWithFormat:<#(NSString *)predicateFormat#>]
...
for (int i = 0; i < count; i++){
...
currentPos = [self uniqueEntityWithName:#"PartOfSpeech" usingMutableArray:selectedPartsOfSpeech predicate:p andDictionary:[NSDictionary dictionaryWithObject:partOfSpeech forKey:#"name"]];
...
}
}
- (NSManagedObject *) uniqueEntityWithName:(NSString *) entityName usingMutableArray:(NSMutableArray *)objects predicate:(NSPredicate *)aPredicate andDictionary:(NSDictionary *) params {
NSPredicate *p = [aPredicate predicateWithSubstitutionVariables:params];
NSArray *filteredArray = [objects filteredArrayUsingPredicate:p];
if ([filteredArray count] > 0) {
return [filteredArray objectAtIndex:0];
}
NSManagedObject *newObject = [NSEntityDescription insertNewObjectForEntityForName:entityName inManagedObjectContext:managedObjectContext];
NSArray *dicKeys = [params allKeys];
for (NSString *key in dicKeys) {
[newObject willChangeValueForKey:key];
[newObject setPrimitiveValue:[params valueForKey:key] forKey:key];
[newObject didChangeValueForKey:key];
}
[objects addObject:newObject];
return newObject;
}
And here is the same, but with caching using NSMutableDictionary, which fails to save afterwards:
- (void) processStringsInBackground:(NSFetchRequest *)wordStringsReq {
NSError *err = NULL;
[req setEntity:[NSEntityDescription entityForName:#"PartOfSpeech" inManagedObjectContext:managedObjectContext]];
NSArray *selectedPartsOfSpeech = [managedObjectContext executeFetchRequest:req error:&err];
NSMutableDictionary *partsOfSpeechChache = [[NSMutableDictionary alloc] init];
for (PartOfSpeech *pos in selectedPartsOfSpeech) {
[partsOfSpeechChache setObject:pos forKey:pos.name];
}
...
for (int i = 0; i < count; i++){
...
currentPos = [self uniqueEntity:#"PartOfSpeech" withName:partOfSpeech usingDictionary:partsOfSpeechChache];
...
}
}
- (NSManagedObject *)uniqueEntity:(NSString *) entityName withName:(NSString *) name usingDictionary:(NSMutableDictionary *) dic {
NSManagedObject *pos = [dic objectForKey:name];
if (pos != nil) {
return pos;
}
NSManagedObject *newPos = [NSEntityDescription insertNewObjectForEntityForName:entityName inManagedObjectContext:managedObjectContext];
[newPos willChangeValueForKey:#"name"];
[newPos setPrimitiveValue:name forKey:#"name"];
[newPos didChangeValueForKey:#"name"];
[dic setObject:newPos forKey:name];
return newPos;
}
Could you help me to find the problem?
Best regards,
Timofey.
The error is caused by forming a relationship between managedObjects that don't share the same persistent store. You can do that by:
Creating a managed object with initialization without inserting it into a context.
Deleting a managed object from a context while retaining it in another object e.g. array, and then forming a relationship with it.
Accidentally creating two Core Data stacks so that you have two context and two stores.
Confusing configurations in a multi-store context.
I don't see any part of the code you provided that would trigger the problem.
It turns out, that it is wrong to pass NSManagedContext to a thread different from the one it was created in. Instead, one should pass the NSPersistenceStroreCoordinator to another thread, and create a new managed object context there. In order to merge the changes into the "main" context, one should save the other thread's context, receive the notification on the completion of the save on the main thread and merge the changes (see apple docs regarding Core Data and concurrency, can't give you the link, because i read it in Xcode). So here are the changes i made to my code to make it work (only posting the changed lines):
— (void) processStringsInBackground:(NSDictionary *) params {
NSFetchRequest *wordStringsReq = [params objectForKey:#"wordStringsReq"];
NSPersistentStoreCoordinator *coordinator = [params objectForKey:#"coordinator"];
NSManagedObjectContext *localContext = [[NSManagedObjectContext alloc] init];
[localContext setPersistentStoreCoordinator:coordinator];
(all the references to the managedObjectContext were replaced by localContext
And on the main thread, i call this method thusly:
.......
NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:req, #"wordStringsReq", persistentStoreCoordinator, #"coordinator", nil]; //the params i pass to the background method
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(handleNotification:) name:#"NSManagingContextDidSaveChangesNotification" object:nil]; //register to receive the notification from the save
[self performSelectorInBackground:#selector(processStringsInBackground:) withObject:dict];
}
- (void) handleNotification:(NSNotification *) notific {
NSLog(#"got notification, %#", [notific name]);
[managedObjectContext mergeChangesFromContextDidSaveNotification:notific];
}
Good luck!
Good answers, though a bit dated. The fine documentation notes that the main NSManagedObjectContext should never be used in worker threads. Instead, create a separate NSManagedObjectContext private to the worker using the "main" MOC as a parent, and then that instead. Here's the relevant "Concurrency" page from the Core Data Programming Guide:
https://developer.apple.com/library/prerelease/ios/documentation/Cocoa/Conceptual/CoreData/Concurrency.html
Snippet (Swift)
let jsonArray = … //JSON data to be imported into Core Data
let moc = … //Our primary context on the main queue
let privateMOC = NSManagedObjectContext(concurrencyType: .PrivateQueueConcurrencyType)
privateMOC.parentContext = moc
privateMOC.performBlock {
for jsonObject in jsonArray {
let mo = … //Managed object that matches the incoming JSON structure
//update MO with data from the dictionary
}
do {
try privateMOC.save()
} catch {
fatalError("Failure to save context: \(error)")
}
}
Help me please with the following problem:
- (NSDictionary *)getGamesList
{
NSMutableDictionary *gamesDictionary = [[NSMutableDictionary dictionary] retain];
// I was trying to change this on the commented code below, but did have no effect
// NSMutableDictionary *gamesDictionary = [[NSMutableDictionary alloc] init];
// [gamesDictionary retain];
while (sqlite3_step(statement) == SQLITE_ROW)
{
NSString *key = [NSString stringWithUTF8String:(char *)sqlite3_column_text(statement, 1)];
NSArray *gameDate = [key componentsSeparatedByString:#" "];
NSNumber *_id = [[NSNumber alloc] initWithInt:sqlite3_column_int(statement, 0)];
NSString *date_time = [NSString stringWithFormat:#"%#, %#",[gameDate objectAtIndex:0],[gameDate objectAtIndex:2]];
if (![gamesDictionary valueForKey:date_time]) [gamesDictionary setValue:[NSMutableArray array] forKey:date_time];
[[gamesDictionary valueForKey:date_time] addObject:[[_id copy] autorelease]];
[_id release];
}
sqlite3_reset(statement);
return gamesDictionary;
}
The leak starts in another method of another class, there the getGamesList method is called, like this:
NSMutableDictionary *gamesDictionary;
gamesDictionary = [[NSMutableDictionary dictionaryWithDictionary:[appDelegate getGamesList]] retain];
After that there are a lot of leaks that points to NSCFArray in the string:
NSArray *keys = [[NSArray arrayWithArray:[gamesDictionary allKeys]] retain];
in this method:
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
NSArray *keys = [[NSArray arrayWithArray:[gamesDictionary allKeys]] retain];
if ([keys count] != 0) return [[keys objectAtIndex:section] uppercaseString];
return #"";
}
I assume these things are connected to each other, but I still can not understand all of the memory management tips.
Thanks a lot!
Didn't use Cocoa for years (that's why I can't tell you an exact answer :/). But I guess your problem is that you systematically use retain on your objects.
Since the object reference count never get to 0, all dictionaries are keep in memory and not freed.
Try to remove the retain on [NSArray arrayWithArray] and [NSMutableDictionary dictionaryWithDictionary
http://en.wikibooks.org/wiki/Programming_Mac_OS_X_with_Cocoa_for_beginners/Some_Cocoa_essential_principles#Retain_and_Release
It does look like you are over-retaining your array.
When you create the gamesDictionary it is created with an retain count of +1. You then retain it (count becomes +2). When you get the value outside this function you retain again (count becomes +3).
You are correct that if you create an object you are responsible for it's memory management. Also, when you get an object from a method, you should retain it if you want to keep it around for longer than the span of the function. In your case, you just want to get at some of the properties of the object, so you don't need to retain it.
Here is a suggestion:
- (NSDictionary *)getGamesList
{
NSMutableDictionary *gamesDictionary = [NSMutableDictionary dictionary]; // Remove the retain.
while (sqlite3_step(statement) == SQLITE_ROW)
{
NSString *key = [NSString stringWithUTF8String:(char *)sqlite3_column_text(statement, 1)];
NSArray *gameDate = [key componentsSeparatedByString:#" "];
NSNumber *_id = [[NSNumber alloc] initWithInt:sqlite3_column_int(statement, 0)];
NSString *date_time = [NSString stringWithFormat:#"%#, %#",[gameDate objectAtIndex:0],[gameDate objectAtIndex:2]];
if (![gamesDictionary valueForKey:date_time]) [gamesDictionary setValue:[NSMutableArray array] forKey:date_time];
[[gamesDictionary valueForKey:date_time] addObject:[[_id copy] autorelease]];
[_id release];
}
sqlite3_reset(statement);
return gamesDictionary;
}
This next bit is messy. you create a new dictionary and retain it. The original dictionary is not autoreleased, so the count isn't decremented and it always hangs around. Just assign the dictionary rather than create a new one.
NSMutableDictionary *gamesDictionary = [[appDelegate getGamesList] retain];
// Retaining it, becuase it looks like it's used elsewhere.
Now, in this method:
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
NSString *returnString;
// Don't need to retain the keys because you are only using it within the function
// and since you didn't alloc, copy or retain the array it contains, you aren't responsible for it's memory management.
NSArray *keys = [NSArray arrayWithArray:[gamesDictionary allKeys]];
if ([keys count] != 0) {
returnString = [[NSString alloc] initWithString:[[keys objectAtIndex:section] uppercaseString]];
return [returnString autorelease];
}
return #"";
}