ViewController的瘦身技术

ViewController中的代码量一般都是很大, 而且其中包含了许多没必要要的代码. 因此ViewController中代码的复用率一般都是最低的, 接下来会介绍几种技术对ViewController进行瘦身处理, 让代码变得能够复用, 将代码移动到合适的地方.objective-c

把 Data Source 和其余 Protocols 分离出来

把UITableViewDataSource的代码提出来放到一个单独的类中, 是为ViewController进行瘦身的一项强大技术.数组

举个例子, 在项目中会有个PhotoViewController类, 它有如下的一些方法来为tableView提供数据.缓存

# pragma mark ---

- (Photo*)photoAtIndexPath:(NSIndexPath*)indexPath {
    return photos[(NSUInteger)indexPath.row];
}

- (NSInteger)tableView:(UITableView*)tableView
 numberOfRowsInSection:(NSInteger)section {
    return photos.count;
}

- (UITableViewCell*)tableView:(UITableView*)tableView
        cellForRowAtIndexPath:(NSIndexPath*)indexPath {
    PhotoCell* cell = [tableView dequeueReusableCellWithIdentifier:PhotoCellIdentifier
                                                      forIndexPath:indexPath];
    Photo* photo = [self photoAtIndexPath:indexPath];
    cell.label.text = photo.name;
    return cell;
}
复制代码

能够看到上面的代码都是围绕数组在作事情, 更仔细地说, 是围绕PhotoViewController所管理的photos数组作一些事情. 咱们能够将这些与数组相关的代码移动到单独的类中. 而后对于cell的具体内容设置, 咱们可使用代理或者block.markdown

@implementation ArrayDataSource

- (id)itemAtIndexPath:(NSIndexPath*)indexPath {
    return items[(NSUInteger)indexPath.row];
}

- (NSInteger)tableView:(UITableView*)tableView
 numberOfRowsInSection:(NSInteger)section {
    return items.count;
}

- (UITableViewCell*)tableView:(UITableView*)tableView
        cellForRowAtIndexPath:(NSIndexPath*)indexPath {
    id cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier
                                              forIndexPath:indexPath];
    id item = [self itemAtIndexPath:indexPath];
    configureCellBlock(cell,item);
    return cell;
}

@end
复制代码

如今咱们将与dataSource都放到了单独的类ArrayDataSource中, 而且让ArrayDataSource遵循UITableViewDataSource协议. 以后咱们在ViewController中就能够把UITableViewDataSource所需实现的三个方法去掉, 取而代之的是使用ArrayDataSource做为tableView的datasource. 像下面这样调用:网络

void (^configureCell)(PhotoCell*, Photo*) = ^(PhotoCell* cell, Photo* photo) {
   cell.label.text = photo.name;
};
photosArrayDataSource = [[ArrayDataSource alloc] initWithItems:photos
                                                cellIdentifier:PhotoCellIdentifier
                                            configureCellBlock:configureCell];
self.tableView.dataSource = photosArrayDataSource;
复制代码

能够看到咱们只须要简单的把数据传给photosArrayDataSource, 而且将其设置为datasource, 系统就会在合适时机调用数据源方法, 提供数据.ide

可是上面的代码还存在不足, 能够看到咱们在block中设置了cell的内容, 这明显是属于View的逻辑, 不该该放入Controller中, 较好的方法是为cell建立一个分类或者设置一个模型属性. 后者较为常见, 经过调用cell的set方法, 在set方法写入设置cell内容的逻辑, 就能够完成了.oop

第一种方法是为cell建立一个分类.以下所示:布局

#import "PlayerCell+ConfigureForVoice.h"

@implementation PlayerCell (ConfigureForVoice)

- (void)configureForVoice:(NSURL *)voiceUrl Width:(CGFloat)width IndexPath:(NSIndexPath *)indexPath{
    
    self.cellView.playUrl = voiceUrl;
    self.cellView.width = width;
    self.cellView.indexPath = indexPath;
    self.cellView.hidden = NO;
    
}
@end
复制代码

这样咱们就只须要调用cell的分类方法, 在分类中完成内容的设置, 就能够把VIew的逻辑和Controller分开.url

void(^configureCellBlock)(PlayerCell*, NSURL*, NSIndexPath* ) = ^(PlayerCell *cell, NSURL *url, NSIndexPath *indexPath){

     [cell configureForVoice:url Width:self.tableView.width IndexPath:indexPath];

     cell.cellView.delegate = self;

   };
复制代码

经过将DataSource的逻辑写入单独的类中, 咱们可让代码在任意ViewController中复用, 而且你还能加入一些额外的方法如tableView:commitEditingStyle:forRowAtIndexPath:, 除此以外你也能够在单独的类中写入UICollectionViewDataSource, 使这个类同时支持两种协议, 使代码获得更好的复用.spa

将业务逻辑移到 Model 中

下面是ViewController中一些代码, 用来查找用户目前的优先事项的列表.

- (void)loadPriorities {
    NSDate* now = [NSDate date];
    NSString* formatString = @"startDate = %@";
    NSPredicate* predicate = [NSPredicate predicateWithFormat:formatString, now, now];
    NSSet* priorities = [self.user.priorities filteredSetUsingPredicate:predicate];
    self.priorities = [priorities allObjects];
}
复制代码

其实这些逻辑应该是属于Model层面上的, 咱们能够为Model建立一个分类, 在分类中实现查找逻辑.

这样ViewController就只须要像下面这样简单的调用, 就能够完成功能.

- (void)loadPriorities {
    self.priorities = [user currentPriorities];
}
复制代码

User+Extensions.m 中:

