Is this the right way to run a generic method? - ios

I have this method that should set the name of objects but objects can be of 3 classes, A, B and C.
If I simply do this
[object setName: #"new name"]; //at this point I am treating object as of type id
Xcode will complain that there are multiple methods named "setName", then I am doing this
if ([object isKindOfClass:[ClassA class]]) {
[(ClassA *)object setName:newName];
} else
if ([object isKindOfClass:[ClassB class]]) {
[(ClassB *)object setName:newName];
} else
[(ClassC *)object setName:newName];
}
But this appears lame to me.
I was trying to fool Xcode by using something like this
[(typeof(object))object setName:newName];
But Xcode is not liking it too, probably because typeof(object) is returning id and we are back to square one. Same error.
What better/elegant ways I have to do this?

Use a protocol that class A, B, and C all conform to.
#protocol MyProtocol <NSObject>
#required
- (void)setName:(NSString *)name;
#end
And then
id<MyProtocol> object = ...
[object setName:newName];

A better way would be having A, B and C implementing the same protocol that will define a method setName.
NameProtocol.h
#protocol NameProtocol <NSObject>
- (void)setName:(NSString *)name;
#end
A.h
#interface A : NSObject <NameProtocol>
B.h
#interface B : NSObject <NameProtocol>
C.h
#interface C : NSObject <NameProtocol>

