Can I extend UIView with a category, but have it only work on subclasses that implement a specific protocol (WritableView
)?
I.e. can I do something like the following?
@interface UIView<WritableView> (foo) // SYNTAX ERROR
- (void)setTextToDomainOfUrl:(NSString *)text;
- (void)setTextToIntegerValue:(NSInteger)value;
- (void)setCapitalizedText:(NSString *)text;
@end
@implementation UIView<WritableView> (foo)
// implementation of 3 above methods would go here
@end
Imagine I want the following category function added to any instance of UILabel
:
[label setTextToDomainOfUrl:@"http://google.com"];
Which simply sets a UILabel's text
property to google.com
.
Simlarly, I want to be able to call this function on several other classes:
[button setTextToDomainOfUrl:@"http://apple.com"]; // same as: [button setTitle:@"apple.com" forState:UIControlStateNormal];
[textField setTextToDomainOfUrl:@"http://example.com"]; // same as: textField.text = @"example.com"
[tableViewCell setTextToDomainOfUrl:@"http://stackoverflow.com"]; // same as: tableViewCell.textLabel.text = @"stackoverflow.com"
Let's say I'm really happy with my design so far, and I want to add 2 more methods to all 4 classes:
[label setTextToIntegerValue:5] // same as: label.text = [NSString stringWithFormat:@"%d", 5];
[textField setCapitalizedText:@"abc"] // same as: textField.text = [@"abc" capitalizedString]
So now we have 4 classes that have 3 methods each. If I wanted to actually make this work, I would need to write 12 functions (4*3). As I add more functions, I need to implement them on each of my subclasses, which can quickly become very hard to maintain.
Instead, I want to implement these methods only once, and simply expose a new category method on the supported components called writeText:
. This way instead of having to implement 12 functions, I can cut the number down to 4 (one for each supported component) + 3 (one for each method available) for a total of 7 methods that need to be implemented.
Note: These are silly methods, used just for illustrative purposes. The important part is that there are many methods (in this case 3), which shouldn't have their code duplicated.
My first step at trying to implement this is noticing that the first common ancestor of these 4 classes is UIView
. Therefore, the logical place to put the 3 methods seems to be in a category on UIView
:
@interface UIView (foo)
- (void)setTextToDomainOfUrl:(NSString *)text;
- (void)setTextToIntegerValue:(NSInteger)value;
- (void)setCapitalizedText:(NSString *)text;
@end
@implementation UIView (foo)
- (void)setTextToDomainOfUrl:(NSString *)text {
text = [text stringByReplacingOccurrencesOfString:@"http://" withString:@""]; // just an example, obviously this can be improved
// ... implement more code to strip everything else out of the string
NSAssert([self conformsToProtocol:@protocol(WritableView)], @"Must conform to protocol");
[(id<WritableView>)self writeText:text];
}
- (void)setTextToIntegerValue:(NSInteger)value {
NSAssert([self conformsToProtocol:@protocol(WritableView)], @"Must conform to protocol");
[(id<WritableView>)self writeText:[NSString stringWithFormat:@"%d", value]];
}
- (void)setCapitalizedText:(NSString *)text {
NSAssert([self conformsToProtocol:@protocol(WritableView)], @"Must conform to protocol");
[(id<WritableView>)self writeText:[text capitalizedString]];
}
@end
These 3 methods will work as long as the current instance of UIView conforms to the WritableView
protocol. So I extend my 4 supported classes with the following code:
@protocol WritableView <NSObject>
- (void)writeText:(NSString *)text;
@end
@interface UILabel (foo)<WritableView>
@end
@implementation UILabel (foo)
- (void)writeText:(NSString *)text {
self.text = text;
}
@end
@interface UIButton (foo)<WritableView>
@end
@implementation UIButton (foo)
- (void)writeText:(NSString *)text {
[self setTitle:text forState:UIControlStateNormal];
}
@end
// similar code for UITextField and UITableViewCell omitted
And now when I call the following:
[label setTextToDomainOfUrl:@"http://apple.com"];
[tableViewCell setCapitalizedText:@"hello"];
It works! Hazzah! Everything works perfectly... until I try this:
[slider setTextToDomainOfUrl:@"http://apple.com"];
The code compiles (since UISlider
inherits from UIView
), but fails at run time (since UISlider
doesn't conform to the WritableView
protocol).
What I would really like to do is make these 3 methods only available to those UIViews which have a writeText:
method implemented (I.e. those UIViews which implement the WritableView
protocol I set up). Ideally, I would define my category on UIView like the following:
@interface UIView<WritableView> (foo) // SYNTAX ERROR
- (void)setTextToDomainOfUrl:(NSString *)text;
- (void)setTextToIntegerValue:(NSInteger)value;
- (void)setCapitalizedText:(NSString *)text;
@end
The idea is that if this were valid syntax, it would make [slider setTextToDomainOfUrl:@"http://apple.com"]
fail at compile time (since UISlider
never implements the WritableView
protocol), but it would make all my other examples succeed.
So my question is: is there any way to extend a class with a category, but limit it to only those subclasses that have implemented a specific protocol?
I realize I could change the assertion (which checks that it conforms to the protocol) to an if
statement, but that would still allow the buggy UISlider line to compile. True, it won't cause an exception at runtime, but it won't cause anything to happen either, which is another kind of error I am also trying to avoid.
Similar questions that haven't been given satisfactory answers:
It sounds like what you're after is a mixin: define a series of methods that form the behaviour that you want, and then add that behaviour to only the set of classes that need it.
Here is a strategy I've used to great success in my project EnumeratorKit, which adds Ruby-style block enumeration methods to built-in Cocoa collection classes (in particular EKEnumerable.h
and EKEnumerable.m
:
Define a protocol that describes the behaviour you want. For method implementations you are going to provide, declare them as @optional
.
@protocol WritableView <NSObject>
- (void)writeText:(NSString *)text;
@optional
- (void)setTextToDomainOfUrl:(NSString *)text;
- (void)setTextToIntegerValue:(NSInteger)value;
- (void)setCapitalizedText:(NSString *)text;
@end
Create a class that conforms to that protocol, and implements all the optional methods:
@interface WritableView : NSObject <WritableView>
@end
@implementation WritableView
- (void)writeText:(NSString *)text
{
NSAssert(@"expected -writeText: to be implemented by %@", [self class]);
}
- (void)setTextToDomainOfUrl:(NSString *)text
{
// implementation will call [self writeText:text]
}
- (void)setTextToIntegerValue:(NSInteger)value
{
// implementation will call [self writeText:text]
}
- (void)setCapitalizedText:(NSString *)text
{
// implementation will call [self writeText:text]
}
@end
Create a category on NSObject
that can add these methods to any other class at runtime (note that this code doesn't support class methods, only instance methods):
#import <objc/runtime.h>
@interface NSObject (IncludeWritableView)
+ (void)includeWritableView;
@end
@implementation
+ (void)includeWritableView
{
unsigned int methodCount;
Method *methods = class_copyMethodList([WritableView class], &methodCount);
for (int i = 0; i < methodCount; i++) {
SEL name = method_getName(methods[i]);
IMP imp = method_getImplementation(methods[i]);
const char *types = method_getTypeEncoding(methods[i]);
class_addMethod([self class], name, imp, types);
}
free(methods);
}
@end
Now in the class where you want to include this behaviour (for example, UILabel
):
WritableView
protocolwriteText:
instance methodAdd this to the top of your implementation:
@interface UILabel (WritableView) <WritableView>
@end
@implementation UILabel (WritableView)
+ (void)load
{
[self includeWritableView];
}
// implementation specific to UILabel
- (void)writeText:(NSString *)text
{
self.text = text;
}
@end
Hope this helps. I've found it a really effective way to implement cross-cutting concerns without having to copy & paste code between multiple categories.