【问题标题】:Creating a category for classes that implement a specific protocol in Objective-C?为在 Objective-C 中实现特定协议的类创建一个类别?
【发布时间】:2013-07-19 06:38:30
【问题描述】:

问题简述

我可以用一个类别扩展 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: 方法的 UIViews(即那些实现了我设置的 WritableView 协议的 UIViews)。理想情况下,我会在 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 objective-c-category objective-c-protocol


    【解决方案1】:

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

    这是我在我的项目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
      

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

    【讨论】:

    • 太棒了!感谢您的详细回答。我希望 Apple 让它像 @interface UIView&lt;WritableView&gt; (foo) 一样简单。
    • @Senseful 是的,更不用说在 RubyMotion 中你可以只使用include WriteableView。尽管如此,运行时允许它仍然很棒!
    • 我有一个简单的问题:在您的代码中,您基本上使该类的所有实例都可以使用这些方法。是否可以将行为“粘贴”到特定实例,即将方法或块的实现发送到实例并让它自己执行?
    • @unmircea 据我所知,没有支持将方法实现限制为类的特定实例的方法。听起来有点像使用模拟框架你可以在对象上存根方法——你说的是那种东西吗?
    • 我希望这样做:stackoverflow.com/questions/23029297/… 并从这样的块中获取 IMP:stackoverflow.com/questions/1805578/… 这样做的主要原因是让块 IMP 使用本地上下文变量和指针,但是在对象本身上执行它们,这样它们就会意识到它们所扮演角色的整个上下文。这有意义吗?
    【解决方案2】:

    Swift 2.0 引入了Protocol Extensions,这正是我想要的。如果我只是使用 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 可见)。

    【讨论】:

      猜你喜欢
      • 2017-03-19
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-08-23
      • 1970-01-01
      相关资源
      最近更新 更多