Message forwarding does not work on particular messages - ios

I've found out that some Obj-C messages do no get properly forwaded when an object does not explicity handle them.
Here's an example:
Create a new UITableView like:
UITableView *v ? [[UITableView alloc] initWithFrame:CGRectMake(0, 0, 0, 0) style:UITableViewStylePlain];
Then create an instance of an NSObject with the following methods:
(A) - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
NSLog(#"tableView:numberOfRowsInSection:");
return 1;
}
(B) - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
NSLog(#"tableView:cellForRowAtIndexPath:");
return nil;
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSLog(#"methodSignatureForSelector: %#", NSStringFromSelector(aSelector));
return [NSMethodSignature signatureWithObjCTypes:"##:#"];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
NSLog(#"forwardInvocation: %#", NSStringFromSelector(anInvocation.selector));
}
Let's call this object ds and set it as the dataSource for the table view:
v.dataSource = ds;
And when you run your code you should see that both methods (A) and (B) get called by the table view in the expected order:
LOG: tableView:numberOfRowsInSection:
LOG: tableView:cellForRowAtIndexPath:
Now it will get interesting, try removing (or just change the name of) method (A), and you should get:
LOG: methodSignatureForSelector: tableView:numberOfRowsInSection:
LOG: forwardInvocation: tableView:numberOfRowsInSection:
Which is fine because the table view tries to call the tableView:numberOfRowsInSection: and since the method is not there, the message gets forwarded to forwardInvocation:
Apparently, this is the default behavior for all messages that reference a method that is not defined at an object.
However try restoring the (A) method and removing (B), and you should get:
LOG: tableView:numberOfRowsInSection:
... and an error telling you that tableView:cellForRowAtIndexPath: was called but didn't return a cell.
For some reason tableView:cellForRowAtIndexPath: gets called (or not) but without following the expected forwardInvocation: pathway.
Is this a bug?
Am I doing something wrong?
Are there some messages that explicitly do not get forwarded?
Even though respondsToSelector: should return FALSE to both functions, somewhere in the implementation sends the message for cellForRowAtIndexPath: to some other object and breaks the default expected behavior...
What I've learned is basically:
If you are planning to respond to a message in your object, even through some implementation of forwardInvocation:, you should ALWAYS overload the respondsToSelector: method and return TRUE for all messages you plan to implement in your object.

The following is my hypothesis based on some experimentation:
The difference is that the table view doesn't attempt to call cellForRowAtIndexPath if it isn't defined, whereas it does attempt to call numberOfRowsInSection. Therefore, you only see the forwarding mechanism with numberOfRowsInSection. The error telling you a cell wasn't returned is just poorly worded and slightly misleading.
I came to this conclusion by observing that, if the data source doesn't define cellForRowAtIndexPath, the implementation on UITableViewController gets called instead. So, the table view is clearly checking for existence. Therefore, it stands to reason that what happens is something like this:
Call on dataSource if defined
Otherwise, call on UITableViewController if defined
Otherwise, don't call anything
If (3) is reached, then somewhere downstream an error is thrown because the table view's internal cell variable is nil.

The runtime system forwards messages only if it's unable to find a match for the given selector in the target class or any of its ancestors. However if the runtime system's search for the selector succeeds, it then invokes the method's implementation directly -- no need for forwarding.
By the way, if the lookup fails, the runtime system calls forwardingTargetForSelector: first; it then calls forwardInvocation: only if forwardingTargetForSelector: returns either nil or self.

Related

IOS/Objective-C: Trace precise place of exception

I am getting an exception when I load a view controller. The error message suggests the program is trying to send length to NSNull as follows:
[NSNull length]: unrecognized selector sent to instance 0x3b1cda70
(lldb)
The last log statement prior to the exception is at the end of a method and from what I can tell although a value is null that it returns, this method is not the issue.
- (NSString *)pickerView:(UIPickerView *)pickerView
titleForRow:(NSInteger)row
forComponent:(NSInteger)component
{
NSLog(#"in titleforrow");
NSLog(#"about to return firsts[row]%#",firsts[row]);//this is null
return firsts[row];
}
This suggests the problem is in some other method. So I have put log statements before every use of length in the view controller class (all of which are in a save method that should not be called on loading) and, in fact, none of these log statements appear in the log suggesting that length is never sent.
Nonetheless, the program is throwing the exception. I tried adding an exception breakpoint as suggested in this answer
While it lets me view the threads, I still can't figure out what is throwing the exception.
Would appreciate any suggestions on where to go from here.

isKindOfClass returning different values for Test Cases

I have a method someMethod. This method, at some point has the following if-else condition.
- (void) someMethod {
// ... some more code ...
if ([userArray[0] isKindOfClass:[Me class]]) {
// some code
}
else {
// some other code
}
}
Now this if-condition is always met when I execute the code normally. But when I call it from one of my test-cases, the else-part gets executed instead. I am calling this method exactly the same way (it has no side-effects, etc).
When I debugged the thing in both normal run, and testing run. I saw something different.
While running in Test, the userArray had 1 object, (Me_Me_2 *)0x00007fa61d39dbf0.
And while running it normally, the userArray had the same object, but there was one difference. It said (Me_Me_ *)0x00007fce71459ae0.
When I print the value of NSStringFromClass([userArray[0] class]), they both print "Me".
"Me" is a NSManagedObject.
Another interesting thing is, if I add an expression in the debugger and evaluate it, it always evaluates to true - ([((NSObject*)userArray[0]) isKindOfClass:[Me class]]) returns (bool)true. This is totally bizarre! If the condition is true, why does it ever go into the else block?
Now some questions -
What is happening over here? Are Core Data objects treated different when running in tests?
Why is the type of the object "Me_Me_2" while testing and "Me_Me_" otherwise? Why is it not just "Me"?
This sounds similar to the following issue: isKindOfClass doesn't work as expected
In short, is the class being compared a target member of the test target? It should only be a target member of the application.

NSProxy and forwardInvocation: invoke called within a block causes nil return value

I am using a NSProxy subclass and forwardInvocation: for capturing calls to my Backend API object (a shared instance).
Some Background information:
I want to capture the API calls so I can check everytime if I have to refresh my authentication token. If yes I just perform the refresh before.
The method parameters (of invocation) contain blocks.
Some simplified code:
- (void)forwardInvocation:(NSInvocation *)invocation {
[invocation setTarget:self.realAPI];
[invocation retainArguments];
// Perform refresh call and forward invocation after
// successfully refreshed
if (authenticationRefreshNeeded) {
[self.realAPI refreshWithBlock:^(NSObject *someObject) {
[invocation invokeWithTarget:self.realAPI];
}];
}
// Otherwise we just forward the invocation immediately
else {
[invocation invokeWithTarget:self.realAPI];
}
return;
}
I am already calling retainArguments so my blocks and other parameters don't get lost because of the late execution of invokeWithTarget: (refreshWithBlock: makes an async API call).
Everything works fine so far - BUT:
The return value of invocation is always nil when invokeWithTarget: is performed within the refresh block. Is there any way to retain the return value (like the arguments)?
Any hints? Suggestions?
Update
As response to #quellish:
The problem is that the return value is of type NSURLSessionDataTask (that I use to show an activity indicator) which I read directly after making the call. But the proxy does not forward the call immediately so the return value is not there - of course (I was blind).
What would be a possible workaround? Can I return a placeholder value or how can I know as caller when the method gets invoked so I can retrieve the return value later?
To perform an operation when your invocation is complete, passing the result:
if (authenticationRefreshNeeded) {
[self.realAPI refreshWithBlock:^(NSObject *someObject) {
NSURLSessionDataTask *resultTask = nil;
[invocation invokeWithTarget:self.realAPI];
[invocation getReturnValue:&resultTask];
if (completion != nil){
completion(resultTask);
}
}];
}
Where completion() is a block that takes an NSURLSessionDataTask as a parameter. Blocks can be used as callbacks, which make them well suited to what you are trying to do ("when I'm done, do this() ") Ideally, this would have been passed into the method containing the above - but since this is forwardInvocation: , that gets a little more... challenging. You could set it as a property on this proxy object and read it from there.
Another approach would be to extend UIApplication with a category or informal protocol with a method like addDataTask: which you could call instead of your block, which would hand off responsbility for the "i just added a data task" to another receiver, most likely the application's delegate (and you can extend the UIApplicationDelegate protocol with a new method, application:didAddDataTask: to handle this). It sounds like your data task and activity indicator are application-level concerns, which may make this a good fit.
That said, I have some experience with almost exactly the problems you are trying to solve (token based authorization). I would suggest taking at a look at how ACAccountStore approaches this problem , it may offer some ideas for alternative implementations.

forwardInvocation to nil object

I have an object (A) that needs to work as a proxy to an other object. there is also a condition that when verified should make the object work as nil.
I've implemented:
-(void)forwardInvocation:(NSInvocation *)anInvocation
{
if (condition)
[anInvocation invokeWithTarget:self.object];
else
[anInvocation invokeWithTarget:nil];
}
but it's not enough. when the condition is satisfied and a method is called on A unrecognized selector sent to instance is raised.
From the documentation on invoke in NSInvocation's class reference:
You must set the receiver’s target, selector, and argument values before calling this method.
If you haven't set target (or have set it to nil, which is basically the same thing), you shouldn't be invoking the invocation. If you want to mimic the behaviour of sending a message to nil, you can just return 0/nil - this is the default behaviour of objc_msgSend if the target is nil. If you're interested in seeing why this works, you can have a look here for an overview of objc_msgSend's implementation (fair warning, it's all in assembly).

Table View Controller delegate is nil, but should be a valid reference

I'm designing a simple Grocery List app, but having trouble adding a Food entity to a List entity.
Sequence of events: ListDetailTVC -> AddFoodToListTVC -> type in food name -> save
at this point I want to see this food in the list, but instead nothing happens. The screen stays the same. After some delving into I realized the delegate was nil in AddFoodToListTVC, so there was no screen to return to after 'save' was pressed.
Then I tried manually setting the delegate with the following code:
`if(delegate == nil)
{
self.delegate = (ListDetailTVC *)[[UIApplication sharedApplication] delegate];
}
NSLog(#"delegate = %#", delegate);
`
This sets the AddFoodToListTVC delegate to something, but I'm not sure how to verify Im setting it correctly. When I run my app with the above addition, I get this error when I click save.
"Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[AppDelegate theSaveButtonOnTheAddFoodToListTVCWasTapped:]: unrecognized selector sent to instance 0x6b56610' "
0x6b56610 in this case is the address of the delegate I am manually setting.
I know that's not much info, but I can supply any relevant code. Im at my wits end, how do I set the delegate for AddFoodToListTVC correctly?
It looks like your app delegate does not implement this method: theSaveButtonOnTheAddFoodToListTVCWasTapped:
Delegates are something like event triggers which are received by delegated object... like rowWasSelected is an event which needs to be handled by someone for the mandatory methods....
Now the thing is ur setting AppDelegate as the receiver object for delegates [self.delegate = (ListDetailTVC *)[[UIApplication sharedApplication] delegate];]...
But as AppDelegate doesnt handle those events, you get an exception [unrecognized selector]...
So first thing is declare protocol i.e., UITableViewDelegate, UITableViewDataSource... [protocols are like interface which has only declaraion not implementation, you need to provide implementation] Set tableViews DataSource and Delegate and implement those protocols. Will solve your problem

Resources