Создание категории для классов, которые реализуют определенный протокол в 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. Правда, это не вызовет исключения во время выполнения, но и не вызовет ничего, что является еще одной ошибкой, которую я также пытаюсь избежать.

Подобные вопросы, на которые не были даны удовлетворительные ответы:

2 ответа

Решение

Похоже, что вы ищете, это миксин: определите серию методов, которые формируют поведение, которое вы хотите, а затем добавьте это поведение только к набору классов, которые в нем нуждаются.

Вот стратегия, которую я использовал с большим успехом в своем проекте EnumeratorKit, который добавляет методы перечисления блоков в стиле Ruby к встроенным классам коллекции Какао (в частности, EKEnumerable.h а также EKEnumerable.m:

  1. Определите протокол, который описывает поведение, которое вы хотите. Для реализаций методов, которые вы собираетесь предоставить, объявите их как @optional,

    @protocol WritableView <NSObject>
    
    - (void)writeText:(NSString *)text;
    
    @optional
    - (void)setTextToDomainOfUrl:(NSString *)text;
    - (void)setTextToIntegerValue:(NSInteger)value;
    - (void)setCapitalizedText:(NSString *)text;
    
    @end
    
  2. Создайте класс, который соответствует этому протоколу, и реализует все дополнительные методы:

    @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
    
  3. Создать категорию на 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):

  1. Принять WritableView протокол
  2. Реализовать необходимое writeText: метод экземпляра
  3. Добавьте это в начало вашей реализации:

    @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- С).

Другие вопросы по тегам