# Delegation in Objective-C

> 

Published: 2013-05-24
URL: https://kaochenlong.com/delegation-in-objective-c

---

在開發 iOS app 的過程中，Delegation（委任）幾乎是避不掉的東西，例如在 ViewController 裡處理 UITableView 的時候，大家一定都寫過像這樣的程式碼：

```objc
self.tableView.delegate = self
```

坊間的書本大多會教要這樣寫，但不一定有說明為什麼要這麼寫。其實 delegation 的概念並不困難，只是要用程式碼來表達的時候，對新手來說可能就需要多一點的想像力了。

Delegation，中文翻譯成「委任」，委任兩字講的好聽是拜託別人做事，講白一點就是自己不想做或不會做，所以外包出去叫別人做。

但是，就算是要叫別人做也不能隨便找一個路人就可以，舉個例子，我想要把「撰寫 Ruby 程式」這件事委任給別人，要有能力處理這份工作的人至少得知道 Ruby 程式怎麼寫。

&lt;!-- more --&gt;

那要判斷對方是否有「能力」來接受我的委任，就是問這個被委任的人是否有符合（Conform）我訂的條件（Protocol），然後這個條件就跟面試新人一樣，某些技能是必須的（Required），但其它條件是非必須的（Optional）。

一樣以 UITableView 來舉個例子，如果我希望某個 ViewController 可以有能力接受 TableView 的委任，假設這個 UITableView 是設定成一個叫做 `tableView` 的 IBOutlet 的話，我們可以直接在 ViewDidLoad 的地方這樣寫：

```objc
- (void)viewDidLoad
{
    [super viewDidLoad];
    _tableView.delegate = self;
}
```

或是直接在 Storyboard 用拉的也行：

![](/rails/active_storage/representations/proxy/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBBa2NCIiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--ef04b10c9abcd4e2979eb72416f49f0885c7440f/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdCem9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFJQUJXa0NBQVE9IiwiZXhwIjpudWxsLCJwdXIiOiJ2YXJpYXRpb24ifX0=--578d6799c87a604ca574298502ba874c9075e929/drag-delegate-to-viewcontroller.png)

這樣就完成了 delegation 的設定啦!

但是，剛剛才講到，我怎麼知道這個 ViewController 有能力可以完成我想要委任的工作? 其實就是讓這個 ViewController 實作 `UITableViewDelegate` 這個 Protocol 就行了。

```objc
@interface ViewController()&lt;UITableViewDelegate&gt;

@end
```

關於 Protocol，大家可以參考之前寫的[這篇](/2010/12/11/protocol-in-objective-c/)

但是，`UITableViewDelegate` 這個 protocol 到底定義了哪些東西? 讓我們按著鍵盤的 Cmd 鍵加上滑鼠點擊 `UITableViewDelegate` 就可以連過去看一下它的定義（部份程式碼省略）：

```objc
@protocol UITableViewDelegate&lt;NSObject, UIScrollViewDelegate&gt;

@optional

// ..省略

// Variable height support

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath;
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section;
- (CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section;

// Section header &amp; footer information. Views are preferred over title should you decide to provide both

- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section;
- (UIView *)tableView:(UITableView *)tableView viewForFooterInSection:(NSInteger)section;

// ..省略

@end
```

`UITableViewDelegate` 這個 protocol 定義在 UIKit 的 UITableView.h 裡面，而且全部都是 optional 的，也就是說就算不實作任何這個 protocol 裡的方法也不會怎麼樣，它還是有自己預設的行為。

但如果你想要做一些 UI 客製化，例如想要動態的調整每個 cell 的高度，你就可以在這個 ViewController，也就是這個 tableview 所「委任」的對象，覆寫這個 method：

```objc
#pragma mark - UITableView delegate
- (CGFloat) tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
    return 100.0f;
}
```

上面的實作這個方法，你可以想像成以下情境：

&gt; tableview 問 view controller 說：「喂，我每個 cell 要設定多高啊?」
&gt;
&gt; view controller 回答：「就 100 個點（point）吧。」

再舉個例子，如果你想要客製化這個 tableview 的 section header 或 footer，就是覆寫這兩個方法：

```objc
- (UIView *) tableView:(UITableView *)tableView viewForFooterInSection:(NSInteger)section
{
    // 內容省略
}

- (UIView *) tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section
{
    // 內容省略
}
```

以此類推。

UITableView 所需要的一些行為或是外觀描述，都是透過 delegate 的方式，請被委任的傢伙(通常是那個 tableview 所在的 view controller)告知。

