9

简短的问题描述

我可以用一个类别扩展 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 的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://stackoverflow.com"]; // same as: tableViewCell.textLabel.text = @"stackoverflow.com"

假设到目前为止我对我的设计非常满意,并且我想为所有 4 个类添加另外 2 个方法:

[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:. 通过这种方式,我可以将数量减少到 4(每个支持的组件一个)+ 3(每个可用的方法一个),而不是实现 12 个函数,总共需要实现 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    

只要当前 UIView 的实例符合WritableView协议,这 3 个方法就可以工作。因此,我使用以下代码扩展了我的 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 种方法只对那些writeText:实现了方法的 UIView 可用(即那些实现WritableView我设置的协议的 UIView)。理想情况下,我会在 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 行。没错,它不会在运行时导致异常,但也不会导致任何事情发生,这是我也试图避免的另一种错误。

尚未得到满意答案的类似问题:

4

2 回答 2

8

听起来您所追求的是一个混合:定义一系列形成您想要的行为的方法,然后将该行为仅添加到需要它的一组类中。

这是我在项目EnumeratorKit中取得巨大成功的一个策略,它将 Ruby 样式的块枚举方法添加​​到内置的 Cocoa 集合类(特别是EKEnumerable.hEKEnumerable.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
    

希望这可以帮助。我发现它是一种非常有效的方式来实现横切关注点,而无需在多个类别之间复制和粘贴代码。

于 2013-07-19T07:40:39.247 回答
1

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 的有限测试中,您似乎无法在 Objective-C 中使用 Swift 协议扩展(例如,当我选择在 Swift中扩展协议WritableView时,协议WritableView不再对 Objective-可见- C)。

于 2015-11-11T18:25:20.993 回答