iOS開(kāi)發(fā)-聊聊協(xié)議

前言

何為協(xié)議霹购,簡(jiǎn)單來(lái)說(shuō)在OC中我們使用關(guān)鍵字@protocol可以聲明一個(gè)協(xié)議访惜,并在協(xié)議中添加多個(gè)屬性宦芦、方法供于遵循者實(shí)現(xiàn)宙址,從某個(gè)角度上來(lái)說(shuō),這是一種不同于category機(jī)制的category调卑。在日常開(kāi)發(fā)中抡砂,協(xié)議可謂無(wú)處不在大咱,最為核心的UITableView通過(guò)協(xié)議來(lái)獲取數(shù)據(jù)、完成事件處理等注益。下面就是一個(gè)最粗淺的協(xié)議

@protocol CustomProtocol

- (void)doSomething;

@end

對(duì)于協(xié)議的理解碴巾,很多的開(kāi)發(fā)者依舊保留在委托-代理等于協(xié)議等認(rèn)知上。然而前者依賴于后者的實(shí)現(xiàn)丑搔,而后者即便不通過(guò)前者也能完成抽象解耦的工作厦瓢。在繼續(xù)談協(xié)議可以完成的工作之前,有必要來(lái)理解一下何為協(xié)議:

協(xié)議指定了一套行為規(guī)范低匙,遵循協(xié)議的類(lèi)必須實(shí)現(xiàn)對(duì)應(yīng)的行為

協(xié)議應(yīng)用

代理回調(diào)

開(kāi)發(fā)中我們幾乎都會(huì)寫(xiě)的代碼一定是UITableView系列的代理和數(shù)據(jù)源方法旷痕。毫無(wú)疑問(wèn),蘋(píng)果提供的這個(gè)視圖是如此的優(yōu)雅而強(qiáng)大顽冶,即便在現(xiàn)在這個(gè)數(shù)據(jù)源方法因代碼過(guò)多被瘋狂吐槽的年代欺抗,你依然無(wú)法想到其他實(shí)現(xiàn)UITableView的更佳實(shí)踐,這個(gè)控件充分向我們展示了委托-代理的強(qiáng)大强重。

強(qiáng)大的UITableView

協(xié)議最簡(jiǎn)單直觀的應(yīng)用是委托-代理設(shè)計(jì)模式绞呈,在封裝自定義控件的時(shí)候,我喜歡使用自定義的協(xié)議來(lái)完成用戶點(diǎn)擊等業(yè)務(wù)處理间景。個(gè)人認(rèn)為佃声,如果你想要了解代理這一模式,起碼要自定義過(guò)自己的代理協(xié)議:

@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

上面是我曾經(jīng)自定義過(guò)的分段控制器的代理偽實(shí)現(xiàn)代碼倘要,對(duì)于項(xiàng)目開(kāi)發(fā)而言圾亏,代理這種回調(diào)機(jī)制的好處包括不僅于接口目的性強(qiáng)、易于追溯調(diào)試等封拧。

什么時(shí)候用代理

這里不免就要提到另一個(gè)跟Delegate同樣實(shí)用受歡迎的機(jī)制Block志鹃。比如常用的分享功能通常使用block進(jìn)行結(jié)果回調(diào):

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

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

            case ShareResultFailed:
                //....

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

這樣的代碼看著很緊湊簡(jiǎn)潔,如果用Delegate來(lái)完成結(jié)果回調(diào)又是另一種感覺(jué)了:

- (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泽西。這是什么原因呢曹铃?個(gè)人認(rèn)為原因如下:

  • 由于代理沒(méi)有Block的捕獲上下文特性,需要傳入調(diào)用方參數(shù)捧杉。但在方法中陕见,ShareManager并沒(méi)有任何作用
  • 相較于block,代理將分享步驟和結(jié)果處理分到兩個(gè)地方味抖,邏輯不夠緊湊

同樣的评甜,假如上面的分享?yè)Q成網(wǎng)絡(luò)請(qǐng)求,并且要求在請(qǐng)求中同步下載進(jìn)度仔涩,單純的使用Block可能就會(huì)變成這樣:

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

這個(gè)時(shí)候的Block就會(huì)有些雜亂忍坷,看著頭疼。通過(guò)這種對(duì)比,我們不難看出如果你需要執(zhí)行某個(gè)任務(wù)承匣,并且在任務(wù)完成的時(shí)候執(zhí)行額外的操作時(shí),Block的使用效果優(yōu)于代理锤悄。而如果某個(gè)事件是存在多種狀態(tài)韧骗,在每個(gè)狀態(tài)發(fā)生或者改變時(shí)需要回調(diào)的,定義一個(gè)協(xié)議來(lái)回調(diào)是更好的選擇零聚。

數(shù)據(jù)源

