When using Objection, implementation of custom protocol crashes with unrecognized selector - objective-c

I am defining a custom protocol:
#protocol NGSAuthProvider <NSObject>
- (BOOL)isReady;
- (BOOL)isSessionValid;
- (void)login;
- (void)logout;
- (NSString *)accessToken;
- (BOOL)handleOpenURL:(NSURL *)url;
#end
I want to have different providers. So one is a Facebook provider:
#interface NGSFacebookAuthProvider : NSObject <NGSAuthProvider>
#end
#interface NGSFacebookAuthProvider () <FBSessionDelegate>
#property BOOL ready;
#property(nonatomic, retain) Facebook *facebook;
#property(nonatomic, retain) NSArray *permissions;
#end
#implementation NGSFacebookAuthProvider
//Implementation of fbLogin, fbLogout and the methods in NGSAuthProvider that forward calls to self.facebook
- (NSString *)accessToken
{
return [self.facebook accessToken];
}
#end
I setup Objection to bind from my class to the protocol.
#interface NGSObjectionModule : ObjectionModule
#end
#implementation NGSObjectionModule
- (void)configure
{
self bind:[NGSFacebookAuthProvider class] toProtocol:#protocol(NGSAuthProvider)];
}
#end
I setup the Global Injector:
#implementation NGSAppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
ObjectionModule *module = [[NGSObjectionModule alloc] init];
ObjectionInjector *injector = [Objection createInjector:module];
[module release];
[Objection setGlobalInjector:injector];
}
I am using this in my RootViewController like this:
#interface RootViewController : UITableViewController
#end
#interface RootViewController ()
#property(nonatomic, retain) id<NGSAuthProvider> authProvider;
#end
#implementation RootViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.authProvider = [[Objection globalInjector] getObject:#protocol(NGSAuthProvider)];
}
- (void)processConfig {
NSString *token = [self.authProvider accessToken];
// use the access token
}
#end
When I run this, I get the following error:
2011-07-26 21:46:10.544 ngs[6133:b603] +[NGSFacebookAuthProvider accessToken]: unrecognized selector sent to class 0x30c7c
2011-07-26 21:46:10.546 ngs[6133:b603] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '+[NGSFacebookAuthProvider accessToken]: unrecognized selector sent to class 0x30c7c'
*** Call stack at first throw:
(
0 CoreFoundation 0x00e825a9 __exceptionPreprocess + 185
1 libobjc.A.dylib 0x00fd6313 objc_exception_throw + 44
2 CoreFoundation 0x00e8417b +[NSObject(NSObject) doesNotRecognizeSelector:] + 187
3 CoreFoundation 0x00df3966 ___forwarding___ + 966
4 CoreFoundation 0x00df3522 _CF_forwarding_prep_0 + 50
5 ngs 0x0000324b -[RootViewController processConfig] + 731
6 ngs 0x000041a2 __33-[RootViewController viewDidLoad]_block_invoke_0 + 50
So my class implements the protocol. It successfully is assigned to id<NGSAuthProvider>. I tried contructing [[NGSFacebookAuthProvider alloc] init] explicitly instead of using Objection and it still crashed.
I tried looping through the selectors using objc/runtime.h methods to see which selectors are there but the only thing it finds is initialize:
- (void)logSelectors:(id)obj
{
int i=0;
unsigned int mc = 0;
Method * mlist = class_copyMethodList(object_getClass([obj class]), &mc);
NSLog(#"%d methods", mc);
for(i=0;i<mc;i++)
NSLog(#"Method no #%d: %s", i, sel_getName(method_getName(mlist[i])));
free(mlist);
}
This has to be something simple that I am missing. I use protocols defined by Cocoa and don't have this issue. I have defined custom protocols for UIViewController-based delegates without issue.
I am stumped as to why Obj-C runtime can't find my methods! If I change id<NGSAuthProvider> to NGSFacebookAuthProvider and construct it explicitly then it all works.
SOLUTION:
The problem was I misunderstood how to bind to a protocol. One way that works is:
#implementation NGSObjectionModule
- (void)configure
{
[self bind:[[[NGSFacebookAuthProvider alloc] init] autorelease] toProtocol:#protocol(NGSAuthProvider)];
}
#end
What I would like to do is bind a class to a protocol, but Objection probably wouldn't know the initializer to call?

The issue is you're trying to use the static class method (denoted because you've got a +) instead of a method run on an instance of your object (which is what you've written it as, with a -)

Chris,
You could use Objection's meta class bindings that allows you to bind a meta class to a protocol and invoke class methods against the meta class instance.
For example,
[self bindMetaClass:[NGSFacebookAuthProvider class] toProtocol:#protocol(NGSAuthProvider)];
But only if you want to use class methods. Otherwise you can use the protocol bindings against a share instance.

I also faced the same issue as I was using
[self bind:[MyClass class] toProtocol:#protocol(MyProtocol)];
The right way for it to work is
[self bindClass:[MyClass class] toProtocol:#protocol(MyProtocol)];

Related

Obj-C, How do I use a category to supply methods which I will use in delegate methods?

I want to provide methods used in several view controllers called in my delegate methods.
For example, I have some CloudKit functionality (I've added this to my own framework, but I don't think thats important), where I want to provide some crash logging.
Previosuly I had a crashLog function in each of my view controllers, which worked fine, but I have a lot of duplicate code.
Therefore I'd like to produce a category with these methods instead.
However I'm having difficulty getting my delegate methods to see these category methods.
Here's my code..
UIViewController+CloudKitDelegates.h
#interface UIViewController (CloudKitDelegates) <iCloudDBDelegate>
#property (weak,nonatomic) id<iCloudDBDelegate>iCloudDBDelegate;
-(void)crashLog:(NSString*)message, ...;
#end
UIViewController+CloudKitDelegates.m
#import "UIViewController+CloudKitDelegates.h"
#implementation UIViewController (CloudKitDelegates)
#dynamic iCloudDBDelegate;
-(void)crashLog:(NSString*)message, ...
{
va_list args;
va_start(args, message);
NSLog(#"%#", [[NSString alloc] initWithFormat:message arguments:args]);
va_end(args);
}
#end
h file - my calling view controller (e.g. My View Controller)
#import "UIViewController+CloudKitDelegates.h"
m file - delegate method
-(NSString*)getDBPath
{
[self.iCloudDBDelegate crashLog: #"testing"];
From this call I'm getting an error ...
'NSInvalidArgumentException', reason: '-[MyViewController crashLog:]:
unrecognized selector sent to instance
The error is showing that my calling view controller called MyViewController doesn't have the crashLog method, which I have in my category.
Any ideas where I'm going wrong ?
The problem: identical method crashLog: in multiple classes, for example
#interface ViewController : UIViewController
#end
#implementation ViewController
- (void)someMethod {
[self crashLog:#"error"];
}
-(void)crashLog:(NSString *)message {
NSLog(#"%#", message);
}
#end
Solution A: move crashLog: to a common superclass (or a category on superclass UIViewController)
#interface CommonViewController : UIViewController
-(void)crashLog:(NSString *)message;
#end
#implementation CommonViewController
-(void)crashLog:(NSString *)message {
NSLog(#"%#", message);
}
#end
#interface ViewController : CommonViewController
#end
#implementation ViewController
- (void)someMethod {
[self crashLog:#"error"];
}
#end
Solution B: move crashLog: to a delegate and protocol
#protocol ICloudDBDelegate
-(void)crashLog:(NSString *)message;
#end
#interface DelegateClass : AnyClass <ICloudDBDelegate>
#end
#implementation DelegateClass
-(void)crashLog:(NSString *)message {
NSLog(#"%#", message);
}
#end
#interface ViewController : UIViewController
#end
#implementation ViewController
#property (weak, nonatomic) id <ICloudDBDelegate> iCloudDBDelegate;
- (void)viewDidLoad
{
[super viewDidLoad];
AppDelegate *appDel = (AppDelegate *)[[UIApplication sharedApplication] delegate];
self.iCloudDBDelegate = appDel.iCloudDBDelegate;
}
- (void)someMethod {
[self.iCloudDBDelegate crashLog:#"error"];
}
#end
#interface AppDelegate : UIResponder <UIApplicationDelegate, AppDelProtocolDelegate, iCloudDBDelegate>
#property (strong, nonatomic) id<iCloudDBDelegate>iCloudDBDelegate;
#end
#implementation AppDelegate
- (id<iCloudDBDelegate>)iCloudDBDelegate {
if (!_iCloudDBDelegate) {
_iCloudDBDelegate = [[DelegateClass alloc] init];
}
return _iCloudDBDelegate;
}
#end
Now we have new problem: property iCloudDBDelegate in multiple classes
Solution B + A: move crashLog to a delegate, move iCloudDBDelegate property to a superclass
#protocol ICloudDBDelegate
-(void)crashLog:(NSString *)message;
#end
#interface DelegateClass : AnyClass <ICloudDBDelegate>
#end
#implementation DelegateClass
-(void)crashLog:(NSString *)message {
NSLog(#"%#", message);
}
#end
#interface CommonViewController : UIViewController
#property (weak, nonatomic) id <ICloudDBDelegate> iCloudDBDelegate;
#end
#implementation CommonViewController
#end
#interface ViewController : CommonViewController
#end
#implementation ViewController
- (void)someMethod {
[self.iCloudDBDelegate crashLog:#"error"];
}
#end
Solution C:
Another approach is a singleton object like NSUserDefaults.standardUserDefaults or NSFontManager.sharedFontManager: CloudDBManager.sharedCloudDBManager. No category or protocol required, just include CloudDBManager.h and use CloudDBManager.sharedCloudDBManager from everywhere.
#interface CloudDBManager : NSObject
#property(class, readonly, strong) CloudDBManager *sharedCloudDBManager;
-(void)crashLog:(NSString *)message;
#end
#implementation CloudDBManager
+ (CloudDBManager *)sharedCloudDBManager {
static CloudDBManager *sharedInstance = nil;
static dispatch_once_t onceToken = 0;
dispatch_once(&onceToken, ^{
sharedInstance = [[CloudDBManager alloc] init];
// Do any other initialisation stuff here
});
return sharedInstance;
}
-(void)crashLog:(NSString *)message {
NSLog(#"%#", message);
}
#end
#interface ViewController : CommonViewController
#end
#implementation ViewController
- (void)someMethod {
[CloudDBManager.sharedCloudDBManager crashLog:#"error"];
}
#end
(I've added this to my own framework, but I don't think thats important)
Yep, that's the typical problem. You've failed to include -ObjC in the link flags.
See Building Objective-C static libraries with categories. This applies to frameworks as well.
ObjC does not create linker symbols for methods. It can't, they're not resolved until runtime. So the category methods aren't seen by the linker as "missing" and it doesn't bother linking the relevant compile unit. This is an important optimization that keeps you from linking all of a massive C library just because you use one function in it, but Objective-C categories break some of the linker's assumptions. The compiler saw the definition (via the header), but the linker didn't care, so there's no error until runtime.
The -ObjC flag says "this C-looking compile unit is actually Objective-C; link all of it even if you don't think you need to."

No visible #interface for X declares the selector Y on a singleton

So I have a singleton like this:
#import "SCAppManager.h"
#implementation SCAppManager
+ (instancetype)sharedApplication {
static SCAppManager *sharedApplication = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
if (sharedApplication == nil) {
sharedApplication = [[SCAppManager alloc] init];
}
});
return sharedApplication;
}
+ (void)test {
NSLog(#"test");
}
#end
And its interface is this:
#import <Foundation/Foundation.h>
#interface SCAppManager : NSObject
+ (instancetype)sharedApplication;
+ (void)test;
#end
But when a try to use the [[SCAppManager sharedApplication] test]; in a ViewController, I get the error:
No visible #interface for 'SCAppManager' declares the selector 'test'
I've look for all the possibilities and I already imported my singleton class correclty and declared my method in the public interface. I also searched for some answers here, but all the fix didnt work for me.
Has anyone faced this problem?
Thanks!
The error is coming because you declared test as a class method not as instance method. You can call the class method by it's class name, so you should call it like:
[SCAppManager test];
Or change the method to instance method:
.h
#interface SCAppManager : NSObject
+ (instancetype)sharedApplication;
- (void)test;
#end
.m
#implementation SCAppManager
// Other methods
- (void)test
{
NSLog(#"test");
}
#end
And use it like:
[[SCAppManager sharedApplication] test];

Objective-C : Calling a property of a class in another

I'm using Cocos2d and I can not access properties of an object from another one.
Here I just want to get the hitpoints of a ship form a scene. It fails and returns an error : uncaught exception 'NSInvalidArgumentException', reason: '-[CCSprite hitpoints]: unrecognized selector...
As hitpoints is declared in the interface of the class Ship I can't figure out why.
The only thing I understand is that it's a inhéritance issue.
Let's show some code :
Ship.h
#import <Foundation/Foundation.h>
#import "cocos2d.h"
#interface Ship : CCSprite {
int hitpoints;
}
#property (nonatomic, assign) int hitpoints;
- (id)init;
#end
Then Ship.m
#import "Ship.h"
#implementation Ship
#synthesize hitpoints;
- (id)init
{
hitpoints = 3;
self = [CCSprite spriteWithImageNamed:#"ship.png"];
return self;
}
#end
In the Scene.m
#import "Ship.h"
#implementation Scene
{
Ship *_player;
}
- (id)init
{
_player = [[Ship alloc] init];
[self addChild:_player];
// ERROR HERE
NSLog(#"%s = %d", "_player hp", [_player hitpoints]);
}
Thank you.
I suspect the issue is with your init method; you shouldn't be accessing properties until the object is initialised and you should be calling [super initWith...] instead of the class creation method.
I would suggest the following changes:
Ship.h:
#interface Ship : CCSprite
#property (nonatomic, assign) int hitpoints;
#end
Ship.m:
#import "Ship.h"
#implementation Ship
#synthesize hitpoints;
- (id)init
{
self = [super initWithImageNamed:#"ship.png"];
if (self) {
self.hitpoints = 3;
}
return self;
}
#end
Always use object.property when referencing a property, even when object == self.
This:
- (id)init
Means "a method that returns any kind of object, which is called init".
This:
self = [CCSprite spriteWithImageNamed:#"ship.png"];
Means "create an instance of CCSprite". You then return that instance.
So _player ends up being an instance of CCSprite. CCSprite does not implement hitpoints so the exception is raised.
What are you actually trying to achieve? A subclass of CCSprite? trojanfoe has covered how to write a proper init for that. Things I think you need to know:
all classes look the same at runtime;
declaring the type of class pointers helps humans and the compiler to check your code but doesn't change the code generated;
some Objective-C patterns (probably most notably including class clusters) are built around init being able to return any kind of class — it's a useful feature, not a misfeature.

Passing data between objects/threads in Cocoa

I create a new thread from a controller like this:
[NSThread detachNewThreadSelector:#selector(makeMovie) toTarget:movieMaker withObject:nil];
What is the best way to call methods on the controller from the new thread/movieMaker object?
In this case, you probably want to use the delegate pattern. In your movie-maker class’s .h file:
#protocol MovieMakerDelegate;
#interface MovieMaker : NSObject
{
id<MovieMakerDelegate> delegate;
...
}
#property (nonatomic, assign) id<MovieMakerDelegate> delegate;
...
#end
#protocol MovieMakerDelegate <NSObject>
- (void)movieMaker:(MovieMaker *)maker didSomething:(id)result;
#end
...and in its .m:
#implementation MovieMaker
#synthesize delegate;
...
- (void)makeMovie
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
...
[self performSelectorOnMainThread:#selector(notifyDelegateOfDoingSomethingWithObject:) withObject:someObject waitUntilDone:YES];
[pool release];
}
- (void)notifyDelegateOfDoingSomethingWithObject:(id)someObject
{
[self.delegate movieMaker:self didSomething:someObject];
}
...
#end
Then in your controller’s header, declare that it supports the MovieMakerDelegate protocol, like this:
#interface MyController : Whatever <MovieMakerDelegate>
and in your .m, before calling your -detachNewThreadSelector:etc:, call movieMaker.delegate = self;. Implement the -movieMaker:didSomething: method on your controller class and you’re good to go.
You may pass the controller instance to your makeMovie method, with the withObject parameter.
[NSThread detachNewThreadSelector:#selector(makeMovie) toTarget:movieMaker withObject:self];
You can also use a singleton-like pattern, if applicable, to retrieve your controller instance from the thread.
Use performSelectorOnMainThread:withObject:waitUntilDone: (or similar performSelector:... methods), it is the easiest way.

Why does a readonly property still allow writing with KVC

I'm working through the "Key Value Coding" chapter in "Programming for Mac OS X". I've built an interface with a slider and a label, both bound to fido, an int. If I set the property for fido to readonly, moving the slider still causes the label to change it's value. I had assumed that I'd get some sort of error for this. If the property is readonly, how come the slider can still write to the property? I thought that it would have no setters created, and KVC wouldn't work. Thanks.
Here's the code I'm using:
#import <Cocoa/Cocoa.h>
#interface AppController : NSObject
{
int fido;
}
#property (readonly, assign) int fido;
#end
#import "AppController.h"
#implementation AppController
#synthesize fido;
- (id)init
{
[super init];
[self setValue:[NSNumber numberWithInt:5] forKey:#"fido"];
NSNumber *n = [self valueForKey:#"fido"];
NSLog(#"fido = %#", n);
return self;
}
#end
alt text http://idisk.me.com/nevan/Public/Pictures/Skitch/Window-20091001-174352.png
AppController.h:
#interface AppController : NSObject
{
int fido;
}
#property (readonly, assign) int fido;
#end
import "AppController.h"
#implementation AppController
#synthesize fido;
...
#end
At this point, you have declared that AppController has a -fido method and you have synthesized that method. There is no -setFido: method. So, why does the following "work"?
- (id)init
{
if (self=[super init]) {
[self setValue:[NSNumber numberWithInt:5] forKey:#"fido"];
NSNumber *n = [self valueForKey:#"fido"];
NSLog(#"fido = %#", n);
}
return self;
}
(BTW: I fixed your -init to implement the correct pattern)
This works because KVC follows a heuristic to set or get the value. The call to -setValue:forKey: first looks for -setFoo:. If not found, it then looks for the instance variable foo and sets it directly.
Note that if you change the instance variable fido to _fido, the set will work, but the valueForKey will return 0 as it calls the synthesized method (since I'm on 64 bit, the #synthesize synthesizes a fido instance variable. Confusing, I know.).
If you were to change the name of your ivar to bar and then use #synthesize foo=bar;, the code would fail at runtime.
You'll see:
2009-10-01 08:59:58.081 dfkjdfkjfjkfd[24099:903] *** Terminating app due to uncaught exception 'NSUnknownKeyException', reason: '[<AppController 0x20000e700> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key fido.'
*** Call stack at first throw:
(
0 CoreFoundation 0x00007fff85b055a4 __exceptionPreprocess + 180
1 libobjc.A.dylib 0x00007fff85c5a0f3 objc_exception_throw + 45
2 CoreFoundation 0x00007fff85b5caf9 -[NSException raise] + 9
3 Foundation 0x00007fff814e14f5 -[NSObject(NSKeyValueCoding) setValue:forKey:] + 434
(
0 CoreFoundation 0x00007fff85b055a4 __exceptionPreprocess + 180
1 libobjc.A.dylib 0x00007fff85c5a0f3 objc_exception_throw + 45
2 CoreFoundation 0x00007fff85b5caf9 -[NSException raise] + 9
3 Foundation 0x00007fff814e14f5 -[NSObject(NSKeyValueCoding) setValue:forKey:] + 434
4 dfkjdfkjfjkfd 0x0000000100000d96 -[AppController init] + 130
Having readonly property means that compiler won't generate you setter for that property. It's still legal to write to it via KVO/KVC.
The compiler directives #property and #synthesize are just shorthand ways to create the methods to get and set the variable in question.
The setter method created is named setFido:, and the getter method is just named fido.
When you specify readonly, I believe that simply tells the compiler not to create the setter method, but only the getter. It doesn't put any sort of barrier in the way of setting the variable by other means.
(Hope I've got all that right. Good luck!)