Создание категории для классов, реализующих определенный протокол в 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"];

Это просто устанавливает для свойства text UILabel значение 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://stackoverflow.com"]; // same as: tableViewCell.textLabel.text = @"stackoverflow.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. Таким образом, логичным местом для размещения трех методов является категория на 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"];

Оно работает! Хаза! Все работает отлично ... пока я не попробую это:

[slider setTextToDomainOfUrl:@"http://apple.com"];

Код компилируется (поскольку UISlider наследуется от UIView), но не выполняется во время выполнения (поскольку UISlider не соответствует протоколу WritableView).

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

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


person Senseful    schedule 19.07.2013    source источник


Ответы (2)


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

Вот стратегия, которую я успешно использовал в моем проекте EnumeratorKit, который добавляет перечисление блоков в стиле Ruby. методы для встроенных классов коллекции Какао (в частности, _1 _ и 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
    

Надеюсь это поможет. Я обнаружил, что это действительно эффективный способ решения сквозных проблем без необходимости копировать и вставлять код между несколькими категориями.

person Adam Sharp    schedule 19.07.2013
comment
Потрясающий! Спасибо за подробный ответ. Хотелось бы, чтобы Apple сделала это так же просто, как @interface UIView<WritableView> (foo). - person Senseful; 21.07.2013
comment
@Senseful Да, не говоря уже о том, что в RubyMotion вы могли просто include WriteableView. Тем не менее, здорово, что среда выполнения вообще это позволяет! - person Adam Sharp; 23.07.2013
comment
У меня быстрый вопрос: в вашем коде вы, по сути, делаете методы доступными для всех экземпляров этого класса. Можно ли вставить поведение в конкретный экземпляр, то есть отправить реализацию метода или блока в экземпляр и заставить его выполнить это на себе? - person unom; 05.06.2014
comment
@unmircea Насколько мне известно, не существует поддерживаемого способа ограничения реализации метода конкретным экземпляром класса. Это немного похоже на то, как с помощью фреймворка mocking вы можете заглушить метод объекта - это то, о чем вы говорите? - person Adam Sharp; 05.06.2014
comment
Я хотел сделать это: stackoverflow.com/questions/23029297/ и получить мои IMP из таких блоков : stackoverflow.com/questions/1805578/ Основная причина для этого состоит в том, чтобы IMP блока принимали локальную контекстную переменную и указатель вместе с ними, но выполняли их на самих объектах, таким образом они узнают обо всем контекст, в котором они играют роль. Есть ли в этом смысл? - person unom; 05.06.2014

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, протокол < em> WritableView больше не отображается для Objective-C).

person Senseful    schedule 11.11.2015