聊聊 iOS 开发中的协议

前言

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

@protocol CustomProtocol

- (void)doSomething;

@end

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

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

协议应用

代理回调

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

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

@protocol SegmentControlDelegate

@optional

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

@end

@interface SegmentControl: UIView

@property (nonatomic, weak) id 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

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

@end

@interface CustomView

@property (nonatomic, weak) id 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大神跳出面向思想系列就提出了继承的缺陷。

然而,这和本文有什么关系?

在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 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 ()

@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 * 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多种回调方式的自定义控件,因此,本文严格来说算是纪录曾经的一次成长。

时间: 2024-12-28 23:17:10

聊聊 iOS 开发中的协议的相关文章

聊聊iOS开发中耳机的那点事(监听耳机拔插、耳机线控)-b

如果说一个项目出现的最重大的事故,那无疑就是开发人员使用了不可控的元素. 前言 iOS开发当中有关于视音频播放的开发不在少数,用户时常会使用到一种输出设备,那就是"耳机",这一篇博客写的就是关于耳机的一些开发相关的技术点. 检测耳机是否插入 看到上面的标题的时候一定要注意,这里说的是"检测耳机是否插入",这里只是一次性的检测,不是实时监控耳机的拔插,但是有一些时候,下面的这个方法已经足够满足我们的开发需求了. 首先,我们需要导入AVFoundation.framew

iOS开发中KVC、KVO简介

在iOS开发中,KVC和KVO是经常被用到的.可以使用KVC对对象的属性赋值和取得对象的属性值,可以使用KVO监听对象属性值的变化.简单介绍一下KVC和KVO. 一:键值编码(KVC) KVC,全称 Key Value Coding(键值编码),是OC 语言的一个特性,使用KVC,可以对对象的属性进行动态读写. KVC的操作方法由 NSKeyValueCoding协议提供,而NSObject已经实现了这个协议,因此OC中的几乎所有对象都可以使用KVC操作.常用的KVC操作方法有: (1)设置属性

总结iOS开发中的断点续传那些事儿

前言 断点续传概述 断点续传就是从文件赏赐中断的地方重新开始下载或者上传数据,而不是从头文件开始.当下载大文件的时候,如果没有实现断点续传功能,那么每次出现异常或者用户主动的暂停,都会从头下载,这样很浪费时间有木有.所以呢,项目中实现大文件下载的时候,断点续传功能是必不可少了.当然咯,断点续传有一种特殊的情况,就是我们的应用呗用户kill掉或者应用crash,要实现应用重启之后的断点续传,这种情况就是我们将要解决的问题. 断点续传的原理 要实现断点续传,服务器必须是要支持的.目前最常见的两种方式

IOS开发中数据持久化的几种方法--NSUserDefaults

IOS开发中数据持久化的几种方法--NSUserDefaults IOS 开发中,经常会遇到需要把一些数据保存在本地的情况,那么这个时候我们有以下几种可以选择的方案: 一.使用NSUserDefault是最简单直接的一个办法: 1)保存数据: 1 // 实例化一个NSUserDefaults单例对象 2 NSUserDefaults *user = [NSUserDefaults standardUserDefaults]; 3 // 把一个数组array保存在key为allContact的键值

iOS开发——淫技篇&iOS开发中各种淫技总结(五)

淫技篇&iOS开发中各种淫技总结(五) ARC的使用: ARC并不能避免所有的内存泄露.使用ARC之后,工程中可能还会有内存泄露,不过引起这些内存泄露的主要原因是:block,retain循环,对CoreFoundation对象(通常是C结构)管理不善,以及真的是代码没写好. reuseIdentifier 在iOS程序开发中一个普遍性的错误就是没有正确的为UITableViewCells.UICollectionViewCells和UITableViewHeaderFooterViews设置r

[转载]对iOS开发中内存管理的一点总结与理解

对iOS开发中内存管理的一点总结与理解 做iOS开发也已经有两年的时间,觉得有必要沉下心去整理一些东西了,特别是一些基础的东西,虽然现在有ARC这种东西,但是我一直也没有去用过,个人觉得对内存操作的理解是衡量一个程序员成熟与否的一个标准.好了,闲话不说,下面进入正题. 众所周知,ObjectiveC的内存管理引用的一种叫做“引用计数“ (Reference Count)的操作方式,简单的理解就是系统为每一个创建出来的对象,(这里要注意,只是对象,NSObject的子类,基本类型没有‘引用计数’)

iOS开发中的单元测试(三)——URLManager中的测试用例解析

本文转载至 http://www.cocoachina.com/cms/plus/view.php?aid=8088 此前,我们在<iOS开发中的单元测试(一)&(二)>中介绍了从使用者的角度对比当下比较流行的两款单元测试框架OCUnit和GHUnit,这篇文章中我们将介绍一款导航控件URLManager. URLManager是一个基于UINavigationController和UIViewController,以URL Scheme为设计基础的导航控件,目的是实现ViewCont

iOS开发中设置UITextField的占位文字的颜色,和光标的颜色

在iOS开发中,对于很多初学者而言,很有可能碰到需要修改UITextField的占位文字的颜色,以及当UITextField成为第一响应者后光标的颜色,那么下面小编就介绍一下修改占位文字和光标的颜色.1:当你在使用Storyboard开发是,点击UITextField,在点击右上角的属性检测器,其实在这里面你是找不到有可以修改占位文字和光标颜色的属性的.2:那就进入UITextField的协议里面去查找,但是还是找不到,3:在进代理里面去查找,看看有没有通过代理方法,返回颜色并控制占位文字的方法

iOS开发中的那些的约定俗成(1)————《编写高质量iOS与OS X代码的52个有效方法》读书笔记(第一章)

iOS开发中的那些的约定俗成(1) ----<编写高质量iOS与OS X代码的52个有效方法>读书笔记(第一章) 前言 "我要成为一个高产的开发人员.""想要混的好,就得多努力." 写这些东西是因为毕竟看了书,但是看书看过去之后,也许印象不是很深刻,有些东西现在也理解不了,那我就把我理解的,现在就可以用到的东西,简单的写出来就好,让自己今后看到就能明白其中的意思. 还有就是锻炼一下表达,编辑能力,慢慢的提升自己,随时随地的都要有一个锻炼的心. 最后当然就