我该如何在Objective-C中创建代理?

774

我知道代理(delegates)的工作原理,也知道如何使用它们。

但是,我该如何创建它们?


20个回答

918

Objective-C代理是分配给另一个对象的delegate属性的对象。要创建一个代理,您需要定义一个实现您感兴趣的代理方法的类,并将该类标记为实现代理协议。

例如,假设您有一个UIWebView。如果您想实现其代理的webViewDidStartLoad:方法,您可以创建一个如下所示的类:

@interface MyClass<UIWebViewDelegate>
// ...
@end

@implementation MyClass
- (void)webViewDidStartLoad:(UIWebView *)webView { 
    // ... 
}
@end

然后您可以创建一个 MyClass 的实例并将其分配为 web view 的委托:

MyClass *instanceOfMyClass = [[MyClass alloc] init];
myWebView.delegate = instanceOfMyClass;

UIWebView方面,它可能有类似以下代码来查看委托是否响应webViewDidStartLoad:消息,使用respondsToSelector:并在适当时发送它。
if([self.delegate respondsToSelector:@selector(webViewDidStartLoad:)]) {
    [self.delegate webViewDidStartLoad:self];
}

委托属性本身通常被声明为weak(在ARC中)或assign(在ARC之前),以避免保留循环,因为对象的委托通常持有对该对象的强引用。(例如,视图控制器通常是其所包含视图的委托。)

为您的类创建委托

要定义自己的委托,您需要在某个地方声明它们的方法,如苹果关于协议的文档中所讨论的那样。通常您会声明一个正式协议。该声明,摘自UIWebView.h,看起来像这样:

@protocol UIWebViewDelegate <NSObject>
@optional
- (void)webViewDidStartLoad:(UIWebView *)webView;
// ... other methods here
@end

这类似于接口或抽象基类,因为它为您的委托创建了一个特殊类型,在这种情况下是UIWebViewDelegate。委托实现者必须采用此协议:

@interface MyClass <UIWebViewDelegate>
// ...
@end

然后在协议中实现方法。对于在协议中声明为@optional(像大多数委托方法一样)的方法,在调用特定方法之前,您需要使用-respondsToSelector:进行检查。

命名

委托方法通常以委托类名称开头,并将委托对象作为第一个参数。它们通常使用will-、should-或did-形式。例如,webViewDidStartLoad:(第一个参数是Web视图),而不是loadStarted(不带参数)。

速度优化

我们可以在设置委托时缓存信息,而不是每次想要发送消息时都检查委托是否响应选择器。其中一种非常干净的方法是使用位域,如下所示:

@protocol SomethingDelegate <NSObject>
@optional
- (void)something:(id)something didFinishLoadingItem:(id)item;
- (void)something:(id)something didFailWithError:(NSError *)error;
@end

@interface Something : NSObject
@property (nonatomic, weak) id <SomethingDelegate> delegate;
@end

@implementation Something {
  struct {
    unsigned int didFinishLoadingItem:1;
    unsigned int didFailWithError:1;
  } delegateRespondsTo;
}
@synthesize delegate;

- (void)setDelegate:(id <SomethingDelegate>)aDelegate {
  if (delegate != aDelegate) {
    delegate = aDelegate;

    delegateRespondsTo.didFinishLoadingItem = [delegate respondsToSelector:@selector(something:didFinishLoadingItem:)];
    delegateRespondsTo.didFailWithError = [delegate respondsToSelector:@selector(something:didFailWithError:)];
  }
}
@end

然后,在代码主体中,我们可以通过访问delegateRespondsTo结构体来检查我们的代理处理消息的情况,而不是一遍又一遍地发送-respondsToSelector:

非正式代理

在协议出现之前,通常使用NSObject上的category声明代理可以实现的方法。例如,CALayer仍在使用此方法:

@interface NSObject(CALayerDelegate)
- (void)displayLayer:(CALayer *)layer;
// ... other methods here
@end

