indexOfObjectIdenticalTo: returns value outside of array count - objective-c

I'm currently working on an app that populates a UITableView with items from a MPMediaItemCollection. I'm trying to add a UITableViewCellAccessoryCheckmark to the row that matches the title of the currently playing track.
I've done so by creating a mutable array of the track titles, which are also set for my cell's textLabel.text property. (for comparison purposes)
Note: This is all done in - (UITableViewCell *) tableView: (UITableView *) tableView cellForRowAtIndexPath: (NSIndexPath *) indexPath
MPMediaItem *mediaItem = (MPMediaItem *)[collectionMutableCopy objectAtIndex: row];
if (mediaItem) {
cell.textLabel.text = [mediaItem valueForProperty:MPMediaItemPropertyTitle];
}
[mutArray insertObject:cell.textLabel.text atIndex:indexPath.row];
To the best of my knowledge this all works fine except for the below. At this point, I am trying to get the index of the currently playing tracks title and add the UITableViewCellAccessoryCheckmark to that row.
if (indexPath.row == [mutArray indexOfObjectIdenticalTo:[mainViewController.musicPlayer.nowPlayingItem valueForProperty:MPMediaItemPropertyTitle]]) {
cell.accessoryType = UITableViewCellAccessoryCheckmark;
}
Getting to my question, I added all of the above (mostly irrelevant) code because I'm stumped on where I went wrong. When I log indexOfObjectIdenticalTo: it spits out "2147483647" every time, even though there are never more than 5 objects in the array. But why?
If anyone has any tips or pointers to help me fix this it would be greatly appreciated!