## Delegate 怎麼寫?

上面大概說明了怎麼用別人寫好的 delegate，如果也想自己仿著做一個，該怎麼做呢?

我這邊來做個簡單的範例，大概就是希望某個 UIView 在進行動畫完成之後透過 delegate 發送一個「喂，我已經搞定囉」的通知，並同時也回傳狀態。

我開了一個新的 Xcode 專案，選了 `Single View Application`，裡面應該只有預設的 `ViewController.h` 跟 `ViewController.m`，接著我新增了一個繼承自 UIView 的類別，叫做 `AnimationSquareView`：

![](/rails/active_storage/representations/proxy/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBBa2dCIiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--bf3d88378e5f482bf831c138cc9425f6f1fa9575/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdCem9MWm05eWJXRjBTU0lJYW5CbkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFJQUJXa0NBQVE9IiwiZXhwIjpudWxsLCJwdXIiOiJ2YXJpYXRpb24ifX0=--c7384179b79b360ce488e92f67a9c0795f833455/animationsquareview.jpg)

並且在上面加了一個叫做 `run` 的 public method：

```objc
//== 檔案：AnimationSquareView.h ==
#import &lt;UIKit/UIKit.h&gt;

@interface AnimationSquareView : UIView
- (void) run;
@end
```

```objc
// == 檔案：AnimationSquareView.m ==
#import &quot;AnimationSquareView.h&quot;

@implementation AnimationSquareView
- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self)
    {
        self.backgroundColor = [UIColor blackColor];
    }
    return self;
}

- (void) run
{
    [UIView animateWithDuration:3.0 animations:^{
        CGRect newFrame = CGRectMake(self.frame.origin.x + 150,
                                     self.frame.origin.y,
                                     self.frame.size.width,
                                     self.frame.size.height);
        self.frame = newFrame;
        self.alpha = 0.5;
    } completion:^(BOOL finished) {
        // do something later..
    }];
}

@end
```

這個 `run` 做的事情就是在 3 秒鐘完成一個簡單的動畫（其實就是讓它自己往右邊移動 150 個點，並且透明度變 50%）。

ViewController 的部份程式碼如下：

```objc
// == 檔案：ViewController.m ==

#import &quot;ViewController.h&quot;
#import &quot;AnimationSquareView.h&quot;

@interface ViewController()

@end

@implementation ViewController

- (void)viewDidLoad
{
    [super viewDidLoad];

    AnimationSquareView* squareView = [[AnimationSquareView alloc] initWithFrame:CGRectMake(50, 50, 100, 100)];
    [self.view addSubview:squareView];
    [squareView run];
}

@end
```

執行一下，應該會看到一個黑色的方塊緩緩的往右程動，過三秒鐘之後就停下來了：

![](/rails/active_storage/representations/proxy/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaHBBa2tCIiwiZXhwIjpudWxsLCJwdXIiOiJibG9iX2lkIn19--78c1e66c1768002fa9a69c04bbfa9fc5794e4df8/eyJfcmFpbHMiOnsibWVzc2FnZSI6IkJBaDdCem9MWm05eWJXRjBTU0lJY0c1bkJqb0dSVlE2RkhKbGMybDZaVjkwYjE5c2FXMXBkRnNIYVFJQUJXa0NBQVE9IiwiZXhwIjpudWxsLCJwdXIiOiJ2YXJpYXRpb24ifX0=--578d6799c87a604ca574298502ba874c9075e929/animation-square-run.png)

### 加上 delegate

如果，我想要讓這個 ViewController 知道這個方塊什麼時候做完它的動畫效果，該怎麼做?

一種方法是可以在那個動畫 block 的 completion 區塊利用 `NSNotification Center` 並且在 ViewController 搭配 `KVO（Key-Value Observing）` 來實作，不過我們這邊選擇用 delegation。首先，我先定義一個叫做 `AnimationSquareViewDelegate` 的 protocol，並且在原來的 `AnimationSquareview` 類別裡加了一個名為 `delegate` 的 property：

```objc
// == 檔案：AnimationSquare.m ==
#import &lt;UIKit/UIKit.h&gt;

@class AnimationSquareView;
@protocol AnimationSquareViewDelegate &lt;NSObject&gt;

@optional
- (void) animationSquareView:(AnimationSquareView *) squareView didFinishAnimationWithStatus:(NSDictionary *)status;

@end

@interface AnimationSquareView : UIView

@property (nonatomic, weak) id &lt;AnimationSquareViewDelegate&gt; delegate;
- (void) run;

@end
```

