Question

I am experimenting in adding functionality to my UIViews (configuring CALayers according to state) by setting up a NSProxy subclass to stand in for any UIView I choose. Here's what I've tried:

In my NSProxy subclass, I have the following code:

#pragma mark Initialization / Dealloc

- (id)initWithView:(UIView *)view
{
    delegate = view;
    [delegate retain];

    return self;
}

- (void)dealloc
{
    [delegate release];
    [super dealloc];
}


#pragma mark Proxy Methods

- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    [anInvocation setTarget:delegate];
    [anInvocation invoke];
    return;
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    return [delegate methodSignatureForSelector:aSelector];
}

- (BOOL)respondsToSelector:(SEL)aSelector 
{
    BOOL rv = NO;

    if ([delegate respondsToSelector:aSelector]) { rv = YES; }

    return rv;
}

And, using my NSProxy subclass this way:

UILabel *label = [[HFMultiStateProxy alloc] initWithView:[[[UILabel alloc] initWithFrame:cellFrame] autorelease]];
label.text = text;
label.font = font;
label.textAlignment = UITextAlignmentCenter;
label.backgroundColor = [UIColor clearColor];
label.opaque = NO;

[self addSubview:label];

Seems to work until I hit the addSubview: line.

Turning message tracing on ( instrumentObjcMessageSends(YES); ) shows the forwarding for each of the previous messages working until deep inside of the addSubview:, where this series of method calls show up in the log (the first message shown here was invoked via the proxy):

- UILabel UIView _makeSubtreePerformSelector:withObject:
- UILabel UIView _makeSubtreePerformSelector:withObject:withObject:copySublayers:
- CALayer CALayer sublayers
- NSMethodSignature NSMethodSignature methodReturnType
- NSMethodSignature NSMethodSignature _argInfo:
- NSMethodSignature NSMethodSignature _frameDescriptor
+ UILabel NSObject resolveInstanceMethod:
- UILabel NSObject forwardingTargetForSelector:
- UILabel NSObject forwardingTargetForSelector:
- UILabel NSObject methodSignatureForSelector:
- UILabel NSObject methodSignatureForSelector:
- UILabel NSObject class
- UILabel NSObject doesNotRecognizeSelector:

And I get the following error:

2011-02-20 16:38:52.048 FlashClass_dbg[22035:207] -[UILabel superlayer]: unrecognized selector sent to instance 0x757d470

if I do not use an NSProxy subclass and instead use a UILabel subclass (HFMultiStateLabel), it works fine. Here is the message trace that occurs once addSubview: is called (HFNoteNameControl is the superview of the label):

- HFNoteNameControl UIView addSubview:
- HFNoteNameControl UIView _addSubview:positioned:relativeTo:
- HFMultiStateLabel UIView superview
- HFMultiStateLabel UIView window
- HFNoteNameControl NSObject isKindOfClass:
- HFNoteNameControl NSObject class
- HFNoteNameControl UIView window
- UIWindow NSObject isKindOfClass:
- UIWindow NSObject class
- HFNoteNameControl UIView _shouldTryPromoteDescendantToFirstResponder
- HFMultiStateLabel UIView _isAncestorOfFirstResponder
- HFMultiStateLabel UIView _willMoveToWindow:withAncestorView:
- HFMultiStateLabel UIView _willMoveToWindow:
- HFMultiStateLabel UIView willMoveToWindow:
- HFMultiStateLabel UIView _makeSubtreePerformSelector:withObject:withObject:copySublayers:
- CALayer CALayer sublayers
- HFMultiStateLabel UIView willMoveToSuperview:
- HFMultiStateLabel UIView _unsubscribeToScrollNotificationsIfNecessary:
- HFMultiStateLabel UIView _makeSubtreePerformSelector:withObject:
- HFMultiStateLabel UIView _makeSubtreePerformSelector:withObject:withObject:copySublayers:
- CALayer CALayer sublayers
- CALayer CALayer superlayer

I can verify that each of the methods up until -superlayer are called successfully when using NSProxy. For some reason, with the NSProxy, superlayer on UILabel is being called instead of CALayer. Perhaps somewhere something gets confused and UILabel is inserted into the sublayers instead of its CALayer?

Am I missing something?

Does the UIKit do some sort of optimizations that bypass the normal mechanism that NSProxy hooks into?

Other thoughts?

Thanks!

Henry

PS I have only tried this in the Simulator, not the device. Would that behavior be any different?

Was it helpful?

Solution 2

I gave up trying. I've come to the conclusion that NSProxy is such an underused object that it's potential for uses beyond Apple examples has not been fully explored nor debugged. In short, I believe that NSProxy is not ready to be used as a generic way to extend an object's functionality without subclassing or adding a category.

In the old days, I would have used a poseAsClass call to implement my desired functionality.