2147483647 just mean the object is not found.
From the documentation of -[NSArray indexOfObjectIdenticalTo:]:
Return Value
The lowest index whose corresponding array value is identical to anObject. If none of the objects in the array is identical to anObject, returns NSNotFound.
and NSNotFound is defined as:
enum {
NSNotFound = NSIntegerMax
};
and 2147483647 = 0x7fffffff is the maximum integer on 32-bit iOS.
Please note that even if two NSString have the same content, they may not be the identical object. Two objects are identical if they share the same location, e.g.
NSString* a = #"foo";
NSString* b = a;
NSString* c = [a copy];
assert([a isEqual:b]); // a and b are equal.
assert([a isEqual:c]); // a and c are equal.
assert(a == b); // a and b are identical.
assert(a != c); // a and c are *not* identical.
I believe you just want equality test instead of identity test, i.e.
if (indexPath.row == [mutArray indexOfObject:[....]]) {

Looking at the docs for NSArray
Return Value
The lowest index whose corresponding array value is identical to anObject. If none of the objects in the array is identical to anObject, returns NSNotFound.
So you should probably do a check
NSInteger index = [array indexOfObjectIdenticalTo:otherObject];
if (NSNotFound == index) {
// ... handle not being in array
} else {
// ... do your normal stuff
}

Related

How to find if an object of a class with same data already exists in a NSMutableArray?

I apologize for this basic question, but I am 2-month new to obj-c.
Problem:
I am not able to find if an object with same data already exists in the NSMutableArray.
What I am doing?
ScanDigInfoForTable* sfile = [[ScanDigInfoForTable alloc]init];
sfile.data = "myData";
int inde = [_DataList indexOfObject:sfile] ;
if(inde == -1)
[_DataList addObject:sfile];
ScanDigInfoForTable* sfile2 = [[ScanDigInfoForTable alloc]init];
sfile2.data = "myData";
inde = [_DataList indexOfObject:sfile2] ;
if(inde == -1)
[_DataList addObject:sfile2];
Issue:
The _DataList get 2 objects instead of 1. Many thanks in advance for your attention.
S.P: I already know that I may traverse the whole array in a loop in order to check the data already exists. Looking for a better solution as the array may have thousands of records.
Well, comparing two custom objects is really not that simple for the simple fact there is no defined way to declare equality. It is individual choice to define the rules for equality for the objects they are creating.
In your case, it would be two step process:
Step 1: Implement isEqual: in your ScanDigInfoForTable class. Assuming ScanDigInfoForTable is a model class and that it has three string properties - code, data & itemID (you can have any type).
- (BOOL)isEqual:(ScanDigInfoForTable *)other {
return [self.code isEqualToString:other.code] && [self.data isEqualToString:other.data] && [self.itemID isEqualToString:other.itemID];
}
Step 2: Call containsObject: method on NSMutableArray. This method would internally call isEqual: to give you the results based on the rules you defined.
// If the object does not exist in the list, we add it
if (![_DataList containsObject:sfile2]) {
[_DataList addObject:sfile2];
}
In Objective-C object equality is determined by the methods -isEqual: and -hash.
When testing object membership in a collection the items of the collection are sent isEqual:. The default implementation only compares the addresses of objects, which is why you are seeing duplicates. Your objects do no provide their own implementation of equality based on the data they contain.
To fix this you can override isEqual: to compare objects based on the data they represent. Using your example in your question, this could just be:
- (BOOL) isEqual:(id)object {
BOOL result = N0;
if (object != self){
if ([object isKindOfClass:[self class]]){
result = [[self data] isEqual:[(ScanDigInfoForTable *)object data]];
}
} else {
result = YES;
}
return result;
}
Mike Ash has a great article about implementing equality. In general, if you are implementing a custom class you should make equality a part of that.
You can user filteredArrayUsingPredicate for example
NSArray * matches = [_DataList filteredArrayUsingPredicate:
[NSPredicate predicateWithFormat:#"data == %# ",sfile2.data]];
if(matches.count == 0) {
[_DataList addObject:sfile2];
}
Something like this?
NSMutableSet* set1 = [NSMutableSet setWithArray:array1];
NSMutableSet* set2 = [NSMutableSet setWithArray:array2];
[set1 intersectSet:set2]; //this will give you only the obejcts that are in both sets
NSArray* result = [set1 allObjects];
This has the benefit of not looking up the objects in the array, while looping through another array, which has N^2 complexity.
and also set2 doesn't have to be mutable, might as well use just
NSSet* set2 = [NSSet setWithArray:array2];

Adding items from 1 array 2 another filtered by dictionary key

I've been trying to logically think about this for a while now and usually I can solve it by writing it out or thinking about it while doing other stuff not associated with programming. However everything I try isn't working. So basically I have 2 NSArrays, which are both populated from 2 different plists. Each array is filled with all dictionaries: all have the same keys, and all have the same identical structure, but some may have information associated with a key that some don't. I basically need to check each item in array1 and see if it exists in array2 and if it does NOT exist in array2 then add it to array1 (or array3 seeing how you can't mutate an array while iterating through it). I need it to see if it exists by a specific key "name_key".
So... In short my end result needs to be an NSArray with all objects from both arrays without having objects with duplicate names (from the dictionaries name_key).
Below is my actual code that doesn't work.
IN CODE BELOW: originalWhiskiesListArray = array1
newWhiskiesListArray = array2
combinedWhiskiesListArray = array3 (because you can't mutate an array while iterating through it.
BOOL whiskyExists = YES;
for (NSDictionary *newWhisky in newWhiskiesListArray) {
for (NSDictionary *originalWhisky in originalWhiskiesListArray) {
NSString * newWhiskyNameString = [[newWhisky objectForKey:NAME_KEY] lowercaseString];
NSString * origWhiskyNameString = [[originalWhisky objectForKey:NAME_KEY] lowercaseString];
//Compare lowercase strings and if they don't match then add them to the original plist array.
if ([newWhiskyNameString isEqualToString:origWhiskyNameString]) {
whiskyExists = YES;
break;
} else {
whiskyExists = NO;
break;
//NSLog(#"Whisky names do not match. New Whisky: %# Old Whisky: %#",[newWhisky objectForKey:NAME_KEY],[originalWhisky objectForKey:NAME_KEY]);
//doesn't match so add it
}
}
if (whiskyExists == NO) {
[combinedWhiskiesListArray addObject:newWhisky];
NSLog(#"newWhisky added");
whiskyExists = YES;
}
}
Can either of the whiskey name strings be nil? If so then this breaks the isEqualToString comparison because you can always message nil which returns NO.
Also I believe that the breaks are wrong. You only need to exit the inner loop in case you encounter a match. If not you have to keep going until the end of the inner loop.
If I'm understanding you correctly, you can just add all the values from each dictionary to an NSMutableSet, which won't add an item if it already exists in the set. You can then convert the set back to an array with the NSSet method allObjects.

Finding the index of an object in an array using one of that object's properties

I have a NSMutableArray pages with several objects (instances of Page). Each Page instance has a UUID and other properties, such as contents. When a page changes, everything may change except the UUID. After a change, I would like to find out the index of a particular Page in pages by using indexOfObject:. I use the UUID to identify the index I'm looking for and this works just fine:
NSMutableArray *uuids = [[NSMutableArray alloc] init];
for (int i = 0; i < [self.pages count]; i++) {
[uuids addObject:[[pages objectAtIndex:i] uuid]];
}
NSUInteger indexOfPage = [uuids indexOfObject:page.uuid];
Now my question is if there is a more elegant way to do this. All I really need is the index of the Page object in pages that has a given UUID.
There is
- (NSIndexSet *)indexesOfObjectsPassingTest:(BOOL (^)(id obj, NSUInteger idx, BOOL *stop))predicate or indexOfAccessibilityElement:<#(id)#> but I'm not really sure how either of these could help me out with my problem.
Subclass the isEqual: method in Page as follows:
- (BOOL) isEqual: (Page *) otherPage
{
if (![otherPage isKindOfClass:[Page class]]) {
return NO;
}
return [self.UUID isEqualToString: otherPage.UUID];
}
Then indexOfObject: will work correctly.
From Apple's NSArray documentation:
Starting at index 0, each element of the array is sent an isEqual:
message until a match is found or the end of the array is reached.
This method passes the anObject parameter to each isEqual: message.
Objects are considered equal if isEqual: (declared in the NSObject
protocol) returns YES.
From NSArray docs:
each element of the array is sent an isEqual: message until a match is found or the end of the array is reached.
So you can override isEqual: so that it returns YES if the two pages has the same UUID.
- (BOOL)isEqual:(id)object {
if (![object isKindOfClass:[Page class]]) {
return NO;
}
Page *otherPage = object;
return [self.uuid isEqualToString:otherPage.uuid];
}
May be this way by using a predicate, assuming pages is your array of all pages:
predicate = [NSPredicate predicateWithFormat:[NSString stringWithFormat:#"(UUID == %#)", searchUUID]];
filteredArray = [pages filteredArrayUsingPredicate:predicate];
And once you have the object, which should be at objectAtIndex:0 in the filtered array assuming your uuid is unique, you could ask the index of that object:
NSUInteger indexOfPage = [pages indexOfObject:theObject];

How to perform binary search on NSArray?

What is the simplest way to do a binary search on an (already) sorted NSArray?
Some potential ways I have spotted so far include:
The use of CFArrayBSearchValues (mentioned here) - would this work on an NSArray?
The method indexOfObject:inSortedRange:options:usingComparator: of NSArray assumes the array is sorted and takes an opts param of type NSBinarySearchingOptions - does this mean it performs a binary search? The docs just say:
Returns the index, within a specified range, of an object compared with elements in the array using a given NSComparator block.
Write my own binary search method (something along the lines of this).
I should add that I am programming for iOS 4.3+
Thanks in advance.
The second option is definitely the simplest. Ole Begemann has a blog entry on how to use the NSArray's indexOfObject:inSortedRange:options:usingComparator: method:
NSArray *sortedArray = ... // must be sorted
id searchObject = ...
NSRange searchRange = NSMakeRange(0, [sortedArray count]);
NSUInteger findIndex = [sortedArray indexOfObject:searchObject
inSortedRange:searchRange
options:NSBinarySearchingFirstEqual
usingComparator:^(id obj1, id obj2)
{
return [obj1 compare:obj2];
}];
See NSArray Binary Search
1 and 2 will both work. #2 is probably easier; it certainly doesn't make sense for that method to do anything other than a binary search (if the range is above a certain size, say). You could verify on a large array that it only does a small number of comparisons.
I'm surprised that nobody mentioned the use of NSSet, which [when it contains objects with a decent hash, such as most Foundation data types] performs constant time lookups. Instead of adding your objects to an array, add then to a set instead (or add them to both if you need to retain a sorted order for other purposes [or alternatively on iOS 5.0 or Mac OS X 10.7 there is NSOrderedSet]).
To determine whether an object exists in a set:
NSSet *mySet = [NSSet setWithArray:myArray]; // try to do this step only once
if ([mySet containsObject:someObject])
{
// do something
}
Alternatively:
NSSet *mySet = [NSSet setWithArray:myArray]; // try and do this step only once
id obj = [mySet member:someObject];
// obj is now set to nil if the object doesn't exist or it is
// set to an object that "isEqual:" to someObject (which could be
// someObject itself).
It is important to know that you will lose any performance benefit if you convert the array to a set each time you do a lookup, ideally you will be using a preconstructed set containing the objects you want to test.
//Method to pass array and number we are searching for.
- (void)binarySearch:(NSArray *)array numberToEnter:(NSNumber *)key{
NSUInteger minIndex = 0;
NSUInteger maxIndex = array.count-1;
NSUInteger midIndex = array.count/2;
NSNumber *minIndexValue = array[minIndex];
NSNumber *midIndexValue = array[midIndex];
NSNumber *maxIndexValue = array[maxIndex];
//Check to make sure array is within bounds
if (key > maxIndexValue || key < minIndexValue) {
NSLog(#"Key is not within Range");
return;
}
NSLog(#"Mid indexValue is %#", midIndexValue);
//If key is less than the middleIndexValue then sliceUpArray and recursively call method again
if (key < midIndexValue){
NSArray *slicedArray = [array subarrayWithRange:NSMakeRange(minIndex, array.count/2)];
NSLog(#"Sliced array is %#", slicedArray);
[self binarySearch:slicedArray numberToEnter:key];
//If key is greater than the middleIndexValue then sliceUpArray and recursively call method again
} else if (key > midIndexValue) {
NSArray *slicedArray = [array subarrayWithRange:NSMakeRange(midIndex+1, array.count/2)];
NSLog(#"Sliced array is %#", slicedArray);
[self binarySearch:slicedArray numberToEnter:key];
} else {
//Else number was found
NSLog(#"Number found");
}
}
//Call Method
#interface ViewController ()
#property(nonatomic)NSArray *searchArray;
#end
- (void)viewDidLoad {
[super viewDidLoad];
//Initialize the array with 10 values
self.searchArray = #[#1,#2,#3,#4,#5,#6,#7,#8,#9,#10];
//Call Method and search for any number
[self binarySearch:self.searchArray numberToEnter:#5];
// Do any additional setup after loading the view, typically from a nib.
}
CFArrayBSearchValues should work—NSArray * is toll-free bridged with CFArrayRef.

iOS indexOfObjectIdenticalTo: issue

I have two arrays, each containing strings. The first array is a list of words, the second array contains alternatives to those words in different languages.
The arrays are matched such that the word at index n in the second array is a translation of the word at index n in the first array.
The words and their translations are displayed in a table view. The user can filter the table view by entering text in a search field. When this is done, I create a filtered array from the first array like this:
- (void)filterContentForSearchText:(NSString*)searchText
[self.filteredarray removeAllObjects];
[firstarray enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop)
{
if ([obj compare:searchText options:NSCaseInsensitiveSearch range:NSMakeRange(0, [searchText length])] == NSOrderedSame)
{
idx= [firstarray indexOfObjectIdenticalTo:obj];
NSUInteger maxindex = idx + 50;
for (idx ; (idx < [firstarray count] && idx <= maxindex && idx!= NSNotFound); idx ++)
{
[self.filteredarray addObject:[firstarray objectAtIndex: idx]];
}
*stop = YES;
}
}];
Then, when I am displaying the values in my table view, I use the following code. This is an exerpt from my cellForRowAtIndexPath method. I am trying to get the index from the original array using the object that has been added to the filtered array.
contentForThisRow = [self.filteredarray objectAtIndex:row];
NSUInteger index = [self.firstarray indexOfObjectIdenticalTo:contentForThisRow];
contentForThisRow2 = [self.secondarray objectAtIndex:index];
This works on the simulator, but on the device I will sometimes get repeats of the same entry from the second array. For example, my first array contains the word "hello" three consecutive times, at indexes x, y and z. My second array contains "hei", "heisan" and "hoppsan", which are all translations of "hello", at indexes x, y and z.
On the simulator, I get three cells, each with a different translation. On the device, I get three cells, all with "hei", the first translation. This does not happen for all repeated translations.
Why is this happening, and how can I get around it?
I think the problem is that iOS (on the device) may be using a slightly different optimisation to the emulator somewhere, either in NSString or NSArray. That is a guess.
indexOfObjectIdenticalTo: returns the index of the first object that has the same memory address as the object you are passing in. On the phone it appears to have re-used the identical string objects in your first array when building the filtered array (possibly even when building firstArray), so you are getting the same index value back each time.
A better solution would be to build your filtered array as an array of dictionaries, storing the values from the correct indexes of firstArray and secondArray at that point. You can then use these values directly when populating the cell instead of searching through both arrays again. This should also have some performance benefits.
You would achieve this using the following code. First, inside your loop when you are building the filtered array, instead of adding the object from firstarray, do this:
[self.filteredArray addObject:[NSDictionary dictionaryWithObjectsAndKeys:[firstarray objectAtIndex:idx],#"english",[secondarray objectAtIndex:idx],#"translated",nil];
Then, in your cellForRowAtIndexPath, to get your two content variables:
NSDictionary *rowData = [self.filteredarray objectAtIndex:row];
contentForThisRow = [rowData objectForKey:#"english"];
contentForThisRow2 = [rowData objectForKey:#"translated"];
An even better solution would be to hold your data like this in the first place, and not try to keep two separate arrays synchronised. I imagine if you want to add or alter anything in your two separate files you could quickly get them out of step. However, I feel I've done enough for the day...
else
contentForThisRow = [self.firstarray objectAtIndex:row];
contentForThisRow2 = [self.secondarray objectAtIndex:row];
You see anything wrong with that?