Swift & runtimeHistorical technical note

Protocols and Delegation in iOS

Uses UITableView and custom controls to distinguish protocols from delegation. It compares delegate and block callbacks, then discusses how data sources and protocol abstractions can reduce coupling between objects.

Original byline:Sindri

Originally credited to Sindri; edited and archived by Bison.

Source listed in the original

English translation of the Chinese original, translated on . The article body uses automated translation; app descriptions, historical reading notes and video subtitles receive separate editorial checks. Original publication dates, attribution, code and version notes are retained.

What is a protocol? In Objective-C, declare one with @protocol and add properties and methods for conforming types to implement. From one perspective, it differs from the category mechanism while acting as a kind of category. Protocols are everywhere in daily development. The central example, UITableView, uses them to obtain data and handle events. Here is a very simple protocol:

Editor: Bison
Source: Sindri’s Notes

@protocol CustomProtocol

- (void)doSomething;

@end

Many developers still equate 委托-代理 with 协议. The former relies on the latter, but the latter can achieve abstraction and decoupling without the former. Before discussing what protocols can do, we need to understand what they are:

A protocol specifies behaviour that conforming classes must implement.

Protocol applications

Delegate callbacks

Almost everyone writes UITableView delegate and data-source methods. Apple's view is elegant and powerful. Even as its verbose data-source methods attract criticism, it is hard to imagine a better implementation of UITableView. This control demonstrates the power of 委托-代理.

1

The simplest and most direct protocol application is the 委托-代理 design pattern. When encapsulating custom controls, I like custom protocols for handling taps and other actions. To understand 代理, I believe you should at least have defined your own delegate protocol:

@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

The code above sketches a delegate implementation for a segmented controller I once built. In project development, advantages of delegate callbacks include 接口目的性强、易于追溯调试, among others.

When to use delegates

Alongside Delegate, another equally useful and popular mechanism is Block. For example, sharing features commonly return results through a block:

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

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

            case ShareResultFailed:
                //....

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

This code looks compact and concise. Returning the result through a Delegate feels quite different:

- (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:
                //....
        }
}

The same logic in a Delegate does not look as nice. Why? My reasons are:

  • A delegate lacks Block's ability to capture context, so caller parameters must be passed in. Yet ShareManager serves no purpose inside the method.
  • Compared with a block, a delegate splits sharing and result handling between two places, making the logic less compact.

Similarly, if the sharing example becomes a network request that also reports download progress, using only a Block might look like this:

- (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
        }
    }];
}

The Block is now rather cluttered and hard to follow. This comparison suggests that a Block works better than a delegate when a task needs an additional operation at completion. For an event with multiple states requiring callbacks as each occurs or changes, defining a protocol is a better choice.

Data sources

Although Apple separates data sources and delegates, both belong to the 委托-代理 mechanism. Apple has not explained exactly how to divide them; from their names, the distinction is roughly:

  • Data sources expose an interface supplying display data to controls.
  • Delegates expose an interface for responding to control events.

For a custom control with a data source, when should that source be called? Following UITableView behavior, its data-source methods are not called in the following situations:

  • UITableView has not been added to a view.

  • One of UITableView’s dimensions is 0.

  • UITableView has no delegate set.

First, address the first two issues. UIView, the superclass of all views, exposes didMoveToSuperview, called after the view is added with addSubview:. Override it and fetch data from the data source:

@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

Similar to didMoveToSuperview , didMoveToWindow is called once on either side of the former method. Appropriate overrides can defer some data-source calls, reducing the chance that many simultaneous calls lower the frame rate.

Although both use 委托-代理, a data source requires more considerations than a simple delegate. Data-source design is therefore a further step in applying protocols.

Protocol abstraction

Here we will discuss 面向对象编程, nearly any experienced developer can immediately name 面向对象编程 features: 多态、抽象、封装 greatly improve productivity, but their convenience has a cost. In Casa’s Think beyond object-oriented habits series discusses inheritance’s weaknesses.

But how does this relate to the article?

iPad development has a special component, UISplitViewController, that divides the screen into smaller and larger regions and displays two controllers simultaneously. The left is a management view and the right shows details. With multiple possible detail views, a tap on the left can require extensive controller creation and configuration code:

#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

In practice, the right side required more than a dozen screens, even before counting complex nib controllers. This made the left-side MasterViewController messy. Although only a weak reference was involved, MasterViewController still needed its containing splitViewController, and much controller creation and configuration code was repetitive. Protocols can improve this. Define a class construction method to unify creation of right-side controllers, plus a method for configuring their data:

@protocol DetailViewControllerProtocol

+ (instancetype)detailViewController;

- (void)configurateWithData: (id)data;

@end

Also define a delegate callback for the left controller, passing the class name of the right controller to show and its configuration data, so splitViewController can handle presentation itself:

///  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

After the change, MasterViewController retains only the data and the right controller's name. Everything else is handled by its delegate, 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

Finally, each right-hand controller implements the protocol to create and configure itself:

@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

This example abstracts common controller behavior—creation and configuration—into a protocol to reduce repeated code. It also lowers coupling: any controller implementing the protocol can be presented in this splitViewController.

Closing remarks

When first teaching myself iOS, I could not understand the point of protocols. After discovering Block, I thought it was so useful that delegates seemed pointless. I overused Block until realizing it was not magic and could introduce subtle bugs, then returned to Delegate. Since then, I have experimented with custom controls using notifications, delegates, and Block callbacks. This article is, strictly speaking, a record of one stage of that learning.


Recommended reading: Implement an iOS launch-screen advertisement in minutes