I'm working on a dynamic implementation of a dictionary that also supports properties declared using the #dynamic keyword (similar to NSManagedObject).
Can I tell at runtime if a particular selector was declared with #dynamic? Is this just compiler trickery for design time tooling and lost at runtime or is there someway to inspect this?
+ (BOOL) resolveInstanceMethod:(SEL)sel
{
NSString *method = NSStringFromSelector(sel);
// ideally I could also check here if the selector is #dynamic
if ([method hasPrefix:#"set"] && [method rangeOfString:#":"].location == method.length -1) {
class_addMethod([self class], sel, (IMP) dynamicSet, "v#:#");
return YES;
}
else if ([method hasPrefix:#"get"] && [method rangeOfString:#":"].location == method.length -1) {
class_addMethod([self class], sel, (IMP) dynamicGet, "v#:#");
return YES;
}
BOOL value = [super resolveInstanceMethod:sel];
return value;
}
Also, my class subclasses NSDictionary but it when [super resolveInstanceMethod:sel] is called for an existing method - it still returns false?
If you know the name of the property you can use some runtime functions to investigate whether it's a dynamic property or not, as shown in the following function. Make sure to import <objc/runtime.h>.
BOOL isClassPropertyDynamic(Class theClass, NSString *propertyName)
{
BOOL isDynamic = NO;
objc_property_t property = class_getProperty(theClass, [propertyName UTF8String]);
char *dynamicAttributeValue = property_copyAttributeValue(property, "D");
if (dynamicAttributeValue != NULL) {
isDynamic = YES;
free(dynamicAttributeValue);
}
return isDynamic;
}
However, it's not always going to be easy to go from a selector name to the property, as both getters and setters names can be customized at declaration time. Typically that is only done for getters of boolean properties but technically anyone can break that convention.
Conventionally, if a selector starts with "set" followed by an uppercase letter and contains one ":" at the end, the property name would be the string resulting from removing "set" and ":" and making the first letter lowercase.
If a selector starts with "is" followed by an uppercase letter and has no arguments, then the property name corresponding to that would be the string resulting from removing "is" and making the first letter lowercase. Selectors that have no arguments and don't start with "is" and an uppercase letter would generally have the property name and the selector name the same.
Again, that's just convention and will be broken by somebody somewhere. So, you have to decide if it's truly valuable to determine whether a selector corresponds to a dynamic property or not (like borrrden I doubt it's really relevant but I'm not familiar with your requirement).
You could also follow rob mayoff's excellent suggestion from the comments that you "iterate over all of the properties (using class_copyPropertyList ) and check the G and S (attributes) of each" to build a mapping between selectors and properties.
Related
I want to check if a JSON object is an NSString and if it isn't, assign it a default string. My ultimate goal is to prevent crashing and assign the properties a proper value no matter what. This is an example of a data model I am using where dict is the JSON dictionary the API returns.
Data *data = [[self alloc] init];
data.name = [NSString validateString:dict[#"name"] defaultString:#""];
data.status = [NSString validateString:dict[#"status"] defaultString:#"OPEN"];
Here is the category method validateString I am using.
+ (NSString *)validateString:(NSString *)aString defaultString:(NSString *)defaultString {
if ([aString isKindOfClass:[NSString class]]) {
return aString;
}
return defaultString;
}
It makes no sense, and is very bad practice, to cast (NSString *)aString and then ask if this is in fact an NSString.
Also, what if it is nil?
All you know when you fetch from a dictionary is that you get an id. Do not assume more than that.
I would suggest writing very plainly: say what you mean, and mean what you say. That is the best practice in Objective-C. Otherwise, dynamic typing and "nil trickery" can lead you into subtle errors. You might not have any trouble in this particular case, but bad habits are bad habits, and it is best not to let them form in the first place. I'd rewrite like this:
+ (NSString *) checkType:(nullable id)obj defaultString:(NSString *)def {
if (obj == nil || ![obj isKindOfClass:[NSString class]]) {
return def;
}
return obj;
}
Like mentioned in other comments: if you want to prevent crashes, you also need to check if it's nil, specially if there is a chance to port your code to Swift in the future.
Just to clarify my last sentence, the line below works in Objective-C even if aString is nil:
if ([aString isKindOfClass:[NSString class]]) {
That's because, in the way Objective-C was made, calling a function on a nil object returns nil, so the if will be considered false, and the function will return defaultString. Yeah... that's certainly a bad idea when they created Objetive-C, since this leads to lots of errors. More details about that behaviour below:
https://stackoverflow.com/a/2696909
Anyway, it's also a good practice to only cast an object after checking its type, so I would recommend adapting your function to this:
+ (NSString *)validateString:(id)obj defaultString:(NSString *)defaultString {
if (obj != nil && [obj isKindOfClass:[NSString class]]) {
return (NSString*)obj;
}
return defaultString;
}
Every object that implements NSObject* has isKindOfClass: (and NSDictionary* only stores objects that implement NSObject*), so we don't need to check if the object responds to it. Also, even if we wanted, respondsToSelector: is also an NSObject* function.
Still, the method that you are using still works. The revised function above is just adapted to better practices and to avoid problems in case you ever need to port this code to Swift (or any other language) in the future.
EDIT: Updated code based in #matt's suggestion.
I know that I can override hash and isEqual to check 2 instances equality. Xcode has the default snippet and doucument https://developer.apple.com/library/ios/documentation/General/Conceptual/DevPedia-CocoaCore/ObjectComparison.html as following
- (BOOL)isEqual:(id)other
{
if (other == self) {
return YES;
} else if (![super isEqual:other]) { //WHAT is this line mean ?
return NO;
} else {
return <#comparison expression#>;
}
}
- (NSUInteger)hash
{
return <#hash expression#>;
}
Okay,
other == self check two objects' pointers.
if ![super isEqual:other], what is this line means ? If super object is not equal other, return NO ? Then it will always return NO, the step 3 will not be executed.
Am I wrong ?
Thanks.
It's a typical implementation in a class-hierarchy, that is, if your class derives from a super class that has its own meaningful isEqual: implementation. In that case it is wise to let the super class test the equality of the common properties. If the common part is not equal, then there is no chance that the derived objects are equal.
It is not needed if you derive directly from NSObject.
Actually, you'll need an extra step as well:
- (BOOL)isEqual:(id)other
{
if (other == self) {
return YES;
} else if (![super isEqual:other]) {
return NO;
} else if (![other isKindOfClass:[MyClass class]]) {
return NO; // comparing incompatible objects
} else {
MyClass *myOther = (MyClass *) other;
return <#compare between self and myOther#>;
}
}
Let's look at one example of class inheritance:
#interface A : NSObject
#property (nonatomic, assign, readwrite) NSInteger fieldA;
#end
#interface B : A
#property (nonatomic, assign, readwrite) NSInteger fieldB;
#end
Now, if you want to implement equality on A, then you want to base it on the equality of fieldA:
// A equality
- (BOOL)isEqual:(id)other {
...
return [self fieldA] == [other fieldA];
}
When you are implementing equality on B, you need two conditions - first you have to make sure that fieldA is equal and then you have to make sure that fieldB is equal.
// B equality
- (BOOL)isEqual:(id)other {
...
return [super isEqual:other] && [self fieldB] == [other fieldB];
}
That's exactly what the [super isEqual:other] is doing - it checks the equality requirement of the superclass, that is fieldA.
To be honest, this isEqual: template is not very good. It is missing one of the most important things and that is the class equality check:
if (![other isMemberOfClass:[self class]]) {
return NO;
}
You don't need this check only when you never mix instances of different classes. However, when you start putting instances of A and B into the same array/dictionary etc. you will have crashes when trying to compare instances of A with instances of B.
There is a slightly different between Hash and isEqual in Objective-C.
First of all, NSObject checks equality with another object with the method isEqual: and basically, two objects may be equal to another, if they share a common set of observable properties.
Hashing in object comparison is an extra step in determining collection membership, which will faster your operation.
This will explain a little bit about hash and isEqual
Object equality is commutative ([a isEqual:b] --> [b isEqual:a])
If objects are equal, then their hash values must also be equal ([a isEqual:b] --> [a hash] == [b hash])
However, the converse does not hold: the hash values of two objects are equal do not mean that their values to be equal.
I hope this would be helpful. For reference, you can visit this link http://nshipster.com/equality/
This may sound like a silly question but Apple provides us with NSNotFound but why didn't they provide one called NSFound? Is there a way one can define a NSFound macro on their own?
The reason I am asking all this is that in order for me to check if a string "contains" a certain character I have to do double negative i.e.
if ([XML rangeOfString:#"error" options:NSCaseInsensitiveSearch].location != NSNotFound)
{
//server is down
}
else
{
//server is up
}
At least for me this would have been so much easier to read if I could simply do this instead
if ([XML rangeOfString:#"error" options:NSCaseInsensitiveSearch].location == NSFound)
{
//server is down
}
else
{
//server is up
}
If I want to define NSFound or SAMFound, how would I go about doing that?
Your issue is really with the design pattern methods like rangeOfString follow - using a single return value for both valid results, of which there are many, and failure indications, of which there is one. You can test for a single failure value with a comparison to a constant, NSNotFound in this case, but you cannot likewise test for many possible values with a simple comparison - instead you use the "double negative" you don't like.
If you find it too ugly change it... Maybe:
#interface NSString (SamExtras)
- (BOOL) SAMcontainsString:(NSString *)string options:(NSStringCompareOptions)options;
#end
#implementation NSString (SamExtras)
- (BOOL) SAMcontainsString:(NSString *)string options:(NSStringCompareOptions)options
{
return [self rangeOfString:string options:options].location != NSNotFound;
}
#end
Which would allow you to use:
if ([XML SAMcontainsString:#"error" options:NSCaseInsensitiveSearch])
{
//server is down
}
else
{
//server is up
}
with no double negative. You can write the category once and use it in all your projects.
HTH
Double Negative doesn't have the consequences in code as it does in grammar.
The reason they provide a not found, as opposed to a found version, is simply the not found value is a single (supposedly invalid) value and everything else is valid. It's therefore simpler to define this single, invalid value.
Also it makes more sense (more efficient, avoiding a double-search and less code) to store the NSRange in a local variable in order to firstly test for validity and then to use the value:
NSRange range = [XML rangeOfString:#"error" options:NSCaseInsensitiveSearch];
if (range.location != NSNotFound) {
// Do thing with range
} else {
// Complain
}
There is nothing whatever wrong with your original test:
if ([XML rangeOfString:#"error" options:NSCaseInsensitiveSearch].location != NSNotFound) {
If all you need to know is whether XML contains the string #"error", that test answers the question and is a perfectly legitimate and idiomatic way to ask it. Observe that even the documentation tells you that containsString: is nothing but a front for calling rangeOfString:options:!
If you really want to know what the positive version would be, it would be to test the length of the returned range and see if it is the same as the length of #"error". The length of a not-found range is 0.
How do I find out if a struct is of a specific type? In other words, if I get an object, how do I know that the underluying type is a struct?
+(BOOL)isPrimitive:(id)input
{
return [input isKindOfClass:[NSNumber class] ] || [input isKindOfClass:[NSDate class]] || [input isKindOfClass:[NSString class]]
|| __IS_THIS_A_STRUCT__ (specifically SEL);
}
What should I put in place of IS_THIS_A_STRUCT?
Based on your comments, it looks like you know a property and want to act in a certain way if it returns a struct. If so then you could do something like:
if(!strcmp([[self class]
instanceMethodSignatureForSelector:#selector(propertyName)].methodReturnType,
#encode(SEL)))
#encode returns the type encoding for the named type, which is a C string. instanceMethodSignatureForSelector returns an NSMethodSignature which can nominate the return type of that method as an encoded type.
The two type encodings are not guaranteed to have the same identity but will have the same value. So you can use the C function strcmp to check that they're the same.
You can use NSSelectorFromString if the selector name is not known at compile time.
What would be the best method to compare an NSString to a bunch of other strings case insensitive? If it is one of the strings then the method should return YES, otherwise NO.
Here's a little helper function:
BOOL isContainedIn(NSArray* bunchOfStrings, NSString* stringToCheck)
{
for (NSString* string in bunchOfStrings) {
if ([string caseInsensitiveCompare:stringToCheck] == NSOrderedSame)
return YES;
}
return NO;
}
Of course this could be greatly optimized for different use cases.
If, for example, you make a lot of checks against a constant bunchOfStrings you could use an NSSet to hold lower case versions of the strings and use containsObject::
BOOL isContainedIn(NSSet* bunchOfLowercaseStrings, NSString* stringToCheck)
{
return [bunchOfLowercaseStrings containsObject:[stringToCheck lowercaseString]];
}
Just to add a few additions to Nikolai's answer:
NSOrderedSame is defined as 0
typedef NS_ENUM(NSInteger, NSComparisonResult) {NSOrderedAscending = -1L, NSOrderedSame, NSOrderedDescending};
So if you call caseInsensitiveCompare: on a nil object you would get nil. Then you compare nil with NSOrderSame (which is 0) you would get a match which of course is wrong.
Also you will have to check if parameter passed to caseInsensitiveCompare: has to be not nil. From the documentation:
This value must not be nil. If this value is nil, the behavior is
undefined and may change in future versions of OS X.