Создание категории для классов, которые реализуют определенный протокол в Objective-C?
Краткое описание проблемы
Могу ли я расширить UIView категорией, но работать только с подклассами, которые реализуют определенный протокол (WritableView
)?
Т.е. я могу сделать что-то вроде следующего?
@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
Подробное описание проблемы
Представьте, что я хочу добавить следующую функцию категории к любому экземпляру UILabel
:
[label setTextToDomainOfUrl:@"http://google.com"];
Который просто устанавливает UILabel's text
собственность на google.com
,
Одновременно я хочу иметь возможность вызывать эту функцию в нескольких других классах:
[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://stackru.com"]; // same as: tableViewCell.textLabel.text = @"stackru.com"
Допустим, я действительно доволен своим дизайном и хочу добавить еще 2 метода ко всем 4 классам:
[label setTextToIntegerValue:5] // same as: label.text = [NSString stringWithFormat:@"%d", 5];
[textField setCapitalizedText:@"abc"] // same as: textField.text = [@"abc" capitalizedString]
Итак, теперь у нас есть 4 класса по 3 метода в каждом. Если бы я действительно хотел сделать эту работу, мне нужно было бы написать 12 функций (4*3). Поскольку я добавляю больше функций, мне нужно реализовать их на каждом из моих подклассов, что может быстро стать очень сложным в обслуживании.
Вместо этого я хочу реализовать эти методы только один раз и просто предоставить новый метод категории для поддерживаемых компонентов, который называется writeText:
, Таким образом, вместо того, чтобы реализовывать 12 функций, я могу сократить число до 4 (по одному на каждый поддерживаемый компонент) + 3 (по одному на каждый доступный метод), чтобы в общей сложности было реализовано 7 методов.
Примечание: это глупые методы, используемые только в иллюстративных целях. Важной частью является то, что существует много методов (в данном случае 3), которые не должны дублировать свой код.
Мой первый шаг в попытке реализовать это - заметить, что первый общий предок этих 4 классов UIView
, Таким образом, логичное место для размещения 3 методов, по-видимому, в категории 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
Эти 3 метода будут работать до тех пор, пока текущий экземпляр UIView соответствует WritableView
протокол. Поэтому я расширяю свои 4 поддерживаемых класса следующим кодом:
@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
И теперь, когда я звоню следующее:
[label setTextToDomainOfUrl:@"http://apple.com"];
[tableViewCell setCapitalizedText:@"hello"];
Оно работает! Hazzah! Все работает отлично... пока я не попробую это:
[slider setTextToDomainOfUrl:@"http://apple.com"];
Код компилируется (так как UISlider
наследуется от UIView
), но терпит неудачу во время выполнения (так как UISlider
не соответствует WritableView
Протокол).
Что я действительно хотел бы сделать, это сделать эти 3 метода доступными только для тех UIViews, которые имеют writeText:
метод реализован (т.е. те UIViews, которые реализуют WritableView
протокол я настроил). В идеале я бы определил свою категорию в UIView следующим образом:
@interface UIView<WritableView> (foo) // SYNTAX ERROR
- (void)setTextToDomainOfUrl:(NSString *)text;
- (void)setTextToIntegerValue:(NSInteger)value;
- (void)setCapitalizedText:(NSString *)text;
@end
Идея состоит в том, что если бы это был правильный синтаксис, это сделало бы [slider setTextToDomainOfUrl:@"http://apple.com"]
сбой во время компиляции (так как UISlider
никогда не реализует WritableView
протокол), но это сделало бы все мои другие примеры успешными.
Итак, мой вопрос: есть ли способ расширить класс категорией, но ограничить его только теми подклассами, которые реализовали определенный протокол?
Я понимаю, что могу изменить утверждение (которое проверяет соответствие протоколу) на if
утверждение, но это все равно позволит скомпилировать ошибочную строку UISlider. Правда, это не вызовет исключения во время выполнения, но и не вызовет ничего, что является еще одной ошибкой, которую я также пытаюсь избежать.
Подобные вопросы, на которые не были даны удовлетворительные ответы:
- Категория для класса, который соответствует протоколу: этот вопрос, кажется, задает то же самое, но не приводит конкретных примеров, поэтому кажется, что его неправильно поняли как нечто другое.
- Определение категорий для протоколов в Objective-C?: Приведен другой пример, поэтому принятый ответ говорит о том, что он должен быть реализован в категории каждого класса, которому нужен метод (т.е. в моем примере вы получите 12 методов); что является вполне хорошим ответом на этот вопрос, но не очень хорошим решением для этой проблемы.
- Как определить категорию, которая добавляет методы к классам, которые реализуют определенный протокол? Ответчик решил пойти по пути проверки реализации протокола (и ничего не делать, если он не соответствует), но что, если вы хотите, чтобы он обнаруживал ошибки во время компиляции?
2 ответа
Похоже, что вы ищете, это миксин: определите серию методов, которые формируют поведение, которое вы хотите, а затем добавьте это поведение только к набору классов, которые в нем нуждаются.
Вот стратегия, которую я использовал с большим успехом в своем проекте EnumeratorKit, который добавляет методы перечисления блоков в стиле Ruby к встроенным классам коллекции Какао (в частности, EKEnumerable.h
а также EKEnumerable.m
:
Определите протокол, который описывает поведение, которое вы хотите. Для реализаций методов, которые вы собираетесь предоставить, объявите их как
@optional
,@protocol WritableView <NSObject> - (void)writeText:(NSString *)text; @optional - (void)setTextToDomainOfUrl:(NSString *)text; - (void)setTextToIntegerValue:(NSInteger)value; - (void)setCapitalizedText:(NSString *)text; @end
Создайте класс, который соответствует этому протоколу, и реализует все дополнительные методы:
@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
Создать категорию на
NSObject
который может добавить эти методы к любому другому классу во время выполнения (обратите внимание, что этот код не поддерживает методы класса, только методы экземпляра):#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
Теперь в классе, где вы хотите включить это поведение (например, UILabel
):
- Принять
WritableView
протокол - Реализовать необходимое
writeText:
метод экземпляра Добавьте это в начало вашей реализации:
@interface UILabel (WritableView) <WritableView> @end @implementation UILabel (WritableView) + (void)load { [self includeWritableView]; } // implementation specific to UILabel - (void)writeText:(NSString *)text { self.text = text; } @end
Надеюсь это поможет. Я считаю, что это действительно эффективный способ реализации сквозных задач без необходимости копировать и вставлять код между несколькими категориями.
Swift 2.0 представляет расширения протокола, который именно то, что я искал. Если бы я только использовал Swift, я смог бы достичь желаемых результатов с помощью следующего кода:
protocol WritableView {
func writeText(text: String)
}
extension WritableView {
func setTextToDomainOfUrl(text: String) {
let t = text.stringByReplacingOccurrencesOfString("http://", withString:"") // just an example, obviously this can be improved
writeText(t)
}
func setTextToIntegerValue(value: Int) {
writeText("\(value)")
}
func setCapitalizedText(text: String) {
writeText(text.capitalizedString)
}
}
extension UILabel: WritableView {
func writeText(text: String) {
self.text = text
}
}
extension UIButton: WritableView {
fun writeText(text: String) {
setTitle(text, forState:.Normal)
}
}
К сожалению, в моих ограниченных тестах со Swift и Objective-C похоже, что вы не можете использовать расширения протокола Swift в Objective-C (например, в тот момент, когда я выбираю расширение протокола WritableView в Swift, протокол WritableView больше не виден для Objective- С).