这会告诉编译器任何对象都可能实现displayLayer:方法。之后,您可以使用与上述描述相同的-respondsToSelector:方法来调用此方法。代理实现此方法并分配delegate属性,就这样(无需声明您符合协议)。该方法在苹果库中很常见,但新代码应使用上面更现代的协议方法,因为该方法会污染NSObject(使自动完成不太有用),并且使编译器难以警告您有关拼写错误和类似错误。

我认为你需要将 unsigned int 类型转换为 BOOL,因为 delegate respondsToSelector 的返回值是 BOOL 类型。 - Roland
委托能像C++中的多态一样用于多态吗? - user4657588
@Dan 是的,当然。协议通常用于多态性。 - Jesse Rusak
@JesseRusak 我认为为了保持一致性,“JSSomethingDelegate”应该改为“SomethingDelegate” :) - Hans Knöchel

397

已批准的答案很好,但如果您想要一分钟的答案,请尝试以下内容:

我的MyClass.h文件应该像这样(使用注释添加委托行!)

#import <BlaClass/BlaClass.h>

@class MyClass;             //define class, so protocol can see MyClass
@protocol MyClassDelegate <NSObject>   //define delegate protocol
    - (void) myClassDelegateMethod: (MyClass *) sender;  //define delegate method to be implemented within another class
@end //end protocol

@interface MyClass : NSObject {
}
@property (nonatomic, weak) id <MyClassDelegate> delegate; //define MyClassDelegate as delegate

@end

我的MyClass.m文件应该长成这样

#import "MyClass.h"
@implementation MyClass 
@synthesize delegate; //synthesise  MyClassDelegate delegate

- (void) myMethodToDoStuff {
    [self.delegate myClassDelegateMethod:self]; //this will call the method implemented in your other class    
}

@end

要在另一个类中使用您的委托(在本例中为名为MyVC的UIViewController),请在MyVC.h文件中进行如下设置:

#import "MyClass.h"
@interface MyVC:UIViewController <MyClassDelegate> { //make it a delegate for MyClassDelegate
}

MyVC.m:

myClass.delegate = self;          //set its delegate to self somewhere

实现代理方法

- (void) myClassDelegateMethod: (MyClass *) sender {
    NSLog(@"Delegates are great!");
}

4
很高兴你将这个答案作为快速参考。但是为什么MyClass.h中的delegate属性标记为'IBOutlet'? - Arno van der Meer
4
@ArnovanderMeer 好发现!我记不得为什么要加这个了。虽然在我的项目中需要,但在这个示例中不需要,我已经删掉了。谢谢。 - Tibidabo
谢谢。虽然被接受的答案非常好和详细,但我更喜欢一些简洁的示例代码来学习。有两个答案是很好的。 - sudo
@Tibidabo 真是太棒了。我真希望每个人都能像你这样清晰地解释编程概念。多年来,我已经看过数百种关于“委托”(delegates)的解释,但直到现在我才真正掌握了这个理论!非常感谢你... - Charles Robertson
7
在MyVC.m文件中,myClass被实例化在哪里? - Lane Rettig

18

当使用正式协议方法创建委托支持时,我发现您可以通过添加以下内容来确保正确的类型检查(虽然是运行时而不是编译时):

if (![delegate conformsToProtocol:@protocol(MyDelegate)]) {
    [NSException raise:@"MyDelegate Exception"
                format:@"Parameter does not conform to MyDelegate protocol at line %d", (int)__LINE__];
}

在你的委托访问器(setDelegate)代码中。这可以帮助最小化错误。


17

也许这更符合您缺少的内容:

如果您是从C++的角度来看,委托需要一些时间来适应-但基本上“它们只是工作的”。

工作方式是,您将编写的某个对象设置为NSWindow的代理对象,但是您的对象仅具有一种或少数几种可能的代理方法的实现(方法)。因此发生了某些事情,NSWindow想要调用您的对象-它只是使用Objective-c的respondsToSelector方法来确定您的对象是否希望调用该方法,然后调用它。这就是objective-c的工作方式-按需查找方法。

使用自己的对象完全没有什么特别之处,非常简单,例如,您可以拥有27个对象的NSArray,所有这些对象都是不同类型的对象,仅有18个对象具有-(void)setToBue;方法。其他9个没有。因此,要在其中的18个对象上调用setToBlue,类似于以下内容:

