Short problem description
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
Detailed problem description
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:
- Category for a class that conforms to a protocol: This question seems to be asking the same thing, but didn't give specific examples, so it seems like it was misunderstood to mean something else.
- Defining categories for protocols in Objective-C?: Different example provided, so the accepted answer says to implement it in the category of each class that needs the method (i.e. you would end up with 12 methods in my example); which is a totally fine answer for that one, but not really a good solution for this problem.
- How do I define a category that adds methods to classes which implement a particular protocol?: The asker chose to go the route of checking for implementation of the protocol (and doing nothing if it doesn't conform), but what if you want it to spot errors at compile time?
@interface UIView<WritableView> (foo)
though. – Brander