views:

93

answers:

2

I created a class that wraps a UITextView and adds some ui elements. I want the new class' API to be identical with UITextView, so I use message forwarding (listing below) to relay messages between the wrapped text view and the delegate.

The irritating thing is that the compiler issues warnings for method invocations on instances of my forwarding class. For example, an error will be generated for the following line:

[aMyTextView setContentOffset:CGPointZero animated:YES];

So I am forced to declare and create "manually forwarding" implementations for these methods, which defeats the whole purpose of using message forwarding.

- (void)setContentOffset:(CGPoint)contentOffset animated:(BOOL)animated
{
    [_textView setContentOffset:contentOffset animated:animated];
}

I know the usual way of getting around this is to use one of the performSelector: methods, but this is a) cumbersome when some arguments are not NSObjects (although Erica Sadun's extensions are a big help), b) again, completely contrary to the intention of creating a transparent wrapper.

(Subclassing UITextView is also out of the question, because I need to insert views below the text view.)

Is there a way to get around this?

Listing of all relevant parts of the class:

@interface MyTextField : UIView<UITextViewDelegate>
{
    UIImageView*                        _border;
    UITextView*                         _textView;
    UIButton*                           _clearButton;
    NSObject<UITextViewDelegate>*       _delegate;
}

@implementation MWTextField
. . . 
// Forwards messages in both directions (textView <--> delegate)
#pragma mark Message forwarding

// Protocol messages will only be sent if respondsToSelector: returns YES
- (BOOL)respondsToSelector:(SEL)aSelector
{
    if ([_delegate respondsToSelector:aSelector])
        return YES;
    else
        return [super respondsToSelector:aSelector];
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector
{
    // First, try to find it in the UITextView
    if ([_textView respondsToSelector:selector])
    {
        return [_textView methodSignatureForSelector:selector];
    }
    // Then try the delegate
    else if ([_delegate respondsToSelector:selector])
    {
        return [_delegate methodSignatureForSelector:selector];
    }
    else
    {
        return [super methodSignatureForSelector: selector];
    }
}

- (void)forwardInvocation:(NSInvocation *)invocation
{
    SEL aSelector = [invocation selector];

    if ([_textView respondsToSelector:aSelector])
    {
        [invocation invokeWithTarget:_textView];
    }
    else if ([_delegate respondsToSelector:aSelector])
    {
        [invocation invokeWithTarget:_delegate];
    }
    else
    {
        [self doesNotRecognizeSelector:aSelector];
    }
}
. . .
@end
+1  A: 

Declare a category that provides the method declarations of the methods you are forwarding:

@interface MyTextField (ImGonnaLetYouFinishButFirstImForwardingThese)
... methods you want to forward here ...
@end

No need to provide an @implementation.

Note that this is a fairly atypical pattern. Not fairly, very. There should be no reason why you can't subclass. You say Subclassing UITextView is also out of the question, because I need to insert views below the text view, but that isn't true.


If I subclass UITextField, all I can do with the other views is to add them as subviews, which means they will be on top of the text field. I guess I could modify drawRect:... Is that what you would suggest? Or what do you have up your sleeve there?

If you need a group, create a UIView subclass that manages the various subviews appropriately, no forwarding necessary. Then you can order the views however you like.

Forwarding is used extremely rarely. Down that path lies madness. It really sounds like your design is a bit in the weeds, but there isn't enough information to really say anything more specific.

bbum
Your solution: nice, but would be even nicer not having to declare the methods, but simply forward them, whatever they are.Subclassing: I want to group the actual text field with some elements, at least one of which must appear *under* the text field. If I subclass UITextField, all I can do with the other views is to add them as subviews, which means they will be on top of the text field. I guess I could modify drawRect:... Is that what you would suggest? Or what do you have up your sleeve there?
Felixyz
bbum
You're right of course. If I make these warnings go, then all others will also go away, which wouldn't be good. So your protocol solution seems to be the best. A subclass wouldn't be able to put a view under (in terms of drawing order) itself, because that view would be a subview, and as such always layered above the parent view. Right?
Felixyz
I think I see where they are going with this. They want a composite control that has the interface of and behaves like a UITextField but actually has other controls that are visually outside the bounds of the text field. I would think to have the text field be the top container view would make it hard to have the visual elements of the text field be drawn in a sub-area of the bounds.I think providing a screenshot of your current implementation might help show the issue.
Jon Steinmetz
But ultimately I think the category method is your best bet. The dynamic forwarding of method calls happens at runtime. The compiler checking happens at compile time so there is no opportunity that I am aware of to dynamically tell the compiler what methods your class responds to.
Jon Steinmetz
@Jon It's not that the other views are "visually outside the bounds". The issue is that they have to be "below" the text field in the drawing order (along the z-axis, as it were). @bbum "If you need a group, create a UIView subclass that manages the various subviews appropriately..." That's exactly what I did. However, code that uses this class will expect it to behave exactly like a UITextView, so I'd have to write a lot of glue (forwarding every method invocation to UITextView and its super classes). I used message forwarding to avoid doing that.
Felixyz
Sorry if I was not being clear but that is what I was trying to convey. The other controls need to be outside the associated edit field but within the same enclosing view to make them behave as a coherent composite control. It seems to me you are doing the right thing and that using the category is likely the only way to avoid the warnings. Best of luck.
Jon Steinmetz
Forwarding is still the wrong answer... or, at least, is not the simplest answer and is fraught with fragility.
bbum
A: 

Simple solution is to use dynamic typing and declare aMyTextView as id, the warnings will go away.

zoul
Correct, but runs the risk of failing at runtime if the wrong method is sent. Personally, I'd still like to know why OP thinks subclassing is impossible. :)
bbum