這裡可能有些需要解釋的：

1. `delegate` 這個 property 並不一定要命名為 `delegate`，只是慣例上通常會使用 `delegate` 這個名字，你硬是要把它叫 `abc` 也行，只是在待會我們要設定 delegate 的時候就要用 `abc` 了。另外，protocol 的定義也不一定要放在同一個 header 檔裡，也可以另外獨立的檔案，不過因為這個 delegate 的行為跟這個類別有關，我通常也會把它寫在同一個檔案裡面。
2. 上面第 4 行，為什麼需要那個 `@class AnimationSquareView;`? 因為在定義 protocol 的當下，這個類別還沒被定義出來，所以 `@class` 是告訴編譯器說：「我這個 `AnimationSquareView` 是一個類別，反正我待會就會寫，你先別管這麼多」。
3. Protocol 裡的方法有分兩種，一種是規定必須實作的（required），另一種則是不一定要實作的（optional），在這個範例裡，我認為`完成動畫`的這個方法並不是一個一定要實作的方法，所以我把 `animationSquareView:didFinishAnimationWithStatus:` 它設定成 optional（如果沒有特別標記，就是 required 的）。
4. Protocol 的名字 `AnimationSquareViewDelegate` 也不一定要叫這個名字，只是在後面加上個 `Delegate` 似乎也是慣例。

再來，我們改寫一下剛剛那段動畫的 completion 區塊的程式碼：

```objc
// == 檔案：AnimationSquare.m ==
- (void) run
{
    [UIView animateWithDuration:3.0 animations:^{
        CGRect newFrame = CGRectMake(self.frame.origin.x + 150,
                                     self.frame.origin.y,
                                     self.frame.size.width,
                                     self.frame.size.height);
        self.frame = newFrame;
        self.alpha = 0.5;
    } completion:^(BOOL finished) {
        if ([_delegate respondsToSelector:@selector(animationSquareView:didFinishAnimationWithStatus:)])
        {
            NSDictionary* currentStatus = @{@&quot;status&quot;: @&quot;finished&quot;};
            [_delegate animationSquareView:self didFinishAnimationWithStatus:currentStatus];
        }
    }];
}
```

這裡需要解釋的就應該就只有那段 `respondsToSelector` 了。這段的意思是問自己本身的這個 `delegate` 是不是有實作 `animationSquareView:didFinishAnimationWithStatus:` 這個方法，如果有的話，就呼叫它，並且把自己（self）以及狀態（一個 NSDictionary）傳入。

### 設定 delegate

再來，回到 ViewController 裡，準備設定我們剛剛寫好的 protocol 跟 delegate：

```objc
// == 檔案：ViewController.m ==

#import &quot;ViewController.h&quot;
#import &quot;AnimationSquareView.h&quot;

@interface ViewController()&lt;AnimationSquareViewDelegate&gt;

@end

@implementation ViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    AnimationSquareView* squareView = [[AnimationSquareView alloc] initWithFrame:CGRectMake(50, 50, 100, 100)];
    [self.view addSubview:squareView];
    squareView.delegate = self;
    [squareView run];
}

#pragma mark - AnimationSquareView Delegate
- (void)animationSquareView:(AnimationSquareView *)squareView didFinishAnimationWithStatus:(NSDictionary *)status
{
    NSLog(@&quot;current status = %@&quot;, status);
}

@end
```

這邊多了幾行程式碼：

1. 第 6 行，在類別後面加上了 `&lt;AnimationSquareViewDelegate&gt;`，就是告訴編譯器說：「我有遵守（conform）`AnimationSquareView` 規定的 protocol」，表示可以接受 AnimationSquareView 的委任。
2. 第 17 行，設定 `squareView.delegate = self`，把 squareView 的 delegate 設定到自己身上，也就是目前這個 ViewController。
3. 第 22 行到第 25 行，就是實作 protocol 裡定義的方法。

執行一下，應該可以在動畫執行結束的時候，看到輸出結果。

那，如果第 22 行到第 25 行的程式碼沒實作出來會怎樣? 在我們這個例子，不會怎樣，因為我們並沒有在 protocol 裡設定一定要實作的方法（required）。

透過 delegation，可以降低類別與類別之間的耦合，但仍然可方便的讓類別之間有&quot;溝通&quot;的效果，而且也很容易的可以把要溝通的&quot;訊息&quot;一併傳回。

以上，希望這篇文章能讓大家對 delegation 有更進一步的了解。如果有哪邊寫錯，還請前輩先進指點。



