NSDictionary extend valueForKey method - ios

Can I create category that will extend - (id)valueForKey:(NSString *)key method and if the value of this key will be equal [NSNull null] it will return me empty string #"".
It is connected to nil values for some keys that I get from the backend server in JSON.
So if I need to save some value to the property it can be nil.
Suppose I use this code with value for key nil
artist.name = ad[#"artist"][#"name"]; // nil value on the right.
so if I will have extension it will check value for key and return me #"" intend of nil. Does it make sense.
Or do I need extend NSManagedObject for checking attribute type and then return corresponded value #"", 0 or nil, or 0.0f if the attribute has another not string type.

Rather than defining a category on NSDictionary, you could define a category on NSObject, and do the conversion there:
#interface NSObject (DBNullHandling)
-(NSString*)nullToEmpty;
#end
#implementation NSObject (DBNullHandling)
-(id)nullToEmpty {
return (self == [NSNull null]) ? #"" : self;
}
#end
Now you can do this:
artist.name = [ad[#"artist"][#"name"] nullToEmpty];
If ad[#"artist"][#"name"] returns [NSNull null], artist.name gets assigned #""; otherwise, it gets assigned the value returned by ad[#"artist"][#"name"].

What I do is check before performing the assignment. The tripartite operator is a good shorthand way to do this. You have:
artist.name = ad[#"artist"][#"name"];
Instead, I would write
id val = ad[#"artist"][#"name"];
artist.name = val ? val : #""; // or val ?: #""
(I have not completely understood the question, so it might be necessary to break that check down into more stages or modify it in some way to check for [NSNull null] instead of nil. But you get the idea.)

Instead of subclassing NSDictionary to change the implementation of valueForKey:, I suggest you check if the key exists. So you would instead do this:
BOOL keyExistsInDictionary = [[myDict allKeys] containsObject:key];
id objectForKey = [myDict objectForKey:key];
if (nil == objectForKey && keyExistsInDictionary) objectForKey = #"";
If objectForKey: returns nil, you can check keyExistsInDictionary to see if the key is there and simply contains [NSNull null]. This lets you differentiate between a key that exists and a key that doesn't without modifying NSDictionary.

Can I create category that will extend - (id)valueForKey:(NSString *)key method
The short answer here is no, don't do that. Categories let you extend classes by adding method, but you shouldn't use them to override existing methods. The main reason for this is that it's unreliable -- the order in which the methods in categories are added to a class isn't defined, which means that if you have two categories that override the same method, you'd have no idea which version of the method the class would ultimately end up with.
Another reason is that you have no way to refer to the original method from the overriding method, so you'd have to re-implement the behavior that you want to keep.
Finally, a category changes the class, so every instance of that class will have the new behavior. That's not usually a problem when you're adding methods -- code that doesn't use the new methods remains unaffected by the new functionality. But when you change an existing method, particularly for a class that's as widely used as NSDictionary, it's very likely that you'll break a lot of existing code.
if the value of this key will be equal [NSNull null] it will return me empty string #""
You've gotten some good answers already, but since you asked about categories, here's an approach that solves the problem using a category appropriately. Instead of trying to change -valueForKey:, use a category to add a new method called -stringForKey: that returns a string. It sounds like you're working exclusively with strings, so this seems like a reasonable solution. The implementation can call -valueForKey: and transform non-string objects into strings:
#interface NSDictionary (StringValue)
- (NSString *)stringForKey:(NSString*)key;
#end
#implementation NSDictionary (StringValue)
- (NSString *)stringForKey:(NSString*)key
{
id value = [self valueForKey:key];
NSString *string;
if ([value isKindOfClass:[NSString class]]) {
string = (NSString*)value;
}
else if ([value isKindOfClass:[NSNull class]]) {
string = #"";
}
else {
string = [value description];
}
return string;
}
#end

Related

KVC - key value coding with dot seperation - Exception not KVC compliant

#interface MyClass: NSObject
#property NSArray *arr;
#end
#inplementation MyClass
- (instancetype) init
{
if(self = [super init])
{
self.arr = [[NSArray alloc] init];
}
return self;
}
#end
int main(int argc, char *argv[])
{
MyClass *temp = [[MyClass alloc] init];
[temp valueForKey:#"arr.count"]; //count is ivar of NSArray
return 0;
}
then console says
NSExceptions: [MyClass valueForUnfinedKey:] this class is not key
value-complaint for the key arr.count
Everytime I use dot seperations, this exceptions come out.
I tried to search web and read menu, I still don't know why, could anyone help? Thanks.
The method valueForKey: takes a single key (property or local variable) name, it does not take a key path such as your arr.count.
The method valueForKeyPath: does take a key path, it effectively is a sequence of valueForKey: calls. See Getting Attribute Values Using Keys in About Key-Value Coding.
However you example will still not work due to the way valueForKey: is defined for NSArray:
Returns an array containing the results of invoking valueForKey: using key on each of the array's objects.
So in your case if you try valueForKeyPath:#"arr.count", the arr part of the path will return your array and then NSArray's valueForKey: will attempt to get the count key for each element of the array and not for the array itself. Not what you want...
Which brings us to Collection Operators which provide key Paths which do operate on the collection, array in your case, and not its elements. The collection operator you need is #count giving you the key path arr.#count, so you need to call:
[temp valueForKeyPath:#"arr.#count"]
Unless this is an exercise in learning about KVC this can be shortened to:
temp.arr.count
which doesn’t have the issue of trying to apply count to the array’s elements, and returns an NSUInteger value rather than an NSNumber instance.
HTH
It's because arr.count is not key value-complaint of MyClass. When program runs, it cann't find any property of MyClass name arr.count.
valueForKeyPath: - Returns the value for the specified key path relative to the receiver. Any object in the key path sequence that is not key-value coding compliant for a particular key—that is, for which the default implementation of valueForKey: cannot find an accessor method—receives a valueForUndefinedKey: message.

overriden isEqual method not getting called every time during comparisons

I have a class called
Contact;
In Contact I have (simple version to test, no hash yet)
- (BOOL)isEqual:(id)other {
if (other == self)
return YES;
if (!other || ![other isKindOfClass:[self class]])
return NO;
return NO;
}
I do:
NSMutableArray *arr = [[NSMutableArray alloc] init];
Contact *contact = [Contact new];
[arr addObject:contact]
// I expect my isEqual to be called here but it does not get called
[arr containsObject:contact] // this evaluates to true somehow!!!
However if I add a another object to type NSString, then it gets called for comparing String object but not for the contact object. Which means
[arr addObject:#""] // so now arr has two elements
// here I expect two calls to isEqual but only one gets there
// when comparing string object against Contact
[arr containsObject:contact]
Why is isEqual not getting called in cases I mentioned above??
Please read the discussion about isEqual: in the NSObject Protocol in the Reference Library.
You'll find that for objects which are inside a collection (such as an NSArray), hash might be used to determine whether two objects are actually the same. If two pointers are actually pointing to the same object, there is no need to check for equality - hence isEqual: never gets called.
The solution as suggested by the reference library is to implement hash in your subclass as well.

iOS How do I compare if Objects are Equal? [duplicate]

This question already has answers here:
How do I compare objects in Objective-C?
(4 answers)
Closed 8 years ago.
I have a list of Objects that I pull from a web service. When I update my UITableView, I retrieve the objects again from the web service, and compare them to each other for equality. I then remove the ones that are not present, and insert the new objects, then update my UITableView. How can I test to see if the new object equals the old object? I've created a test for clarity..
requestA should equal requestC, but fails.
Is this possible to do without looking at each property value as the objects have many values?
I was originally comparing the ID only, but this doesn't work as sometimes other property values change and the ID stays the same.
Request *requestA = [[Request alloc] init];
Request *requestB = [[Request alloc] init];
Request *requestC = [[Request alloc] init];
requestA.requestID = #"1";
requestA.productName = #"Clutch";
requestB.requestID = #"2";
requestB.productName = #"Wiper";
requestC.requestID = #"1";
requestC.productName = #"Clutch";
if (requestA == requestB)
NSLog(#"A == B");
if (requestA == requestC)
NSLog(#"A == C");
if ([requestA isEqual:requestB])
NSLog(#"A isEqual B");
if ([requestA isEqual:requestC])
NSLog(#"A isEqual C");
// Look at the pointers:
NSLog(#"%p", requestA);
NSLog(#"%p", requestB);
NSLog(#"%p", requestC);
isEqual: is a method declared in NSObject Protocol. From official docs of isEqual:
This method defines what it means for instances to be equal. For
example, a container object might define two containers as equal if
their corresponding objects all respond YES to an isEqual: request.
See the NSData, NSDictionary, NSArray, and NSString class
specifications for examples of the use of this method.
If two objects are equal, they must have the same hash value. This
last point is particularly important if you define isEqual: in a
subclass and intend to put instances of that subclass into a
collection. Make sure you also define hash in your subclass.
Thus, as Salavat Khanov pointed out in his answer:
You need to implement -isEqual: and -hash methods for your Request class.
You want to do something like this:
// TestRequest.h
#interface TestRequest : NSObject
#property (nonatomic) NSString *requestID;
#property (nonatomic) NSString *productName;
#end
// TestRequest.m
#import "TestRequest.h"
#implementation TestRequest
- (BOOL)isEqual:(TestRequest *)object {
if (self == object) {
return YES;
}
if (![self.requestID isEqual:object.requestID]) {
return NO;
}
if (![self.productName isEqual:object.productName]) {
return NO;
}
return YES;
}
- (NSUInteger)hash {
// this is a very simple hash function
return [self.requestID hash] ^ [self.productName hash];
}
#end
or you can use a custom method:
- (BOOL)isEqualToRequest:(TestRequest *)otherRequest {
return [self.requestID isEqualToString:otherRequest.requestID] &&
[self.productName isEqualToString:otherRequest.productName];
}
Check this answer: How do I compare objects in Objective-C?
You need to implement -isEqual: and -hash methods for your Request class.
You need to overwrite isEqual: of your Request object to specify the properties to compare.
Write sth. like this:
- (BOOL)isEqual:(id)other {
if (other == self) return YES;
if (!other || ![other isKindOfClass:[self class]]) return NO;
if (![(id)[self name] isEqual:[other name]]) return NO;
// add other checks if needed
return YES;
}
First off. == is a check for "are these two objects actually the SAME OBJECT". I.e. They are just two pointers to the same but of memory.
You need to be using the isEqual method. However, in order to do this properly you need to override the method in the class.
Something like...
- (BOOL)isEqual:(Request *)otherObject
{
return [self.productName isEqual:otherObject.productName]
&& [self.requestID isEqual:otherObject.requestID];
}

iOS how does NSMutableArray check if it contains NSNumber objects?

I'm writing some code that will be using NSMutableArray and storing int values within it, wrapped within NSNumbers.
I would like to confirm that querying an iOS NSArray or NSMutableArray using new NSNumbers with same values is legal, of if I need to explicitly iterate over the array, and check if each int value is equal to the value I want to test against?
This appears to work:
NSMutableArray* walkableTiles = [NSMutableArray array];
[walkableTiles addObject:#(1)];
[walkableTiles addObject:#(2)];
[walkableTiles addObject:#(3)];
if([walkableTiles containsObject:#(1)])
{
DLog(#"contains 1"); //test passes
}
if([walkableTiles containsObject:[NSNumber numberWithFloat:2.0]])
{
DLog(#"contains 2");//test passes
}
if([walkableTiles containsObject:[NSNumber numberWithInt:3]])
{
DLog(#"contains 3");//test passes
}
What you are doing is fine. Why wouldn't it be?
The containsObject: method actually iterates over the array and calls the isEqual: method on each object passing in the object you are checking for.
BTW - there is nothing special here about using NSNumber. It's the same with an array of any object type. As long as the object's class has a valid isEqual: method, it will work.
Per the Apple's NSNumber documentation, you should use isEqualToNumber:
isEqualToNumber: Returns a Boolean value that indicates whether the
receiver and a given number are equal.
- (BOOL)isEqualToNumber:(NSNumber *)aNumber

Bad excess code for an array setter

I have an array of type NSArray as below:
#property (nonatomic, strong) NSArray *myArray;
In the setter of this array, I want to check if the array count is 0, then return nil else return the array as it is. I am doing it as below:
- (NSArray *)myArray
{
return ([self.myArray count] == 0) ? nil : self.myArray;
}
I am not getting any build error if I return nil for an NSArray, but its giving me run time error of bad access code. What mistake am I making here?
Your getter method is recursively calling itself, because
self.myArray
is translated by the compiler to
[self myArray]
This causes a stack overflow (!) eventually.
Inside a setter or getter method of a property, you have to access
the associated instance variable instead, e.g.
- (NSArray *)myArray
{
return ([_myArray count] == 0) ? nil : _myArray;
}
Do you instantiate the array anywhere else prior to calling this method? It will have to exist before you can access it.
_myArray = [NSArray arrayWithObjects:#"one",#"two",nil];
or one of the many other techniques to create an Array.

Resources