My solution ended up something like this:

  • I added a category to UIView that added additional properties. These property implementations forwarded their set & get messages to a "addOn" property of the UIView that I also put into the category. The default value of this "addOn" property in the UIView's category implementation is, of course, nil. (I could have implemented a static hash table to enable associating an AddOn instance for any UIView, but it struck me as a risky ploy to manage with the retain counts properly.)

  • The "AddOn" class had extra code in it to directly manipulate the UIView, and it added extra drawing code in it.

  • For each type of UIView that I wanted to add this added functionality, I had to subclass it with code that: a) Created an instance method and corresponding property code for the "AddOn" class b) Subclassed any functions I covered to give the "AddOn" code a chance to add its functionality.

  • Each of these subclasses has essentially the same code in it to forward the desired functionality to the AddOn instance.

SO, I ended up minimizing code duplication as much as I could, but each of the UIView's descendant subclasses that enable use of the the "AddOn" functionality ends up duplicating code.

It appears that I could have further minimized code duplication by using class method manipulation functions, but that learning curve and further obfuscation of the code deterred me from following that path.

OTHER TIPS

I was trying to solve the same issue - use NSProxy with UIView (in my case UITableViewCell) when I encountered this problem. I logged all calls to the console:

...
App[2857:c07] MyHeaderCell: --- method signature for: _unsubscribeToScrollNotificationsIfNecessary:
App[2857:c07] MyHeaderCell: --- _unsubscribeToScrollNotificationsIfNecessary:
App[2857:c07] MyHeaderCell: --- method signature for: _makeSubtreePerformSelector:withObject:
App[2857:c07] MyHeaderCell: --- _makeSubtreePerformSelector:withObject:
App[2857:c07] +[MyHeaderCell superlayer]: unrecognized selector sent to class 0x1331f8c
App[2857:c07] CRASH: +[SMSHeaderCell superlayer]: unrecognized selector sent to class 0x1331f8c
App[2857:c07] Stack Trace:...

It crashes on the unrecognized selector exception.

Normally, the object is asked the - (NSMethodSignature *)methodSignatureForSelector:(SEL)sel method first and when that is returned, it invokes the - (void) forwardInvocation:(NSInvocation *)invocation in the proxy. This way we can redirect the messages. If there is not NSMethodSignature returned, the doesNotRecognizeSelector: method is called on the object. So we get even unrecognized selector calls.

This works for instance methods, but this crash is caused by a class method, which we have no power over - the object itself is not called (the class is). I wanted to force the runtime to call my proxy class even for class methods by overriding the getter of my NSProxy subclass

- (Class) class
{
    return _myRealClass;
}

Which did not work. So NSProxy is not enough to do this. Right now I'm trying to use the NSObject instead of NSProxy to achieve all the desired behavior and since NSObject has the + (BOOL)resolveClassMethod:(SEL)sel method which might be useful. I will edit this post once I found out if NSObject is better suited for this.

//Edit

It seems that the problem is that with NSProxy, superlayer is being called on UIView instead of CALayer. Proof: http://t2523.codeinpro.us/q/508112244f1eba38a4fb032e

So it really seems like a UIKit shortcut problem - they're not sending a regular message call (speed optimization I would guess).

Anyways, this I am searching for a way to get around this now.

I have never tried using NSProxy with views, but I have done something similar by using a custom view class to display another view. Maybe the system requires an actual view and not a proxy object. There are two ways you could use a "proxy" view:

  1. Make the proxied view a subview of the proxy view. The proxy would take the frame, autoresizing mask, etc. from the proxied view, then add the proxied view as its subview and set its frame to be the proxy view's bounds, and its autoresizing mask so that it always fills the proxy view. When the proxied view is removed, any settings are copied back into it from the proxy view. Any properties not copied into the proxy are passed to the proxied view using forwarding.

  2. The proxy view passes almost every message to the proxied view. The proxy view does not override the lock/unlockFocus, display, etc. methods. It overrides drawRect: to call drawRect: on the proxied view.

After trying the same thing, and searched for the error (which got me here), I tried to circumvent the problems... It wasn't pretty.

Identifying the root problem was easy. Somewhere in the framework, Apple is using direct pointer access to the variables in UIView subclasses. If you check the headers, the variables are declared with @package access identifier.

What I basically tried was:

  1. Create a proxy class at runtime with ivars copied from the UIView class definition, and then set the values of these pointers to the objects in the UIView. Couldn't get far there.

  2. Declare just the CALayer * in the proxy subclass, and only copy that pointer from the protected UIView instance. Worked, but I think it was buggy? It didn't work with auto layout at all, though, so I decided to move away from that solution.

The code I tried can be found in the RTLSegmentedControl repo under the proxy-pattern branch

I also wrote a blog post about the details.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top