for (id anObject in myArray)
{
  if ([anObject respondsToSelector:@selector(@"setToBlue")])
     [anObject setToBlue]; 
}

另一个关于委托的事情是,它们不会被保留,因此您总是要在您的MyClass dealloc方法中将委托设置为nil


16
请看下面的简单教程,了解iOS中Delegate是如何工作的。

iOS中的Delegate

我创建了两个ViewController(用于从一个ViewController向另一个ViewController发送数据)

  1. FirstViewController 实现了代理方法 (提供数据)。
  2. SecondViewController 声明了代理 (接收数据)。

15

作为苹果推荐的良好实践,代理(根据定义是协议)最好符合NSObject协议。

@protocol MyDelegate <NSObject>
    ...
@end

使用@optional注解可以创建在委托中可选的方法(即不一定需要实现的方法)。示例如下:

& to create optional methods within your delegate (i.e. methods which need not necessarily be implemented), you can use the @optional annotation like this :


@protocol MyDelegate <NSObject>
    ...
    ...
      // Declaration for Methods that 'must' be implemented'
    ...
    ...
    @optional
    ...
      // Declaration for Methods that 'need not necessarily' be implemented by the class conforming to your delegate
    ...
@end

因此,在使用您指定为可选的方法时,您需要在您的类中使用 respondsToSelector 检查视图(符合您的委托)是否实际实现了您的可选方法。


11

Swift版本

代理是一个为另一个类做一些工作的类。阅读以下代码,这是一个有点傻但希望能启发您的Playground示例,展示了在Swift中如何实现此功能。

// A protocol is just a list of methods (and/or properties) that must
// be used by any class that adopts the protocol.
protocol OlderSiblingDelegate: class {
    // This protocol only defines one required method
    func getYourNiceOlderSiblingAGlassOfWater() -> String
}

class BossyBigBrother {
    
    // The delegate is the BossyBigBrother's slave. This position can 
    // be assigned later to whoever is available (and conforms to the 
    // protocol).
    weak var delegate: OlderSiblingDelegate?
    
    func tellSomebodyToGetMeSomeWater() -> String? {
        // The delegate is optional because there might not be anyone
        // nearby to boss around.
        return delegate?.getYourNiceOlderSiblingAGlassOfWater()
    }
}

// PoorLittleSister conforms to the OlderSiblingDelegate protocol
class PoorLittleSister: OlderSiblingDelegate {

    // This method is repquired by the protocol, but the protocol said
    // nothing about how it needs to be implemented.
    func getYourNiceOlderSiblingAGlassOfWater() -> String {
        return "Go get it yourself!"
    }
    
}

// initialize the classes
let bigBro = BossyBigBrother()
let lilSis = PoorLittleSister()

// Set the delegate 
// bigBro could boss around anyone who conforms to the 
// OlderSiblingDelegate protocol, but since lilSis is here, 
// she is the unlucky choice.
bigBro.delegate = lilSis

// Because the delegate is set, there is a class to do bigBro's work for him.
// bigBro tells lilSis to get him some water.
if let replyFromLilSis = bigBro.tellSomebodyToGetMeSomeWater() {
    print(replyFromLilSis) // "Go get it yourself!"
}

在实际应用中,代理通常用于以下情况:

  1. 当一个类需要向另一个类传递一些信息时
  2. 当一个类想要允许另一个类对它进行自定义时

这些类之间事先不需要了解彼此的任何信息,只需代理类符合所需协议即可。

我强烈推荐阅读以下两篇文章。它们帮助我更好地理解代理,甚至比文档还要好。


11

我认为一旦您理解委托,所有这些答案都有很多道理。就个人而言,我来自C/C++之地,以前使用的是如Fortran等过程化语言,因此在C++范例中寻找类似的模拟品的时间不超过2分钟。

如果要向C++/Java程序员解释委托,我会说:

什么是委托? 这些是指向另一个类内部的类的静态指针。一旦分配了指针,您可以调用该类中的函数/方法。因此,您的类的某些函数被“委托”(在C++世界中,由类对象指针指向)到另一个类。