- (NSArray*)currentPriorities {
    NSDate* now = [NSDate date];
    NSString* formatString = @"startDate = %@";
    NSPredicate* predicate = [NSPredicate predicateWithFormat:formatString, now, now];
    return [[self.priorities filteredSetUsingPredicate:predicate] allObjects];
}
复制代码

任何像上面这样代码和Model关系紧密而且代码很轻松就能够写入Model中的状况, 咱们均可以尝试这么作.

把网络请求逻辑移到单独的类

不要在ViewController中请求网络的逻辑, 而是将请求网络逻辑放入单独的类中, 有不少优秀的第三方库好比AFNetWorking和YTKNetWork, 咱们只须要简单调用这些类提供的回调(多是以block形式或者delegate形式), 在成功回调和失败回调中完成对应的逻辑处理. 这样的好处是缓存和出错控制都会在这个类中处理.

把 View 代码移到 View 层

不要再ViewController中写入复杂的View逻辑, 咱们只须要在控制器中简单调用alloc init方法建立出View, 而后设置成控制器View的subView中. 而那些复杂的View布局, 子View的添加则应直接写到View中, 而后对外提供一些接口给控制器, 在合适的时间通知控制器, 控制器再去作对应的处理. 对于简单的View咱们可使用xib的方式进行建立, 可是记住若View比较复杂最好不要采用这种方法, 否则后期维护更改工做量会特别大.

建立基类ViewController集成重复代码

项目中在VIewController中咱们常常要写一些重复的代码, 好比设置导航条的标题内容及样式, 向导航栏添加返回按钮等等这些重复操做. 而后我就想到既然这些逻辑全部控制器基本都要实现, 那么为何不把代码抽取到公共的基类, 而后建立的控制器都继承这个公共基类, 这样就可使得ViewController变得更加整洁.

以下面我就在基类中写入了返回按钮的代码.

- (instancetype)init{
    if (self=[super init]) {
        [self setDefultBackBtn];
       
    }
    return self;
}
- (void)setDefultBackBtn {
    [self backItemWithImage:@"icon_back" highlight:@"icon_back" title:nil];
}
- (void)backItemWithImage:(NSString *)normalImageName
                         highlight:(NSString *)highlighImageName
                            title:(NSString *)title {
    UIButton * leftBtn=[UIButton buttonWithType:UIButtonTypeCustom];
    if (normalImageName) {
        [leftBtn setImage:[UIImage imageNamed:normalImageName] forState:UIControlStateNormal];
    }
    if (highlighImageName) {
        [leftBtn setImage:[UIImage imageNamed:highlighImageName] forState:UIControlStateHighlighted];
    }
    if (title) {
        leftBtn.titleLabel.font=[UIFont systemFontOfSize:18.0f];
        [leftBtn setTitle:title forState:UIControlStateNormal];
        [leftBtn setTitle:title forState:UIControlStateHighlighted];
        [leftBtn setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
        [leftBtn setTitleColor:[UIColor whiteColor] forState:UIControlStateHighlighted];
    }else {
        leftBtn.titleLabel.font=[UIFont systemFontOfSize:18.0f];
        [leftBtn setTitle:@"    " forState:UIControlStateNormal];
        [leftBtn setTitle:@"    " forState:UIControlStateHighlighted];
        [leftBtn setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
        [leftBtn setTitleColor:[UIColor whiteColor] forState:UIControlStateHighlighted];
    }
    
    
    [leftBtn addTarget:self action:@selector(backToLastVC) forControlEvents:UIControlEventTouchUpInside];
    [leftBtn sizeToFit];
    self.navigationItem.leftBarButtonItem=[[UIBarButtonItem alloc]initWithCustomView:leftBtn];
}

复制代码

如上面代码所示, 首先 用init方法时, 就将返回按钮设置到导航栏上, 而且设置一些默认按钮样式以及点击按钮响应的方法, 这样当咱们建立一个继承基类控制器的子类控制器时就会默认设置好返回按钮了. 大多数页面都是具备返回按钮的, 可是有时候按钮有些特殊需求或者根本不须要返回按钮. 那么咱们只须要在基类中提供一些其余接口就能够适应这些需求. 好比下面:

//调用此方法不使用默认的返回按钮
- (instancetype)initWithDefaultBackBtn:(BOOL)isNeed {
    if (self=[super init]) {
        if (isNeed){
        [self setDefultBackBtn];
        }
    }
    return self;
}
//而后调用此方法设置自定义的返回按钮.
- (void)backItemWithImage:(NSString *)normalImageName
                         highlight:(NSString *)highlighImageName
                            title:(NSString *)title;
复制代码

这里只是简单举了一个返回按钮的例子, 可是能够作的远不止于此. 任何ViewController中的公共逻辑, 咱们均可以写入到类中.

通信

其余常常在ViewController中发生的事情是与其余ViewController, View, Model进行通信, 虽然这确实是ViewController应该作的事, 可是咱们尽可能但愿用较少的代码完成.

关于ViewController和Model通信已经有阐述的很好的技术来完成好比KVO, 可是ViewController之间的消息传递就显的不是那么清晰.

当一个ViewController想把一个状态传递给另一个ViewController, 就会出现这样不是很清晰的问题. 比较好的作法就是把状态放入到一个单独的对象里, 而后把这个对象传递给其余ViewController, 而后ViewController观察并修改这个状态. 这样作的好处消息传递都在一个地方(被观察的对象)进行, 咱们不用再纠结复杂的delegate回调.

总结

以上就是一些VIewController瘦身的技术. 他们的核心思想就是把不是ViewController的逻辑放到其余地方, 将控制器的公共逻辑提取出来, 而且使代码逻辑尽可能简单. 咱们的最终目标:写可维护的代码. 只要把握这些原则, 咱们就可使得笨重的ViewController变得整洁.

参考文章: objccn.io/issue-1-1/

相关文章
相关标签/搜索