# iOS开发之协议代理

语言：zh-CN
原文：[iOS开发之协议代理](<https://allluckly.cn/%E6%8A%95%E7%A8%BF/tougao57>)
中文：[中文 HTML](<https://allluckly.cn/%E6%8A%95%E7%A8%BF/tougao57>)
English: [English HTML](<https://allluckly.cn/en/journal/2016-09-01-protocols-and-delegation-in-ios/>)
发布于: 2016-09-01T00:00:00+08:00
内容更新于: 2016-09-01T00:00:00+08:00
署名：Sindri

以 UITableView 和自定义控件为例区分协议与委托，比较 delegate 和 block 回调的使用场景，再讨论数据源与协议抽象如何帮助对象之间减少耦合。

原文署名Sindri，Bison 编辑收录。

[原文所列来源](<http://sindrilin.com/ios-dev/2016/08/25/%E8%81%8A%E8%81%8A%E5%8D%8F%E8%AE%AE>)

**阅读前** 这是 2015–2017 年间的技术记录，本次整理未重新运行验证示例。涉及的 API、依赖和工具版本请结合发表日期阅读。

何为协议，简单来说在OC中我们使用关键字`@protocol`可以声明一个协议，并在协议中添加多个属性、方法供于遵循者实现，从某个角度上来说，这是一种不同于`category`机制的`category`。在日常开发中，协议可谓无处不在，最为核心的`UITableView`通过协议来获取数据、完成事件处理等。下面就是一个最粗浅的协议

编辑：[Bison](<https://allluckly.cn/>)  
 来源：[Sindri的小巢](<http://sindrilin.com/ios-dev/2016/08/25/%E8%81%8A%E8%81%8A%E5%8D%8F%E8%AE%AE>)

```
@protocol CustomProtocol

- (void)doSomething;

@end
```

对于协议的理解，很多的开发者依旧保留在`委托-代理`等于`协议`等认知上。然而前者依赖于后者的实现，而后者即便不通过前者也能完成抽象解耦的工作。在继续谈协议可以完成的工作之前，有必要来理解一下何为协议：

> 协议指定了一套行为规范，遵循协议的类必须实现对应的行为

## 协议应用

#### *代理回调*

开发中我们几乎都会写的代码一定是`UITableView`系列的代理和数据源方法。毫无疑问，苹果提供的这个视图是如此的优雅而强大，即便在现在这个数据源方法因代码过多被疯狂吐槽的年代，你依然无法想到其他实现`UITableView`的更佳实践，这个控件充分向我们展示了`委托-代理`的强大。

![1](<https://allluckly.cn/images/blog/tuogao/tougao57/1.jpg>)

协议最简单直观的应用是`委托-代理`设计模式，在封装自定义控件的时候，我喜欢使用自定义的协议来完成用户点击等业务处理。个人认为，如果你想要了解`代理`这一模式，起码要自定义过自己的代理协议：

```
@protocol SegmentControlDelegate

@optional
- (void)segmentControl: (SegmentControl *)segmentControl didSelectItemAtIndex: (NSUInteger)index;

@end


@interface SegmentControl: UIView

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

@end


@implementation SegmentControl

- (voice)clickItem: (UIButton *)item
{
    if ([_delegate respondsToSelector: @selector(segmentControl:didSelectItemAtInex:)]) {
        [_delegate segmentControl: self didSelectItemAtIndex: item.tag];
    }
}

@end
```

上面是我曾经自定义过的分段控制器的代理伪实现代码，对于项目开发而言，代理这种回调机制的好处包括不仅于`接口目的性强、易于追溯调试`等。

#### *什么时候用代理*

这里不免就要提到另一个跟`Delegate`同样实用受欢迎的机制`Block`。比如常用的分享功能通常使用`block`进行结果回调：

```
typedef NS_ENUM(NSInteger, ShareResult) {
    ShareResultSuccess,
    ShareResultFailed,
    ShareResultCancel
};

- (void)share {
    [shareManager shareWithResult: ^(ShareResult result) {
        switch (result) {
            case ShareResultSuccess:
                //....

            case ShareResultFailed:
                //....

            case ShareResultCancel:
                //....
        }
    }];
}
```

这样的代码看着很紧凑简洁，如果用`Delegate`来完成结果回调又是另一种感觉了：

```
- (void)share
{
    ShareManager * shareManager = [[ShareManager alloc] initWithUrl: @"http://sindrilin.com" title: @"" content: @""];
    shareManager.delegate = self;
    [shareManager share];
}

- (void)shareManager: (ShareManager *)shareManager didCompleteShare: (ShareResult)result
{
    switch (result) {
            case ShareResultSuccess:
                //....

            case ShareResultFailed:
                //....

            case ShareResultCancel:
                //....
        }
}
```

同样的代码在`Delegate`看着并不`nice`。这是什么原因呢？个人认为原因如下：

- 由于代理没有`Block`的捕获上下文特性，需要传入调用方参数。但在方法中，`ShareManager`并没有任何作用
- 相较于`block`，代理将分享步骤和结果处理分到两个地方，逻辑不够紧凑

同样的，假如上面的分享换成网络请求，并且要求在请求中同步下载进度，单纯的使用`Block`可能就会变成这样：

```
- (void)requestData {
    [manager requestWithProgress: ^(CGFloat progress) {
        NSLog(@"download progress: %g", progress);
        self.progressView.progress = progress;
    } complete: ^(id receiveData, NSError * error) {
        if (error) {
            [self showErrorWithMsg: error.description];
        } else {
            // handle receive data
        }
    }];
}
```

这个时候的`Block`就会有些杂乱，看着头疼。通过这种对比，我们不难看出如果你需要执行某个任务，并且在任务完成的时候执行额外的操作时，`Block`的使用效果优于代理。而如果某个事件是存在多种状态，在每个状态发生或者改变时需要回调的，定义一个协议来回调是更好的选择。

#### *数据源*

虽然苹果将代理分为了数据源和代理两种类型，但是本质上都归属于`委托-代理`机制。苹果没有介绍过两者应当怎么划分，单从字面上的意思来划分这两者大概是这样的：

- 数据源为控件提供了数据展示的接口
- 代理为控件提供了事件响应的接口

这里要说的大概就是假如我们自定义了控件的数据源，那么应该什么时候调用数据源？按照`UITableView`的使用来说，当存在下面几种情况的时候，数据源方法是不调用的：

- UITableView未添加到视图上
- UITableView的宽高其中一个值为0
- UITableView没设置代理

首先是前两个问题，作为所有视图父类的`UIView`中开放了一个方法`didMoveToSuperview`，这个方法会在当前视图被`addSubview:`之后回调。因此我们需要重写这个方法并且从数据源获取数据：

```
@protocol CustomViewDataSource<NSObject>

- (CustomViewCell *)customView: (CustomView *)customView cellForRowAtIndexPath: (IndexPath *)indexPath;    

@end


@interface CustomView

@property (nonatomic, weak) id<CustomViewDataSource> dataSource;

@end


@implementation CustomView

- (void)didMoveToSuperview
{
    [super didMoveToSuperview];
    if (!_delegate) { return; }
    if ( CGRectGetWidth(self.frame) == 0 || CGRectGetHeight(self.frame) == 0 ) { return; }

    for (IndexPath * index in _visableIndexPaths) {
        id cell = [_dataSource customView: self cellForRowAtIndexPath: index];
        // configure cell
        [self addSubview: cell];
    }
}

@end
```

与` didMoveToSuperview  `类似的还有`didMoveToWindow`，这个方法会在前一个方法调用的前后分别调用一次，适当的重写这两个方法可以尽量让一些数据源方法调用延后，从而减少了同一时间大量方法调用降低帧数的可能性。

同样是`委托-代理`机制，数据源的使用要比单纯的代理考虑的因素多了许多。因此，数据源的使用算是协议应用的进一步。

#### *协议抽象*

在这里我们要聊聊`面向对象编程`，几乎有经验的开发者都能随口说出`面向对象编程`的特性：`多态、抽象、封装`、这些特性大大的提高了猿们的生产力，但这种便利并不是没有代价的，在Casa大神[跳出面向思想](<http://casatwy.com/tiao-chu-mian-xiang-dui-xiang-si-xiang-yi-ji-cheng.html>)系列就提出了继承的缺陷。

> 然而，这和本文有什么关系？

在iPad开发中有一个特殊的控件`UISplitViewController`，它将屏幕分割成大小两部分并同时显示两个控制器视图。其中左侧作为管理视图，右侧为详细信息视图。如果在右侧详情视图存在多个的情况下，左侧点击发生事件时，可能需要一堆的配置创建控制器的代码：

```
#import "AppDelegate.h"
#import "AddContactViewController.h"
#import "ContactDetailViewController.h"

@interface MasterViewController ()

@property (nonatomic, weak) UISplitViewController * splitViewController;
@property (nonatomic, strong) NSArray * contacts;

@end

@implementation MasterViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    AppDelegate * appDelegate = (AppDelegate *)[UIApplication sharedApplication].delegate;
    self.splitViewController = appDelegate.splitViewController;
}

- (void)contactDetail: (NSInteger)index
{
    ContactDetailViewController * contactDetailVC = [[ContactDetailViewController alloc] init];
    contactDetailVC.contact = _contacts[index];
    [_splitViewController showDetailViewController: contactDetailVC sender: self];
}

- (void)addContact
{
    AddContactViewController * addContactVC = [[AddContactViewController alloc] initWithNibName: @"AddContactViewController" bundle: nil];
    [_splitViewController showDetailViewController: addContactVC sender: self];
}

@end
```

实际上需求当中右侧包含的界面包括何止十来个，还不包括复杂的`nib`控制器，这样就导致了左侧视图`MasterViewController`非常的乱。一方面，虽然只是弱指针引用，但是`MasterViewController`依旧需要获取所在的`splitViewController`。另一方面，太多的控制器创建配置代码雷同而多余。通过使用协议让这些坏毛病变得不那么坏。对于右侧的显示控制器，定义一个用于初始化的类构造方法，将右侧控制器的创建统一化；以及一个配置数据的方法：

```
@protocol DetailViewControllerProtocol

+ (instancetype)detailViewController;

- (void)configurateWithData: (id)data;

@end
```

另一方面，为左侧的控制器定义一个回调代理，传入将要显示的右侧控制器类名以及配置数据，让`splitViewController`自己来完成界面显示：

```
///  MasterViewController.h
@protocol MasterViewControllerDelegate

- (void)showDetailWithClass: (Class)aClass data: (id)data;

@end


@interface MasterViewController: NSObject

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

@end



///  MasterViewController.m
@implementation MasterViewController- (void)contactDetail: (NSInteger)index

- (void)contactDetail: (NSInteger)index
{
    [self showDetail: NSClassFromString(@"ContactDetailViewController") data: _contacts[index]];
}

- (void)addContact
{
    [self showDetail: NSClassFromString(@"AddContactViewController") data: nil];
}


- (void)showDetail: (Class)aClass data: (id)data
{
    if ([_delegate respondsToSelector: @selector(showDetailWithClass:data:)]) {
        [_delegate showDetailWithClass: aClass data: data];
    }
}

@end
```

改动之后`MasterViewController`只保留了数据以及右侧控制器的名称，其余的工作交给代理人也就是`UISplitViewController`来完成：

```
#import "MasterViewController.h"
#import "DetailViewControllerProtocol.h"

@interface SplitViewController ()<MasterViewControllerDelegate>

@end

@implementation SplitViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    for (UIViewController * viewController in self.viewControllers) {
        if ([viewController isKindOfClass: [MasterViewController class]]) {
            MasterViewController * masterVC = (MasterViewController *)viewController;
            masterVC.delegate = self;
            break;
        }
    }
}

- (void)showDetailWithClass: (Class)aClass data: (id)data
{
    UIViewController<DetailViewControllerProtocol> * detailVC = [aClass detailViewController];
    [detailVC configurateWithData: data];
    [self showDetailViewController: detailVC sender: self];
}

@end
```

最后就是右侧不同的控制器实现协议方法，完成控制器的创建和配置：

```
@implementation  ContactDetailViewController

+ (instancetype)detailViewController
{
    return [[[self class] alloc] init];
}

- (void)configurateWithData: (Contact *)data
{
    self.phone = data.phone;
    self.fullName = data.fullName;
}

@end

@implementation AddContactViewController

+ (instancetype)detailViewController
{
    return [[[self class] alloc] initWithNibName: NSStringFromClass([self class) bundle: nil];
}

- (void)configurateWithData: (id)data {  }

///  More. Load from storyboard etc

@end
```

实际上上面的例子是通过协议将不同控制器统一的行为（创建、配置）抽象成协议，从而减少了重复代码的使用。这种抽象统一的好处在于耦合度低，任何控制器只要实现了协议就能很好的在这个`splitViewController`上使用展示。

## 结束语

曾几何时，在懵懂中自学iOS时，总是不能理解协议的用处。甚至在接触了`Block`的时候，觉得这tm的太好用了，代理有个毛卵用。然后一直滥用`Block`直到发现并没有那么的神，往往还会带来隐晦的bug，于是才重新捡起了`Delegate`。从那之后逐渐的开始尝试包括通知、代理、`Block`多种回调方式的自定义控件，因此，本文严格来说算是纪录曾经的一次成长。

---

好文推荐：[分分钟解决iOS开发中App启动动画广告的功能](<https://allluckly.cn/lblaunchimagead/LBLaunchImageAd>)