雖然蘋(píng)果將代理分為了數(shù)據(jù)源和代理兩種類(lèi)型袍暴,但是本質(zhì)上都?xì)w屬于委托-代理機(jī)制。蘋(píng)果沒(méi)有介紹過(guò)兩者應(yīng)當(dāng)怎么劃分隶症,單從字面上的意思來(lái)劃分這兩者大概是這樣的:

  • 數(shù)據(jù)源為控件提供了數(shù)據(jù)展示的接口
  • 代理為控件提供了事件響應(yīng)的接口

這里要說(shuō)的大概就是假如我們自定義了控件的數(shù)據(jù)源政模,那么應(yīng)該什么時(shí)候調(diào)用數(shù)據(jù)源?按照UITableView的使用來(lái)說(shuō)蚂会,當(dāng)存在下面幾種情況的時(shí)候淋样,數(shù)據(jù)源方法是不調(diào)用的:

  • UITableView未添加到視圖上
  • UITableView的寬高其中一個(gè)值為0
  • UITableView沒(méi)設(shè)置代理

首先是前兩個(gè)問(wèn)題,作為所有視圖父類(lèi)的UIView中開(kāi)放了一個(gè)方法didMoveToSuperview胁住,這個(gè)方法會(huì)在當(dāng)前視圖被addSubview:之后回調(diào)趁猴。因此我們需要重寫(xiě)這個(gè)方法并且從數(shù)據(jù)源獲取數(shù)據(jù):

@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類(lèi)似的還有didMoveToWindow,這個(gè)方法會(huì)在前一個(gè)方法調(diào)用的前后分別調(diào)用一次彪见,適當(dāng)?shù)闹貙?xiě)這兩個(gè)方法可以盡量讓一些數(shù)據(jù)源方法調(diào)用延后儡司,從而減少了同一時(shí)間大量方法調(diào)用降低幀數(shù)的可能性。

同樣是委托-代理機(jī)制余指,數(shù)據(jù)源的使用要比單純的代理考慮的因素多了許多捕犬。因此,數(shù)據(jù)源的使用算是協(xié)議應(yīng)用的進(jìn)一步酵镜。

協(xié)議抽象

在這里我們要聊聊面向?qū)ο缶幊?/code>碉碉,幾乎有經(jīng)驗(yàn)的開(kāi)發(fā)者都能隨口說(shuō)出面向?qū)ο缶幊?/code>的特性:多態(tài)、抽象笋婿、封裝誉裆、這些特性大大的提高了猿們的生產(chǎn)力,但這種便利并不是沒(méi)有代價(jià)的缸濒,在Casa大神跳出面向思想系列就提出了繼承的缺陷足丢。

然而,這和本文有什么關(guān)系庇配?

在iPad開(kāi)發(fā)中有一個(gè)特殊的控件UISplitViewController斩跌,它將屏幕分割成大小兩部分并同時(shí)顯示兩個(gè)控制器視圖。其中左側(cè)作為管理視圖捞慌,右側(cè)為詳細(xì)信息視圖耀鸦。如果在右側(cè)詳情視圖存在多個(gè)的情況下,左側(cè)點(diǎn)擊發(fā)生事件時(shí),可能需要一堆的配置創(chuàng)建控制器的代碼:

#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

實(shí)際上需求當(dāng)中右側(cè)包含的界面包括何止十來(lái)個(gè)袖订,還不包括復(fù)雜的nib控制器氮帐,這樣就導(dǎo)致了左側(cè)視圖MasterViewController非常的亂。一方面洛姑,雖然只是弱指針引用上沐,但是MasterViewController依舊需要獲取所在的splitViewController。另一方面楞艾,太多的控制器創(chuàng)建配置代碼雷同而多余参咙。通過(guò)使用協(xié)議讓這些壞毛病變得不那么壞。對(duì)于右側(cè)的顯示控制器硫眯,定義一個(gè)用于初始化的類(lèi)構(gòu)造方法蕴侧,將右側(cè)控制器的創(chuàng)建統(tǒng)一化;以及一個(gè)配置數(shù)據(jù)的方法:

@protocol DetailViewControllerProtocol

+ (instancetype)detailViewController;

- (void)configurateWithData: (id)data;

@end

另一方面两入,為左側(cè)的控制器定義一個(gè)回調(diào)代理净宵,傳入將要顯示的右側(cè)控制器類(lèi)名以及配置數(shù)據(jù),讓splitViewController自己來(lái)完成界面顯示:

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

改動(dòng)之后MasterViewController只保留了數(shù)據(jù)以及右側(cè)控制器的名稱(chēng)谆刨,其余的工作交給代理人也就是UISplitViewController來(lái)完成:

#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

最后就是右側(cè)不同的控制器實(shí)現(xiàn)協(xié)議方法塘娶,完成控制器的創(chuàng)建和配置:

