委托是 Apple 的框架里面使用廣泛的模式,同時它是一個重要的 四人幫的書“設計模式”中的模式。委托模式是單向的,消息的發送方(委托方)需要知道接收方(委托),反過來就不是了。對象之間沒有多少耦合,因為發送方只要知道它的委托實現了對應的 protocol。
本質上,委托模式只需要委托提供一些回調方法,就是說委托實現了一系列空返回值的方法。
不幸的是 Apple 的 API 并沒有尊重這個原則,開發者也效仿 Apple 進入了歧途。一個典型的例子是 [UITableViewDelegate](https://developer.apple.com/library/ios/documentation/uikit/reference/UITableViewDelegate_Protocol/Reference/Reference.html) 協議。
一些有 void 返回類型的方法就像回調
~~~
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath;
- (void)tableView:(UITableView *)tableView didHighlightRowAtIndexPath:(NSIndexPath *)indexPath;
~~~
但是其他的不是
~~~
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath;
- (BOOL)tableView:(UITableView *)tableView canPerformAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender;
~~~
當委托者詢問委托對象一些信息的時候,這就暗示著信息是從委托對象流向委托者,而不會反過來。 這個概念就和委托模式有些不同,它是一個另外的模式:數據源。
可能有人會說 Apple 有一個 [UITableViewDataSouce](https://developer.apple.com/library/ios/documentation/uikit/reference/UITableViewDataSource_Protocol/Reference/Reference.html) protocol 來做這個(雖然使用委托模式的名字),但是實際上它的方法是用來提供真實的數據應該如何被展示的信息的。
~~~
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath;
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView;
~~~
此外,以上兩個方法 Apple 混合了展示層和數據層,這顯的非常糟糕,但是很少的開發者感到糟糕。而且我們在這里把空返回值和非空返回值的方法都天真地叫做委托方法。
為了分離概念,我們應該這樣做:
- 委托模式:事件發生的時候,委托者需要通知委托
- 數據源模式: 委托方需要從數據源對象拉取數據
這個是實際的例子:
~~~
@class ZOCSignUpViewController;
@protocol ZOCSignUpViewControllerDelegate <NSObject>
- (void)signUpViewControllerDidPressSignUpButton:(ZOCSignUpViewController *)controller;
@end
@protocol ZOCSignUpViewControllerDataSource <NSObject>
- (ZOCUserCredentials *)credentialsForSignUpViewController:(ZOCSignUpViewController *)controller;
@end
@protocol ZOCSignUpViewControllerDataSource <NSObject>
@interface ZOCSignUpViewController : UIViewController
@property (nonatomic, weak) id<ZOCSignUpViewControllerDelegate> delegate;
@property (nonatomic, weak) id<ZOCSignUpViewControllerDataSource> dataSource;
@end
~~~
在上面的例子里面,委托方法需要總是有一個調用方作為第一個參數,否則委托對象可能被不能區別不同的委托者的實例。此外,如果調用者沒有被傳遞到委托對象,那么就沒有辦法讓一個委托對象處理兩個不同的委托者了。所以,下面這樣的方法就是人神共憤的:
~~~
- (void)calculatorDidCalculateValue:(CGFloat)value;
~~~
默認情況下,委托對象需要實現 protocol 的方法。可以用`@required` 和 `@optional` 關鍵字來標記方法是否是必要的還是可選的。
~~~
@protocol ZOCSignUpViewControllerDelegate <NSObject>
@required
- (void)signUpViewController:(ZOCSignUpViewController *)controller didProvideSignUpInfo:(NSDictionary *);
@optional
- (void)signUpViewControllerDidPressSignUpButton:(ZOCSignUpViewController *)controller;
@end
~~~
對于可選的方法,委托者必須在發送消息前檢查委托是否確實實現了特定的方法(否則會Crash):
~~~
if ([self.delegate respondsToSelector:@selector(signUpViewControllerDidPressSignUpButton:)]) {
[self.delegate signUpViewControllerDidPressSignUpButton:self];
}
~~~
## 繼承
有時候你可能需要重載委托方法。考慮有兩個 UIViewController 子類的情況:UIViewControllerA 和 UIViewControllerB,有下面的類繼承關系。
`UIViewControllerB < UIViewControllerA < UIViewController`
`UIViewControllerA` conforms to `UITableViewDelegate` and implements `- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath`.
`UIViewControllerA` 遵從 `UITableViewDelegate` 并且實現了 `- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath`.
你可能會想要提供一個和 `UIViewControllerB` 不同的實現。一個實現可能是這樣子的:
~~~
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
CGFloat retVal = 0;
if ([super respondsToSelector:@selector(tableView:heightForRowAtIndexPath:)]) {
retVal = [super tableView:self.tableView heightForRowAtIndexPath:indexPath];
}
return retVal + 10.0f;
}
~~~
但是如果超類(`UIViewControllerA`)沒有實現這個方法呢?
調用過程
~~~
[super respondsToSelector:@selector(tableView:heightForRowAtIndexPath:)]
~~~
會用 NSObject 的實現,尋找,在 `self` 的上下文中無疑有它的實現,但是 app 會在下一行 Crash 并且報下面的錯:
~~~
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[UIViewControllerB tableView:heightForRowAtIndexPath:]: unrecognized selector sent to instance 0x8d82820'
~~~
這種情況下我們需要來詢問特定的類實例是否可以響應對應的 selector。下面的代碼提供了一個小技巧:
~~~
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
CGFloat retVal = 0;
if ([[UIViewControllerA class] instancesRespondToSelector:@selector(tableView:heightForRowAtIndexPath:)]) {
retVal = [super tableView:self.tableView heightForRowAtIndexPath:indexPath];
}
return retVal + 10.0f;
}
~~~
就像上面的丑陋的代碼,一個委托方法也比重載方法好。
## 多重委托
多重委托是一個非常基礎的概念,但是,大多數開發者對此非常不熟悉而使用 NSNotifications。就像你可能注意到的,委托和數據源是對象之間的通訊模式,但是只涉及兩個對象:委托者和委托。
數據源模式強制一對一的關系,發送者來像一個并且只是一個對象來請求信息。但是委托模式不一樣,它可以完美得有多個委托來等待回調操作。
至少兩個對象需要接收來自特定委托者的回調,并且后一個需要知道所有的委托,這個方法更好的適用于分布式系統并且更加廣泛用于大多數軟件的復雜信息流傳遞。
多重委托可以用很多方式實現,讀者當然喜歡找到一個好的個人實現,一個非常靈巧的多重委托實現可以參考 Luca Bernardi 在他的 [LBDelegateMatrioska](https://github.com/lukabernardi/LBDelegateMatrioska) 的原理。
一個基本的實現在下面給出。Cocoa 在數據結構中使用弱引用來避免引用循環,我們使用一個類來作為委托者持有委托對象的弱引用。
~~~
@interface ZOCWeakObject : NSObject
@property (nonatomic, weak, readonly) id object;
+ (instancetype)weakObjectWithObject:(id)object;
- (instancetype)initWithObject:(id)object;
@end
~~~
~~~
@interface ZOCWeakObject ()
@property (nonatomic, weak) id object;
@end
@implementation ZOCWeakObject
+ (instancetype)weakObjectWithObject:(id)object {
return [[[self class] alloc] initWithObject:object];
}
- (instancetype)initWithObject:(id)object {
if ((self = [super init])) {
_object = object;
}
return self;
}
- (BOOL)isEqual:(id)object {
if (self == object) {
return YES;
}
if (![object isKindOfClass:[object class]]) {
return NO;
}
return [self isEqualToWeakObject:(ZOCWeakObject *)object];
}
- (BOOL)isEqualToWeakObject:(ZOCWeakObject *)object {
if (!object) {
return NO;
}
BOOL objectsMatch = [self.object isEqual:object.object];
return objectsMatch;
}
- (NSUInteger)hash {
return [self.object hash];
}
@end
~~~
一個簡單的使用 weak 對象來完成多重引用的組成部分:
~~~
@protocol ZOCServiceDelegate <NSObject>
@optional
- (void)generalService:(ZOCGeneralService *)service didRetrieveEntries:(NSArray *)entries;
@end
@interface ZOCGeneralService : NSObject
- (void)registerDelegate:(id<ZOCServiceDelegate>)delegate;
- (void)deregisterDelegate:(id<ZOCServiceDelegate>)delegate;
@end
@interface ZOCGeneralService ()
@property (nonatomic, strong) NSMutableSet *delegates;
@end
~~~
~~~
@implementation ZOCGeneralService
- (void)registerDelegate:(id<ZOCServiceDelegate>)delegate {
if ([delegate conformsToProtocol:@protocol(ZOCServiceDelegate)]) {
[self.delegates addObject:[[ZOCWeakObject alloc] initWithObject:delegate]];
}
}
- (void)deregisterDelegate:(id<ZOCServiceDelegate>)delegate {
if ([delegate conformsToProtocol:@protocol(ZOCServiceDelegate)]) {
[self.delegates removeObject:[[ZOCWeakObject alloc] initWithObject:delegate]];
}
}
- (void)_notifyDelegates {
...
for (ZOCWeakObject *object in self.delegates) {
if (object.object) {
if ([object.object respondsToSelector:@selector(generalService:didRetrieveEntries:)]) {
[object.object generalService:self didRetrieveEntries:entries];
}
}
}
}
@end
~~~
在 `registerDelegate:` 和 `deregisterDelegate:` 方法的幫助下,連接/解除組成部分很簡單:如果委托對象不需要接收委托者的回調,僅僅需要'unsubscribe'.
這在一些不同的 view 等待同一個回調來更新界面展示的時候很有用:如果 view 只是暫時隱藏(但是仍然存在),它可以僅僅需要取消對回調的訂閱。