什么是协议? 概念上,它的目的与您分配为委托类的类的头文件类似。协议是明确定义需要在将指针设置为类委托的类中实现的方法的方式。

如何在C ++中做类似的事情? 如果您尝试在C ++中执行此操作,则需要在类定义中定义对类(对象)的指针,然后将它们连接到提供其他功能作为基类的委托的其他类。但是,此连接需要在代码中进行维护,会很笨拙且容易出错。Objective-C仅假定程序员不善于维护此纪律,并提供编译器限制以强制执行清洁实现。


你所谈论的是语义,而我所说的是直觉。你所谈论的是虚函数——但仅仅为了适应新术语可能会有些挑战。这个答案适用于想在C++/C中思考并行的初学者。 - DrBug
你说的并不是很清楚。为什么不写一篇新的回复,看看是否更多人会觉得有用,并投赞成票呢? - DrBug

10

好的,这并不是对问题的真正答案,但如果您正在查找如何制作自己的代理,那么可能有一些更简单的答案适合您。

我很少实现我的代理,因为我很少需要。我可以为委托对象拥有仅一个代理。因此,如果您希望将委托用于单向通信/传递数据,则使用通知会更好。

NSNotification 可以将对象传递给多个接收者,而且非常容易使用。 它的工作原理如下:

MyClass.m 文件应该像这样:

#import "MyClass.h"
@implementation MyClass 

- (void) myMethodToDoStuff {
//this will post a notification with myClassData (NSArray in this case)  in its userInfo dict and self as an object
[[NSNotificationCenter defaultCenter] postNotificationName:@"myClassUpdatedData"
                                                    object:self
                                                  userInfo:[NSDictionary dictionaryWithObject:selectedLocation[@"myClassData"] forKey:@"myClassData"]];
}
@end

要在其他类中使用您的通知: 将该类添加为观察者:

[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(otherClassUpdatedItsData:) name:@"myClassUpdatedData" object:nil];

实现选择器:

- (void) otherClassUpdatedItsData:(NSNotification *)note {
    NSLog(@"*** Other class updated its data ***");
    MyClass *otherClass = [note object];  //the object itself, you can call back any selector if you want
    NSArray *otherClassData = [note userInfo][@"myClassData"]; //get myClass data object and do whatever you want with it
}

不要忘记在不需要观察时将自己作为观察者的类删除。

- (void)dealloc
{
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}

8
假设您有一个已开发好的类,并希望声明一个代理属性,以便在某个事件发生时能够通知它:
@class myClass;

@protocol myClassDelegate <NSObject>

-(void)myClass:(MyClass*)myObject requiredEventHandlerWithParameter:(ParamType*)param;

@optional
-(void)myClass:(MyClass*)myObject optionalEventHandlerWithParameter:(ParamType*)param;

@end


@interface MyClass : NSObject

@property(nonatomic,weak)id< MyClassDelegate> delegate;

@end

所以你需要在MyClass头文件(或单独的头文件)中声明一个协议,并声明必须/可选的事件处理程序,你的代理必须/应该实现。然后在MyClass中声明一个类型为id<MyClassDelegate>的属性,这意味着任何符合协议MyClassDelegate的Objective-C类。你会注意到代理属性被声明为弱引用,这非常重要,可以防止保留循环(通常代理会保留MyClass实例,所以如果将代理声明为保留,则它们两个将互相保留,而且永远不会释放)。
你还会注意到,协议方法将MyClass实例作为参数传递给代理,这是最佳实践,以防代理想要调用MyClass实例上的一些方法,并且在代理声明自己为多个MyClass实例的MyClassDelegate时也有帮助,例如当您在ViewController中有多个UITableView实例并向所有实例声明自己为UITableViewDelegate时。
MyClass内部,你可以按照以下方式使用声明的事件通知代理:
if([_delegate respondsToSelector:@selector(myClass: requiredEventHandlerWithParameter:)])
{
     [_delegate myClass:self requiredEventHandlerWithParameter:(ParamType*)param];
}

在调用委托方法之前,您首先要检查委托是否响应该协议方法,以防委托没有实现它并且应用程序将崩溃(即使该协议方法是必需的)。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接