Objective-C Category [duplicate] - objective-c

This question already has answers here:
Closed 10 years ago.
Possible Duplicate:
What happens if two ObjC categories override the same method?
I have two Categories on NSString class as follows:
//first category
#import "NSString+MyCategory1.h"
#implementation NSString (MyCategory1)
-(void)myMethod{
NSLog(#"this is my method from category 1");
}
#end
//second category
#import "NSString+MyCategory2.h"
#implementation NSString (MyCategory2)
-(void)myMethod{
NSLog(#"this is my method from category 2");
}
#end
But the following main method is always calling myMethod from MyCategory1 even after import of the same has been commented out.
#import <Foundation/Foundation.h>
//#import "NSString+MyCategory1.h"
#import "NSString+MyCategory2.h"
int main(int argc, const char * argv[])
{
#autoreleasepool {
[[[NSString alloc]init] myMethod];
}
return 0;
}
Please anyone explain this behavior and how this behavior is useful in practice.

I think what this shows is that you should not have methods in categories that clash.
My (semi informed) guess is that which method gets called us down to how the app was compiled, so it's not something you can influence at runtime. And it's not really useful in practice, it's just... what happens.
As for why this happens, the header file doesn't say what to do when the method is called, only that an implementation for it exists. In your case, it does exist. It just happens not to be the one you want.

Related

Trouble with calling a method in Objective C (Apple Documentation example)

I'm following along with Apple's "Programming with Objective C" document, the link being: https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/WorkingwithObjects/WorkingwithObjects.html#//apple_ref/doc/uid/TP40011210-CH4-SW1
Anyways, I've gotten to the point where it ask for calling the sayHello method.
"Create a new XYZPerson instance using alloc and init, and then call the sayHello method."
#import <Foundation/Foundation.h>
#import "XYZPerson.h"
int main(int argc, const char * argv[]);
XYZPerson *firstPerson = [[XYZPerson alloc] init]; //Initializer element is not a lime-time constant
[firstPerson sayHello]; //No Visible #interface for 'XYZPerson' delcares the selector 'sayHello'
#implementation XYZPerson
- (void)sayHello {
[self saySomething:#"Hello, World"];
}
- (void)saySomething: (NSString *)greeting {
NSLog(#"%#", greeting);
}
#end
I believe I'm having a misunderstanding with how apple is explaining the work or just have no clue.
Wishing apple had these examples done for us to review over.
You need to put the code inside the main function. Right now you have the code just sitting in your file, outside of any function. It should be:
int main(int argc, const char * argv[]) {
XYZPerson *firstPerson = [[XYZPerson alloc] init];
[firstPerson sayHello];
}
Also, according to the docs you should have a separate main.m file that has your main function inside of it.
As you can only access public functions which are declared in .h file with the class object.
Kindly declare that function in .h file and it will solve your problem

Multiple parameters in method issue

My first shot at creating a method with multiple parameters. Still trying to wrap my head around how Objective C does things. Been banging my head for a couple days on this now. Finally ready to ask for help. Searched and tried many posts here on stack overflow. Below is various code chunks I'm working with ... this is a cocos2d v3 project FYI.
// MainPlayScene.h
#import <Foundation/Foundation.h>
#import "cocos2d.h"
#include <sys/sysctl.h>
#interface MainPlayScene : CCScene <CCPhysicsCollisionDelegate>
+ (MainPlayScene *)scene;
- (id)init;
- (void)evaluateTileAttack:(CCNode*)tileTouchedCCNode : (CCNode*)tileTouchedCCNode2;
#end
// MainPlayScene.m
#import "cocos2d.h"
#import "MainPlayScene.h"
#implementation MainPlayScene
{
CCNode *tileTouchedCCNode;
CCNode *tileTouchedCCNode2;
}
+ (instancetype)scene
{
return [[self alloc] init];
}
- (id)init
{
return self;
}
- (void)evaluateTileAttack: (CCNode*)ccnode1 : (CCNode*)ccnode2
{
NSLog(#"ccnode1: %#", ccnode1.physicsBody.collisionType);
NSLog(#"ccnode2: %#", ccnode2.physicsBody.collisionType);
}
- (void)actionMenuAttackHandler: (id)sender
{
[self evaluateTileAttack: tileTouchedCCNode, tileTouchedCCNode2];
^^^^^^^^^^^^^^^^^^^^^
error at this line
}
#end
ERROR: No visible #interface for 'MainPlayScene' declares the selector 'evaluateTileAttack:'
Not sure why I am getting this error because I think I am declaring in MainPlayScene.h properly ...
The method declaration, though technically valid I think, is at least unusual for ObjC. Best seen when you wrap and align (as is customary for long method calls/declarations) on the colon:
- (void)evaluateTileAttack:(CCNode*)tileTouchedCCNode
:(CCNode*)tileTouchedCCNode2;
Normally a method has a name for all parameters:
- (void)evaluateTileAttack:(CCNode*)tileTouchedCCNode
otherNode:(CCNode*)tileTouchedCCNode2;
The call is definitely invalid, ObjC methods do not take a comma-separated list of parameters (unless specifically declared to do so, which is rare). So this is illegal:
[self evaluateTileAttack: tileTouchedCCNode, tileTouchedCCNode2];
Instead it should be (not sure about this unnamed format though):
[self evaluateTileAttack:tileTouchedCCNode
:tileTouchedCCNode2];
This definitely works and is the expected/recommended approach:
[self evaluateTileAttack:tileTouchedCCNode
otherNode:tileTouchedCCNode2];

Objective-C: How to change the class of an object at runtime?

I tried to answer Using a UITableView subclass with a UITableViewController with ISA Switching like so:
self.tableView->isa = [MyTableView class];
But, I get the compile error: Instance variable 'isa' is protected.
Is there a way to get around this? And, if so, is it safe to do?
I'm asking because #AmberStar's answer to that question seems slightly flawed. (See my comment.)
If your tableview class provides ANY storage this will break. I would not recommend the path you're going down. But the correct method would be to use object_setClass(tableView, [MyTableView class]).
Please make sure this is really what you want.
Here is a small code-sample showing how this is a horrible idea.
#import <objc/runtime.h>
#interface BaseClass : NSObject
{
int a;
int b;
}
#end
#implementation BaseClass
#end
#interface PlainSubclass : BaseClass
#end
#implementation PlainSubclass
#end
#interface StorageSubclass : BaseClass
{
#public
int c;
}
#end
#implementation StorageSubclass
#end
int main(int argc, char *argv[])
{
BaseClass *base = [[BaseClass alloc] init];
int * random = (int*)malloc(sizeof(int));
NSLog(#"%#", base);
object_setClass(base, [PlainSubclass class]);
NSLog(#"%#", base);
object_setClass(base, [StorageSubclass class]);
NSLog(#"%#", base);
StorageSubclass *storage = (id)base;
storage->c = 0xDEADBEEF;
NSLog(#"%X == %X", storage->c, *random);
}
and the output
2011-12-14 16:52:54.886 Test[55081:707] <BaseClass: 0x100114140>
2011-12-14 16:52:54.889 Test[55081:707] <PlainSubclass: 0x100114140>
2011-12-14 16:52:54.890 Test[55081:707] <StorageSubclass: 0x100114140>
2011-12-14 16:52:54.890 Test[55081:707] DEADBEEF == DEADBEEF
As you can see the write to storage->c wrote outside the memory allocated for the instance, and into the block I allocated for random. If that was another object, you just destroyed its isa pointer.
The safe way is to create a new instance.
Swapping isa is not safe - you have no idea what the memory layout of a class is or what it will be in the future. Even moving up the inheritance graph is really not safe because objects' initialization and destruction would not be performed correctly - leaving your object in a potentially invalid state (which could bring your whole program down).

Extend iTunesApplication class with Categories

I am just learning how to use ScriptingBridges. I made a method that slowly fades the volume on iTunes, and would like to make it a category so I can do the following:
iTunesApplication* iTunes = [SBApplication applicationWithBundleIdentifier:#"com.apple.iTunes"];
[iTunes lowerVolume:50 speed:1];
I made another category for NSSpeechSynthesizer that works, but I can't get this one to. I keep getting the following build error:
"_OBJC_CLASS_$_iTunesApplication", referenced from:
l_OBJC_$_CATEGORY_iTunesApplication_$_iTunesApplicationAdditions in iTunesApplication.o
objc-class-ref-to-iTunesApplication in iTunesApplication.o
ld: symbol(s) not found
collect2: ld returned 1 exit status
Is there something special I can do to make it work since I can't include the symbols?
Thanks,
Ryan Pendleton
UPDATE:
I only found one solution, which is below. It involves MethodSwizzling, so I'm open to better answers, but for now it's all I have.
The solution I found was to use the Objective-C runtime API. I'm sure there's a better way to organize this, but here's how I did it:
Here are my .h and .m files for creating the category. Notice how lowerVolume is not an actual method, but a C function with the arguments id self, and SEL _CMD. You'll also notice a setupCategories function. We'll call that later.
// iTunes+Volume.h
#import <objc/runtime.h>
#import "iTunes.h"
void lowerVolume(id self, SEL _cmd, int dest, float speed);
void setupCategories();
#interface iTunesApplication (Volume)
- (void)lowerVolume:(int)dest speed:(float)speed;
#end
// iTunes+Volume.m
#import "iTunes+Volume.h"
void lowerVolume(id self, SEL _cmd, int dest, float speed)
{
NSLog(#"Lower Volume: %i, %f", dest, speed);
}
void setupCategories()
{
id object = [[SBApplication alloc] initWithBundleIdentifier:#"com.apple.iTunes"];
Class class = [object class];
[object release];
class_addMethod(class, #selector(lowerVolume:speed:), (IMP)lowerVolume, "#:if");
}
Now that I've made the functions, I need to actually add them to the scripting bridge class using the Objective-C runtime API. I'll do this in main.m to make sure that the methods are ready to be used when the run loop starts.
// main.m
#import <Cocoa/Cocoa.h>
#import "iTunes+Volume.h"
int main(int argc, char *argv[])
{
NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
setupCategories();
return NSApplicationMain(argc, (const char **) argv);
[pool drain];
}
Now, I can use my method wherever I want as long as I include the header files:
- (void)mute
{
iTunesApplication* iTunes = [[SBApplication alloc] initWithBundleIdentifier:#"com.apple.iTunes"];
[iTunes lowerVolume:0 speed:1];
[iTunes release];
}
If any of this doesn't make sense, just tell me and I'll try to explain it better.
I think you need to include -framework ScriptingBridge to your gcc arguments. That got it to compile for me!
As noted above, you can't easily do a category on iTunesApplication because it doesn't exist at compile time, and also because the runtime class name is ITunesApplication (capital "I").
The best solution I've found is to do your category on the class that DOES exist, SBApplication. Here's the code I tested that works and does what the original example was trying to do:
// SBApplication+Extensions.h
#import ScriptingBridge;
#interface SBApplication (Extensions)
- (void)lowerVolume:(int)dest speed:(float)speed;
#end
// SBApplication+Extensions.m
#import "iTunes.h"
#import "SBApplication+Extensions.h"
#implementation SBApplication (Extensions)
- (void)lowerVolume:(int)dest speed:(float)speed
{
NSLog(#"Lower Volume: %i, %f", dest, speed);
}
#end
// Caller, say in AppDelegate
#import "SBApplication+Extensions.h"
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
iTunesApplication *iTunesApp =
[SBApplication applicationWithBundleIdentifier:#"com.apple.iTunes"];
[iTunesApp lowerVolume:4 speed:3.3f];
}

Handling class methods when sub-classing in objective-c

While attempting my first sub-class in Objective-C I have come across the following warning which I cannot seem to resolve. The call to decimalNumberWithMantissa gives a warning of "initialization from distinct Objective-C type".
#import <Foundation/Foundation.h>
#interface NSDecimalNumberSub : NSDecimalNumber {
}
#end
#implementation NSDecimalNumberSub
#end
int main (int argc, char *argv[]) {
NSDecimalNumberSub *ten = [NSDecimalNumberSub
decimalNumberWithMantissa:10
exponent:0
isNegative:NO];
}
Does a class method have to be treated differently with a sub-class? Am I missing something simple? Any help would be appreciated.
NSDecimalNumber defines the decimalNumberWithMantissa:... method to return an NSDecimalNumber, so you're going to get back an instance of the base class and not your custom subclass. You'll have to create your own convenience method to return an instance of your subclass, or just alloc and initialize it another way.
If you're writing your own class you can define a convenience method like that to return type id, and then use [[self alloc] init] when creating the instance to make your class safe for subclassing.