Use Key-Value Coding.
[object setValue:newName forKey:#"name"];
This is a quick and dirty way, and I would recommend using a protocol, but it can be useful in the right circumstance.
Update
KVC has no compile time type checking: increasing the likelihood of defects. Runtime errors result in exceptions: defects cause the app to crash. Its syntax implies object is a dictionary: hiding the intent of the code. It's obscure: junior developers generally won't understand how it works and may cause maintenance issues.
It's a real horror show, but it can be useful in the right circumstance.

Protocols are the way to go. But you can also do:
if ([object respondsToSelector: #selector(setName:)])
{
[object performSelector: #selector(setName:) withObject: name];
}

Related

Objective-C method signatures: Parameter types can differ between declaration and implementation?

I can declare a method in the #interface having parameter type NSString*:
- (id) initWithString:(NSString*)str;
while in the implementation it is NSNumber*:
- (id) initWithString:(NSNumber*)str
For a full example, see the code below. When calling [Work test] the output is a.x = Hi, so the passed-in NSString* went through and one can see that the "correct" initWithString method was called.
Why is this code accepted by the compiler?
Can I make the compiler complain when parameter types differ?
Citing from Apple's documentation Defining Classes :
The only requirement is that the signature matches, which means you must keep the name of the method as well as the parameter and return types exactly the same.
My test code:
#interface ClassA : NSObject
#property (strong, nonatomic) NSNumber *x;
- (id) initWithString:(NSString*)str;
- (void) feed:(NSString*)str;
#end
#implementation ClassA
- (id) initWithString:(NSNumber*)str
{
self = [super init];
if (self) {
self.x = str;
}
return self;
}
- (void) feed:(NSNumber*)str
{
self.x = str;
}
#end
#implementation Work
+ (void) test
{
ClassA *a = [[ClassA alloc] initWithString:#"Hi"];
NSLog(#"a.x = %#", a.x);
}
#end
I added the feed method to see, whether it is "special" to init-like methods, but the compiler doesn't complain either.
(Ran this on Yosemite / Xcode 6.4 / iOS8.4 Simulator.)
PS: If I didn't use the right terms, please correct me :-)
Can I make the compiler complain when parameter types differ?
There's a warning for this which you can activate by including the following line in the header:
#pragma clang diagnostic error "-Wmethod-signatures"
You can also put -Wmethod-signatures into the project's "Other Warning Flags" Xcode build setting to activate this for the whole project.
I don't really understand why Apple is so hesitant to activate helpful warnings like this by default.
My standard pattern on virtually every project is to put -Weverything in "Other Warning Flags". This activates all warnings clang has to offer.
Since there are some warnings that are a little too pedantic or don't serve my coding style, I individually deactivate unwanted warning types as they pop up.
I'm surprised by the quote you found stating that param and return types matter to the uniqueness of the method signature. Re-reading, I think you found a bug in the doc.
Defining a parameter type in the interface will generate a warning for callers that do not pass that type (or cast the parameter to that type), no matter the implementation. Changing the parameter type in the implementation is exactly like casting the parameter within the method. Nothing wrong with that, not even a cause for warning. So long as the different type shares methods (polymorphic or inherited) with the declared type.
In other words, restating by example...
The following will cause a compiler error, proving that distinct param types offers no distinction to the compiler (same is true for return type)...
// .h
- (void)foo:(NSString *)str;
// .m
- (void)foo:(NSString *)str {
NSLog(#"called foo %#", [str class]);
}
- (void)foo:(NSNumber *)str { <----- duplicate declaration error
}
The following will cause no compiler warnings, errors or runtime errors...
// .h
- (void)foo:(NSString *)str;
// .m
- (void)foo:(NSNumber *)str {
// everything implements 'class', so no problem here
NSLog(#"called foo %#", [str class]);
}
The following is exactly like the previous example in every respect...
// .h
- (void)foo:(NSString *)str;
// .m
- (void)foo:(NSString *)str {
NSNumber *number = (NSNumber *)str;
NSLog(#"called foo %#", [number class]);
}
The following will cause no warnings, but will generate a runtime error because we are abusing the cast by invoking a method that the passed type doesn't implement (presuming the caller calls with a string as the interface indicates)...
// .h
- (void)foo:(NSString *)str;
// .m
- (void)foo:(NSNumber *)str {
NSLog(#"does str equal 2? %d", [str isEqualToNumber:#2]); <--- crash!
}
All of the foregoing matches intuition and behavior in practice, just not that passage in the doc. Interesting find!
In Objective-C a method is defined as a string (known as a selector) in the form of doSomethingWithParam:anotherParam:. Or in your case it will be initWithString:. Note there's no parameter types in these strings. One side-effect of defining methods like this is that Objective-C, unlike Java or C++ doesn't allow overloading operators by just changing the parameter type. Another side-effect is the behavior you observed.
EDIT: Additionally, it appears that the compiler does not look at the implementation at all when checking method calls, just the interface. Proof: declare a method in a header, don't specify any implementation for that method, and call this method from your code. This will compile just fine, but of course you'll get an "unrecognized selector" exception when you run this code.
It'd be great if someone could provide a nice explanation of the default compiler behavior.

How to implement countByEnumeratingWithState:objects:count: for class that internally use NSMutableArray

I wanted to use
for (TBL_CardView *cardView in cardsInHand)
{
// <#statements#>
}
TBL_CardView is my custom class, and cardsInHand is just (TBL_CardViewArray*)
So I need to implement countByEnumeratingWithState:objects:count: for my TBL_CardViewArray class.
Is this correct ?
This is my TBL_CardViewArray.h
/**
* Keep TBL_CardView in array
*/
#interface TBL_CardViewArray : NSObject
- (TBL_CardView *)drawCard;
- (void)addCard:(TBL_CardView *)card;
- (NSUInteger)cardsRemaining;
- (NSArray*) cardViewArray;
- (TBL_CardView *)drawRandomCard;
#end
Some important part from TBL_CardViewArray.m
#implementation TBL_CardViewArray
{
NSMutableArray *_cards;
}
So I am just using TBL_CardViewArrayas s wrapper around NSMutableArray for storing my TBL_CardViewclass.
Question
How to implement countByEnumeratingWithState:objects:count: for my TBL_CardViewArray class.
I did google it, but not found some example that I could reuse easy.
My assumption is that because I am already using NSMutableArray for storing that it is not so complicated, but I can not figure it how ?
Quite simply, forward it to the underlaying NSMutableArray:
- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(id __unsafe_unretained [])stackbuf count:(NSUInteger)len {
return [_cards countByEnumeratingWithState:state objects:stackbuf count:len];
}
You have to avoid mutating the array while it's being enumerated.

Strange behavoir when decoding an NSArray via NSSecureCoding

i spent all afternoon banging my head against the wall trying to figure out why decoding of this class was failing. the class has a property that is an NSArray of Foo objects. Foo conforms to NSSecureCoding, and i have successfully encoded and decoded that class by itself. i was getting an error in initWithCoder: that said failed to decode class Foo. through some experimentation, i discovered that i needed to add [Foo class] to initWithCoder: in order for it to work. maybe this will help someone else who's having the same problem. my question is, why is this necessary? i found no suggestion that this is necessary in apple's documentation.
#import "Foo.h"
#interface MyClass : NSObject <NSSecureCoding>
#property (nonatomic) NSArray *bunchOfFoos;
#end
#implementation MyClass
static NSString *kKeyFoo = #"kKeyFoo";
+ (BOOL) supportsSecureCoding
{
return YES;
}
- (void) encodeWithCoder:(NSCoder *)encoder
{
[encoder encodeObject:self.bunchOfFoos forKey:kKeyFoo];
}
- (id) initWithCoder:(NSCoder *)decoder
{
if (self = [super init])
{
[Foo class]; // Without this, decoding fails
_bunchOfFoos = [decoder decodeObjectOfClass:[NSArray class] forKey:kKeyFoo];
}
return self;
}
#end
For those who are still struggling with this: #Ben H's solution didn't solve my problem. And I keep having the following error message:
Terminating app due to uncaught exception 'NSInvalidUnarchiveOperationException', reason: >'value for key 'NS.objects' was of unexpected class 'ClassA'. Allowed classes are '{(
NSArray
)}'.'
And finally, I realized that for custom classes. You have to use the following function instead decodeObjectOfClasses:
- (id)decodeObjectOfClasses:(NSSet *)classes forKey:(NSString *)key
And you to pass a NSSet of all possible classes in the NSArray to the function above! I am not sure why #Ben H could solve the issue by simply adding a [Foo class] outside of the function. Maybe it is a compiler issue. But anyway, if his solution doesn't work, try this one as well.
I've just encountered similar issue and that was weird and extremely time consuming. I wanted to test my class to be NSSecureCoded correctly with Specta/Expecta. So I've implemented everything as needed specifying class when decoded. At the end of my trials I got weirdest exception:
value for key 'key' was of unexpected class 'MyClass'. Allowed classes are '{(
MyClass
)}'.
Test looked something like that:
MyClass *myClassInstance = ...
NSMutableData *data = [NSMutableData data];
NSKeyedArchiver *secureEncoder = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data];
[secureEncoder setRequiresSecureCoding:YES]; // just to ensure things
NSString *key = #"key";
[secureEncoder encodeObject:myClassInstance forKey:key];
[secureEncoder finishEncoding];
NSKeyedUnarchiver *secureDecoder = [[NSKeyedUnarchiver alloc] initForReadingWithData:data];
[secureDecoder setRequiresSecureCoding:YES];
MyClass *decodedInstance = [secureDecoder decodeObjectOfClass:[MyClass class] forKey:key]; // exception here
[secureDecoder finishDecoding];
...expect...
While plain NSCoding (requiresSecureCoding = NO) test succeeded, NSSecureCoding tests kept failing. After vast range of trials I found solution for that, just a single line:
[secureDecoder setClass:[MyClass class] forClassName:NSStringFromClass([MyClass class])];
After that all my tests succeeded, objects were created as expected.
I'm not sure why did that happened, my guess would be that class is not visible as Ben H suggested and it uses something like NSClassFromString(#"MyClass"). The above code worked fine in AppDelegate. MyClass was from development pods I'm developing.
i think i may have figured this out. without the line [Foo class], there is no reference to the class Foo in this file. because of this, i believe the compiler is optimizing the Foo class out, and then the Foo objects within the array cannot be decoded. having [Foo class] in there prevents this.
Yuchen's answer is/was on the right track but the important thing to know is that the NSSet parameter needs to include the class for the collection in addition to the custom class, like so:
_bunchOfFoos = [decoder decodeObjectOfClasses:[NSSet setWithObjects:[NSArray class], [Foo class], nil] forKey:kKeyFoo];
At least that's what seems to be working for me at this point...

notNullObjectForKey addition - do we need this or is there a non-category solution

I'm just looking at some code we have so this question is asked from one of ignorance and making sure I fully understand what is going on. We have a couple of apps that always include a category with the following:
header file:
#import <Foundation/Foundation.h>
#interface NSDictionary (additions)
-(id)notNullObjectForKey:(id)aKey;
#end
implementation file:
#import "NSDictionary+Additions.h"
#implementation NSDictionary (additions)
-(id)notNullObjectForKey:(id)aKey
{
id obj=[self objectForKey:aKey];
if([obj isKindOfClass:[NSNull class]]){
return nil;
}
return obj;
}
#end
We use this for JSON parsing to protect against malformed data like so (in Item.h) - presumably.
-(id)initWithAttributes:(NSDictionary *)attributes
{
self.name=[attributes notNullObjectForKey:#"name"];
return self;
}
Is there a way to do this without having to add this to every project? Is all this checking for is if the key exists and if not, return NSNull?
thx
The code checks to see if the object is equal to NSNull and, if it is, returns nil. This prevents the rest of the code from needing to check. If the value doesn't exist for that key or it is not NSNull then the code doesn't change anything.
There are other ways in which this could be done but a category is a fine solution. Most other solutions would be more contrived and/or less easy to read.

NSDictionary: method only defined for abstract class. My app crashed

My app crashed after I called addImageToQueue. I added initWithObjects: forKeys: count: but it doesn't helped me.
Terminating app due to uncaught exception 'NSInvalidArgumentException',
reason: '*** -[NSDictionary initWithObjects:forKeys:count:]:
method only defined for abstract class.
Define -[DictionaryWithTag initWithObjects:forKeys:count:]!'
my code
- (void)addImageToQueue:(NSDictionary *)dict
{
DictionaryWithTag *dictTag = [DictionaryWithTag dictionaryWithDictionary:dict];
}
#interface DictionaryWithTag : NSDictionary
#property (nonatomic, assign) int tag;
- (id)initWithObjects:(id *)objects forKeys:(id *)keys count:(NSUInteger)count;
#end
#implementation DictionaryWithTag
#synthesize tag;
- (id)initWithObjects:(id *)objects forKeys:(id *)keys count:(NSUInteger)count
{
return [super initWithObjects:objects forKeys:keys count:count];
}
#end
Are you subclassing NSDictionary? That's not a common thing to do in Cocoa-land, which might explain why you're not seeing the results you expect.
NSDictionary is a class cluster. That means that you never actually work with an instance of NSDictionary, but rather with one of its private subclasses. See Apple's description of a class cluster here. From that doc:
You create and interact with instances of the cluster just as you would any other class. Behind the scenes, though, when you create an instance of the public class, the class returns an object of the appropriate subclass based on the creation method that you invoke. (You don’t, and can’t, choose the actual class of the instance.)
What your error message is telling you is that if you want to subclass NSDictionary, you have to implement your own backend storage for it (for example by writing a hash table in C). It's not just asking you to declare that method, it's asking you to write it from scratch, handling the storage yourself. That's because subclassing a class cluster directly like that is the same as saying you want to provide a new implementation for how dictionaries work. As I'm sure you can tell, that's a significant task.
Assuming you definitely want to subclass NSDictionary, your best bet is to write your subclass to contain a normal NSMutableDictionary as a property, and use that to handle your storage. This tutorial shows you one way to do that. That's not actually that hard, you just need to pass the required methods through to your dictionary property.
You could also try using associative references, which "simulate the addition of object instance variables to an existing class". That way you could associate an NSNumber with your existing dictionary to represent the tag, and no subclassing is needed.
Of course, you could also just have tag as a key in the dictionary, and store the value inside it like any other dictionary key.
From https://stackoverflow.com/a/1191351/467588, this is what I did to make a subclass of NSDictionary works. I just declare an NSDictionary as an instance variable of my class and add some more required methods. It's called "Composite Object" - thanks #mahboudz.
#interface MyCustomNSDictionary : NSDictionary {
NSDictionary *_dict;
}
#end
#implementation MyCustomNSDictionary
- (id)initWithObjects:(const id [])objects forKeys:(const id [])keys count:(NSUInteger)cnt {
_dict = [NSDictionary dictionaryWithObjects:objects forKeys:keys count:cnt];
return self;
}
- (NSUInteger)count {
return [_dict count];
}
- (id)objectForKey:(id)aKey {
return [_dict objectForKey:aKey];
}
- (NSEnumerator *)keyEnumerator {
return [_dict keyEnumerator];
}
#end
I just did a little trick.
I'm not sure that its the best solution (or even it is good to do it).
#interface MyDictionary : NSDictionary
#end
#implementation MyDictionary
+ (id) allocMyDictionary
{
return [[self alloc] init];
}
- (id) init
{
self = (MyDictionary *)[[NSDictionary alloc] init];
return self;
}
#end
This worked fine for me.

Resources