@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

實(shí)際上上面的例子是通過(guò)協(xié)議將不同控制器統(tǒng)一的行為(創(chuàng)建、配置)抽象成協(xié)議痊夭,從而減少了重復(fù)代碼的使用刁岸。這種抽象統(tǒng)一的好處在于耦合度低,任何控制器只要實(shí)現(xiàn)了協(xié)議就能很好的在這個(gè)splitViewController上使用展示她我。

結(jié)束語(yǔ)

曾幾何時(shí)虹曙,在懵懂中自學(xué)iOS時(shí),總是不能理解協(xié)議的用處番舆。甚至在接觸了Block的時(shí)候酝碳,覺(jué)得這tm的太好用了,代理有個(gè)毛卵用恨狈。然后一直濫用Block直到發(fā)現(xiàn)并沒(méi)有那么的神疏哗,往往還會(huì)帶來(lái)隱晦的bug,于是才重新?lián)炱鹆?code>Delegate禾怠。從那之后逐漸的開(kāi)始嘗試包括通知返奉、代理、Block多種回調(diào)方式的自定義控件吗氏,因此芽偏,本文嚴(yán)格來(lái)說(shuō)算是紀(jì)錄曾經(jīng)的一次成長(zhǎng)。

轉(zhuǎn)載請(qǐng)注明本文作者以及地址

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末弦讽,一起剝皮案震驚了整個(gè)濱河市污尉,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖被碗,帶你破解...
    沈念sama閱讀 206,968評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件某宪,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡锐朴,警方通過(guò)查閱死者的電腦和手機(jī)缩抡,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)包颁,“玉大人,你說(shuō)我怎么就攤上這事压真∶浣溃” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 153,220評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵滴肿,是天一觀的道長(zhǎng)岳悟。 經(jīng)常有香客問(wèn)我,道長(zhǎng)泼差,這世上最難降的妖魔是什么贵少? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,416評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮堆缘,結(jié)果婚禮上滔灶,老公的妹妹穿的比我還像新娘。我一直安慰自己吼肥,他們只是感情好录平,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,425評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著缀皱,像睡著了一般斗这。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上啤斗,一...
    開(kāi)封第一講書(shū)人閱讀 49,144評(píng)論 1 285
  • 那天表箭,我揣著相機(jī)與錄音,去河邊找鬼钮莲。 笑死免钻,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的臂痕。 我是一名探鬼主播伯襟,決...
    沈念sama閱讀 38,432評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼握童!你這毒婦竟也來(lái)了姆怪?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 37,088評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎稽揭,沒(méi)想到半個(gè)月后俺附,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,586評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡溪掀,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,028評(píng)論 2 325
  • 正文 我和宋清朗相戀三年事镣,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片揪胃。...
    茶點(diǎn)故事閱讀 38,137評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡璃哟,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出喊递,到底是詐尸還是另有隱情随闪,我是刑警寧澤,帶...
    沈念sama閱讀 33,783評(píng)論 4 324
  • 正文 年R本政府宣布骚勘,位于F島的核電站铐伴,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏俏讹。R本人自食惡果不足惜当宴,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,343評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望泽疆。 院中可真熱鬧户矢,春花似錦、人聲如沸殉疼。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,333評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)株依。三九已至驱证,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間恋腕,已是汗流浹背抹锄。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,559評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留荠藤,地道東北人伙单。 一個(gè)月前我還...
    沈念sama閱讀 45,595評(píng)論 2 355
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像哈肖,于是被迫代替她去往敵國(guó)和親吻育。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,901評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容

  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫(kù)淤井、插件布疼、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 12,029評(píng)論 4 62
  • *面試心聲:其實(shí)這些題本人都沒(méi)怎么背,但是在上海 兩周半 面了大約10家 收到差不多3個(gè)offer,總結(jié)起來(lái)就是把...
    Dove_iOS閱讀 27,125評(píng)論 29 470
  • 文/小暮 這個(gè)電影播出之后游两,看到很多人都說(shuō)這應(yīng)該是我今年看的最好的電影了砾层,看完淚奔了。 而看完這個(gè)電影之后贱案,最讓我...
    歲月小暮閱讀 246評(píng)論 0 2
  • Dreamland14肛炮? - Tribute To Jun 2 (Nujabes Tribute)看了那么多的測(cè)評(píng)...
    麥田的怪圈閱讀 333評(píng)論 0 1
  • 森森差不多是從高二上學(xué)期十月份中旬開(kāi)始發(fā)福,宝踪,并且還對(duì)不起一個(gè)真心真意待我侨糟,對(duì)我百般忍讓百般討好的姐們,瘩燥,是的粟害,我...
    餅森森閱讀 195評(píng)論 0 1