Detecting duplicate custom object contained in a NSMutable Array - ios

I've read every similar question, but have determined either I'm doing something stupid (possible) or I fail to grasp the NSArray method containsObject:
I'm trying to setup a UITableView that contains saved "favorites"; locations that are kept as a custom class called "MapAnnotations." This contains stuff like coordinates, title, an info field, and a couple of other parameters. I'm successfully saving/retrieving it from a NSUserDefaults instance, but can't seem to successfully detect duplicate objects held in my NSMutableArray.
Here's the relevant code:
-(void)doSetUp
{
//load up saved locations, if it exists
NSUserDefaults *myDefaults = [NSUserDefaults standardUserDefaults];
//if there are saved locations
if ([myDefaults objectForKey:#"savedLocations"]) {
NSLog(#"file exists!");
//get saved data and put in a temporary array
NSData *theData = [myDefaults dataForKey:#"savedLocations"];
//my custom object uses NSCode protocol
NSArray *temp = (NSArray *)[NSKeyedUnarchiver unarchiveObjectWithData:theData];
NSLog(#"temp contains:%#",temp);
//_myFavs currently exists as a NSMutableArray property
_myFavs = [temp mutableCopy];
}else{
NSLog(#"File doesn't exist");
_myFavs = [[NSMutableArray alloc]init];
}
//_currLoc is an instance of my Mapnnotations custom class
// which contains coordinates, title, info, etc.
if (_currLoc != nil) {
//if this is a duplicate of a saved location
if ([_myFavs containsObject:_currLoc]) {
//pop an alert
UIAlertView *alert = [[UIAlertView alloc]initWithTitle:#"Sorry..." message:#"That location has already been saved." delegate:nil cancelButtonTitle:#"OK" otherButtonTitles:nil, nil];
[alert show];
}else{
//add location to end of myFavs array
[_myFavs addObject:_currLoc];
NSLog(#"myFavs now contains:%#",_myFavs);
//write defaults
NSData *encodedObject = [NSKeyedArchiver archivedDataWithRootObject:_myFavs];
[myDefaults setObject:encodedObject forKey:#"savedLocations"];
[myDefaults synchronize];
}
}
}
I've tried enumerating through the _myFavs array, checking for matches on specific fields (get errors for enumerating through something mutable), tried to copy to a straight array... tried to use indexOfObject:..

You can use containsObject: method with custom objects that implement isEqual: method. Adding an implementation of this method to your Mapnnotations class will fix the problem:
// In the .h file:
#interface Mapnnotations : NSObject
-(BOOL)isEqual:(id)otherObj;
...
#end
// In the .m file:
#implementation Mapnnotations
-(BOOL)isEqual:(id)otherObj {
... // Check if other is Mapnnotations, and compare the other instance
// to this instance
Mapnnotations *other = (Mapnnotations*)otherObj;
// Instead of comparing unique identifiers, you could compare
// a combination of other custom properties of your objects:
return self.uniqueIdentifier == other.uniqueIdentifier;
}
#end
Note: when you implement your own isEqual: method, it is a good idea to implement the hash method as well. This would let you use the custom objects in hash sets and as NSDictionary keys.

Or you could use an NSOrderedSet (or mutable if needed) which is designed to do all your set membership functions, as well as having all your index type functions you'd expect from an NSArray.
You can transform it to array with - array when you need actual an NSArray version where an enumerable won't work.

Related

How to save data to NSMutableDictionary and load data from NSMutableDictionary

I am trying to create shopping cart. I want to put all data NSMutableDictionary from NSDictionary with Bar-code number which has DataItem variable as key. I have created NSMutableDictionary with that code block;
- (void)viewDidLoad {
[super viewDidLoad];
DataItem = [[[_response objectForKey:#"Data"] objectForKey:#"Items"] objectForKey:#"Item"];
NSMutableDictionary *CartItems = [[NSMutableDictionary alloc]init];
for(NSDictionary *transaction in DataItem)
{
[CartItems setValue:transaction forKey:[transaction objectForKey:#"BarcodeNumber"]];
}
}
When I push another viewController for selection another item to put cart. Once again,push that viewController, it deletes before item from CartItems variable.
I know I have putted that code block NSMutableDictionary *CartItems = [[NSMutableDictionary alloc]init]; inside of viewDidLoad. That's why, It generates new CartItems variable. If I want to put it .h file, it returns nil. I did not find any solution.
Is there any way to load data to NSMutableDictionary for reusing? or Is there another way to create same logic? Thanks.
use a singleton class to store you data like this tutorial
Make a singleton class for data which returns same object with first time initialization and use that for storing your data.
+ (id)getBE {
static MyBEClass *objMyBEClass = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
objMyBEClass = [[self alloc] init];
});
return objMyBEClass;
}
Make CarItems dictionary a property of BE class so you can get this from your view controller.
#property(strong)NSMutableDictionary *CartItems;
In init method of BE class initialize your array/dictionary.
CartItems = [[NSMutableDictionary alloc]init];
You can manage this by using NSUserDefaults. Here is viewDidLoad-
- (void)viewDidLoad {
DataItem = [[[_response objectForKey:#"Data"] objectForKey:#"Items"] objectForKey:#"Item"];
NSUserDefaults *prefrence=[[NSUserDefaults alloc]init];
if ([[NSUserDefaults standardUserDefaults]objectForKey:#"cartItems"]!=nil) {
NSMutableDictionary *CartItems=[[NSUserDefaults standardUserDefaults]objectForKey:#"cartItems"];
for(NSDictionary *transaction in DataItem)
{
[CartItems setValue:transaction forKey:[transaction objectForKey:#"BarcodeNumber"]];
}
[prefrence setObject:CartItems forKey:#"cartItems"];
}else{
NSMutableDictionary *CartItems = [[NSMutableDictionary alloc]init];
for(NSDictionary *transaction in DataItem)
{
[CartItems setValue:transaction forKey:[transaction objectForKey:#"BarcodeNumber"]];
}
[prefrence setObject:CartItems forKey:#"cartItems"];
}
}
whenever you want to clear your cart that time also clear NSUserDefaults using [[NSUserDefaults standardUserDefaults]removeObjectForKey:#"cartItems"].
I would not suggest to use a Singleton for such a thing except if your building a huge app with many controllers, and you need your chart everywhere.
If it's not the case, I would suggest you to pass the char as parameter from one controller to the other one, and pass it back with a delegate as in this guide.
if you have an application built with a single navigation controller, remember that it is not deallocated, so you can save data there. Just make getters and setters of your variable visible (.h file) or the variable itself, but remember that that is not a very good practise because it looks like it will be modified by many controllers, and thats another reason why you should avoid using singletons.
Hope it helps!

Load/Save NSUserDefaults Array - Bools?

So, I have an application that I'm building which includes a checklist of 100 items.
I wish to create an array of bools which indicate whether or not an item is "checked", then save it to the device using NSUserDefaults so it can be loaded each time the app is run.
I understand by reading around that you cannot store bool values in NSArrays so I opted of a mutable array which holds strings #"YES" and #"NO"... I'm almost certain this is a bad way of doing it but I'm out of ideas.
I create my array like this:
boolArrayOfCheckList = [[NSMutableArray alloc] initWithCapacity:100];
for (NSInteger i = 0; i < 100; i++){
[boolArrayOfCheckList addObject:[NSString stringWithFormat:#"NO"]];
}
When an item is checked using an IBAction, it updates the array at the index associated with the button (this appears to be adding to the array instead of replacing the object at said index :( ...):
[boolArrayOfCheckList insertObject:[NSString stringWithFormat:#"YES"] atIndex:0];
Then save the array like this:
[[NSUserDefaults standardUserDefaults] setObject:boolArrayOfCheckList forKey:#"myBoolArray"];
When I reload the application, the ViewdidLoad loads the key:
NSMutableArray *boolArrayOfCheckList = (NSMutableArray *)[[NSUserDefaults standardUserDefaults] objectForKey:#"myBoolArray"];
and comparing the element like this and loading the items check/uncheck accordingly:
if([[boolArrayOfCheckList objectAtIndex:0] isEqualToString:#"YES"]) ... //do this
But I'm getting some funny results when I add strings and I'm having trouble loading the data again.
There must be a better more efficient way of storing a checklist in an array that can be saved/updated accordingly. How can this be done?
Thank you.
Update
Just noticed I was inserting instead of replacing, so that explains the array getting bigger instead of replacing the object at a specific index. Now doing this:
[boolArrayOfCheckList replaceObjectAtIndex:1 withObject:[NSString stringWithFormat:#"YES"]];
You can use the NSNumber BOOL #YES and #NO instead of an NSString of #"YES" and #"NO".
NSDictionary *test = #{#"key" : #YES};
To get the value:
BOOL keyBoolValue = [test[#"key"] boolValue];
NSLog(#"keyBoolValue: %i", keyBoolValue);
if (keyBoolValue == YES) {
NSLog(#"true:");
}
else {
NSLog(#"false:");
}
or
if ([test[#"key"] boolValue] == YES) { ...

Checking If One Array Contains An Object of Another Array In Objective C

I am using below function to check whether if an object in an array is present in another array. If the object not present, then I will ADD that object to the new array, or else that object will NOT be included in the new array that I instantiated.
+ (NSMutableArray *)loadUngroupedSpeakerList
{
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
NSMutableArray *speakerList = [[NSMutableArray alloc] init];
NSArray *speakerIDList = [userDefaults objectForKey:DATA_SPEAKER_IDLIST];
NSArray *speakerIDListGrouped = [userDefaults objectForKey:DATA_SPEAKER_IDLIST_GROUPED];
//**** this is for checking the contents of speakerIDListGrouped ****//
for(NSString *speakerID in speakerIDListGrouped)
{
NSLog(#"FLOWCHECK~ loadUngroupedSpeakerList check content:%#", speakerID);
}
for(NSString *speakerID in speakerIDList)
{
if(![speakerIDListGrouped containsObject:speakerID])
{
NSLog(#"FLOWCHECK~ loadUngroupedSpeakerList: speakerID: %#", speakerID);
NSDictionary *speakerDict = [userDefaults dictionaryForKey:[NSString stringWithFormat:#"%#%#", DATA_SPEAKER_DICT, speakerID]];
[speakerList addObject:speakerDict];
}
}
return speakerList;
}
In the above code, speakerList contains all the speakerIDs. While speakerIDListGrouped only contains the speakerIDs that are used in a group. My function needs to eliminate all the speakerIDs used in a group so I did it in a way just like the above code.
My Problem:
When I run the code, I notice that even if speakerIDListGrouped contains the object in speakerIDList, these two lines would still be executed
NSDictionary *speakerDict = [userDefaults dictionaryForKey:[NSString stringWithFormat:#"%#%#", DATA_SPEAKER_DICT, speakerID]];
[speakerList addObject:speakerDict];
Whereas to I understand, It should not happen. Because I only allowed them to be executed only if speakerIDList does not contain that object.
This is the log when I execute the code:
2015-06-15 19:31:24.849 soulbeats[1936:433953] FLOWCHECK~ loadUngroupedSpeakerList check content:72243140485836704
2015-06-15 19:31:24.850 soulbeats[1936:433953] FLOWCHECK~ loadUngroupedSpeakerList check content:7782687177520836128
2015-06-15 19:31:24.850 soulbeats[1936:433953] FLOWCHECK~ loadUngroupedSpeakerList: speakerID: 72243140485836704
2015-06-15 19:31:24.851 soulbeats[1936:433953] FLOWCHECK~ loadUngroupedSpeakerList: speakerID: 7782687177520836128
As can be seen, speakerIDListGrouped DOES contain the two objects. However, when I tried replacing the string inside the lower for loop by hardcoding it to one of the objects I printed on Log, which was 72243140485836704. The function now works properly, I mean it didn't execute the two lines I showed before.
I am now confused. What is the difference between the string I hardcoded and the one that was obtained from the array? The contents are the same.
Many Thanks!
I did the same thing it is working fine...
NSMutableArray *speakerList = [[NSMutableArray alloc] init];
NSArray *speakerIDList = #[#"a",#"b",#"c",#"d",#"e"];
NSArray *speakerIDListGrouped =#[#"a",#"b",#"f",#"g",#"h"];
for(NSString *speakerID in speakerIDListGrouped)
{
NSLog(#"%#", speakerID);
}
for(NSString *speakerID in speakerIDList)
{
if(![speakerIDListGrouped containsObject:speakerID])
{
NSLog(#"FLOWCHECK~ loadUngroupedSpeakerList: speakerID: %#", speakerID);
[speakerList addObject:speakerID];
}
}
There might be some issue with the objects inside the array....
This answer will help other's. It's very simple, use following method of NSArray
id commonObject = [array1 firstObjectCommonWithArray:array2];
Ref: https://developer.apple.com/documentation/foundation/nsarray/1408825-firstobjectcommonwitharray?language=objc

Trying to iterate through an obj-c object properties and copy them to another object

I have a custom object saved to NSUserDefaults. I don't use Core Data as it is way to much for my one object. I am trying to create a method that takes a new object of this type and combine it with the data in a saved object of this type.
Here is what I have so far, but it is erroring out because some objects are of different types (BOOL, NSNumber, NSString, NSDictionary, etc..).
if (request) {
unsigned int numberOfProperties = 0;
objc_property_t *propertyArray = class_copyPropertyList(
[AmbulanceRequest class], &numberOfProperties);
for (NSUInteger i = 0; i < numberOfProperties; i++)
{
objc_property_t property = propertyArray[i];
NSString *name = [[NSString alloc] initWithUTF8String:property_getName(property)];
NSString *attributesString = [[NSString alloc] initWithUTF8String:
property_getAttributes(property)];
if( nil == [request valueForKey:name]) { <--- error here BOOL cannot be nil
[request setValue:[savedRequest valueForKey:name] forKey:name];
}
}
}
Error I am getting is: [NSKeyedUnarchiver initForReadingWithData:]: data is NULL
UDPATE:
I am just using decoder and encoder. When encoding and decoding in general I never have issues. Just in the code above. For example, here is one property that is throwing the error.
Of course these are in the appropriate encodeWithCoder and initWithCoder methods.
#property (nonatomic, assign) BOOL * trip;
self.roundTrip = [decoder decodeBoolForKey:#"trip"];
[encoder encodeBool:self.roundTrip forKey:#"trip"];
//// UPDATE 2: //////
Here is the full code of my method. As you can see the object is pulled back and decoded way before any of this iterating through the properties happens. I have two BooRequests the old one and a new one. I am trying to take the properties of the old one and save them to the new one if the new one doesn't have those same properties.
+(void) saveRequest:(BooRequest*)request {
// If current request then we need to save off the unSetPings
BooRequest *savedRequest = [NSKeyedUnarchiver unarchiveObjectWithData:[[NSUserDefaults standardUserDefaults]
objectForKey:kCurrentActiveRequest]];
if(request){
unsigned int numberOfProperties = 0;
objc_property_t *propertyArray = class_copyPropertyList([BooRequest class], &numberOfProperties);
#warning YO!!! this FOR LOOP is *NOT* done and probably doesn't even work!!!
for (NSUInteger i = 0; i < numberOfProperties; i++)
{
objc_property_t property = propertyArray[i];
NSString *name = [[NSString alloc] initWithUTF8String:property_getName(property)];
NSString *attributesString = [[NSString alloc] initWithUTF8String:property_getAttributes(property)];
if ([[[NSUserDefaults standardUserDefaults] dictionaryRepresentation].allKeys containsObject:#"keyForNonMandatoryObject"]) {
id thing = [request valueForKey:name];
if(NULL == [request valueForKey:name] || nil == thing){
[request setValue:[savedRequest valueForKey:name] forKey:name];
}
}
if(savedRequest) {
request.unSentPings = [[NSMutableArray alloc] initWithArray:savedRequest.unSentPings];
}
request.nextButtonToShowIndex = savedRequest.nextButtonToShowIndex;
} // end for loop
} else {
STPingSender *sender = [[STPingSender alloc] init];
BooRequest *req = [BooRequest currentRequest];
if(req && req.unSentPings && [req.unSentPings count] > 0){
[sender sendToServer:((BooRequest*)[BooRequest currentRequest]).unSentPings];
}
}
[[NSUserDefaults standardUserDefaults] setObject:[NSKeyedArchiver archivedDataWithRootObject:request]
forKey:kCurrentActiveRequest];
[[NSUserDefaults standardUserDefaults] synchronize];
}
Here is my ultimate goal I have a BooRequest that is saved locally from the server. I make changes to it locally. 90% of the time their are not updates from the server which I need to update locally. But when there are I need the new local data merged with the new server data. But the local data I add is only used locally, the server never uses this additional data. It only needs to be merged locally. The reason I am iterating is that I don't want to have to update this method everytime this Objective-c class gets a new property added to it. I want some dynamic code that just says iterate through the properties and when you find the properties that match on the old object and new object, copy the old object properties into the new ones.
id dataObject = [[NSUserDefaults standardUserDefaults] objectForKey:kCurrentActiveRequest];
BooRequest *savedRequest = [NSKeyedUnarchiver unarchiveObjectWithData:dataObject];
when dataObject is nil, which means there is no such key in user defaults, the second line will emit the [NSKeyedUnarchiver initForReadingWithData:]: data is NULL error, in this case, you should not unarchive it, if the new BooRequest object is not nil, you can save the new object to user default directly.
id thing = [request valueForKey:name];
For properties that are of primitive type such as BOOL or NSInteger, the thing is NO and 0 respectively when you don't set the value of the properties, which means the if(NULL == [request valueForKey:name] || nil == thing) check is always false and the new request object is not updated from the old one. I suggest you to change those properties to NSObject type, so the check will pass.
#property (nonatomic, strong) NSNumber* boolProperty;
#property (nonatomic, strong) NSNumber *integerProperty;
Hope that helps.
I'm not sure why you are trying to store a custom object in NSUserDefaults in the first place... that is usually more pain than it is worth.
Why not just create an NSDictionary and save that to NSUserDefaults? Then your BooRequest class can init from dictionary as well as merge from dictionary. You can also create a method -saveToUserDefaults method in BooRequest that saves it back when completed. And depending on what BooRequest actually does you may not need a custom subclass if all you are doing is storing some key-value pairs.
It would be helpful to see what your local data looks like, and what your remote data looks like, and then the desired outcome.
You shouldn't blindly iterate through all the object properties but choose those worth being saved.
As said in another answer, don't use NSUserDefaults for this. If you don't want to use Core Data I recommend serializing Property Lists. As a bonus you'll be able to inspect/manually edit your object values.

Issue with initWithCoder: and NSKeyedUnarchiver

I'm currently attempting to add a game-saving feature to my app. The end goal is to get ~300 custom objects saved to a .plist file and extract them again later. I've made some headway, but I'm having some issues with initWithCoder: and I'm also unsure about my technique.
The following code is being used by a UIViewController to save the objects (I only added 2 objects to the dictionary as an example):
//Saves the contents to a file using an NSMutableDictionary
-(IBAction)saveContents {
//Create the dictionary
NSMutableDictionary *dataToSave = [NSMutableDictionary new];
//Add objects to the dictionary
[dataToSave setObject:label.text forKey:#"label.text"];
[dataToSave setObject:territory forKey:#"territory"];
[dataToSave setObject:territory2 forKey:#"territory2"];
//Archive the dictionary and its contents and set a BOOL to indicate if it succeeds
BOOL success = [NSKeyedArchiver archiveRootObject:dataToSave toFile:[self gameSaveFilePath]];
//Handle success/failure here...
//Remove and free the dictionary
[dataToSave removeAllObjects]; dataToSave = nil;
}
This successfully calls the encodeWithCoder: function in the Territory class twice:
-(void)encodeWithCoder:(NSCoder *)aCoder {
NSLog(#"ENCODING");
[aCoder encodeObject:self.data forKey:#"data"];
[aCoder encodeInt:self.MMHValue forKey:#"MMHValue"];
[aCoder encodeObject:self.territoryName forKey:#"territoryName"];
//Continue with other objects
}
When I look in the directory, sure enough, the file exists. Now, here's the issue I'm having: When the following function is run, the initWithCoder: function is successfully called twice as it should be. The logs inside the initWithCoder: function output what they should, but the logs in the UIViewController's loadContents function return 0, null, etc. However, the label's text is set correctly.
//Loads the contents from a file using an NSDictionary
-(IBAction)loadContents {
//Create a dictionary to load saved data into then load the saved data into the dictionary
NSDictionary *savedData = [NSKeyedUnarchiver unarchiveObjectWithFile:[self gameSaveFilePath]];
//Load objects using the dictionary's data
label.text = [savedData objectForKey:#"label.text"];
NSLog(#"%i, %i", [territory intAtKey:#"Key4"], [territory2 intAtKey:#"Key4"]);
NSLog(#"MMH: %i, %i", territory.MMHValue, territory2.MMHValue);
NSLog(#"NAME: %#, %#", territory.territoryName, territory2.territoryName);
//Free the dictionary
savedData = nil;
}
- (id)initWithCoder:(NSCoder *)aDecoder {
if (self = [super initWithCoder:aDecoder]) {
NSLog(#"DECODING TERRITORY");
self.data = [aDecoder decodeObjectForKey:#"data"];
self.MMHValue = [aDecoder decodeIntForKey:#"MMHValue"];
self.territoryName = [[aDecoder decodeObjectForKey:#"territoryName"] copy];
//Continue with other objects
}
NSLog(#"DECODED INT: %i", self.MMHValue);
NSLog(#"DECODED NAME: %#", self.territoryName);
return self;
}
I've been trying to get this to work for hours, but to no avail. If anyone has any insights to this, please help me out. Also, I'm not entirely sure if my technique for saving is good or not (using an NSMutableDictionary to store references to the objects so it can output to one file)? Thanks!
I read through Apple's documentation on NSKeyedArchiver and NSKeyedUnarchiver, along with a bunch of blog posts on the subject, and I realized what I was doing wrong. After encoding the objects to a file (by adding them to a dictionary using keys and then saving them to a .plist), when I tried decoding them, I never actually extracted the values from the new dictionary (the dictionary holding the contents of the file that was just decoded). This also explains why the label's text was loaded, but territory and territory2 weren't. I changed my loadContents function to the following, and now the code works:
-(IBAction)loadContents {
//Create a dictionary to load saved data into then load the saved data into the dictionary
NSLog(#"***** STARTING DECODE *****");
NSDictionary *savedData = [NSKeyedUnarchiver unarchiveObjectWithFile:[self gameSaveFilePath]];
NSLog(#"***** CALLING initWithCoder: ON OBJECTS *****");
NSLog(#"***** FINISHED DECODE *****");
//Load objects using the dictionary's data
label.text = [savedData objectForKey:#"label.text"];
territory = [savedData objectForKey:#"territory"];
territory2 = [savedData objectForKey:#"territory2"];
//Free the dictionary
savedData = nil;
}

Resources