如何解耦控制器(iOS)


带目录版请移步纸简书生

前言:如果你维护老项目,项目里面的那些臃肿的控制印象应该很深吧。在原来上千行代码里修改,新加代码那感觉简直了。😄,今天就来看看可以用哪些方法去分解臃肿的控制器。

控制器变得臃肿,事实上也就是我们项目中的业务在版本迭代中不断增加而导致的。加上苹果推荐MVC这种模式,大量的业务交给控制器处理,不臃肿才怪。

在实际开发中,我们通常会用#pragma mark来区分开各个部分的代码段,比如tableView的代理,处理键盘的通知等等。有一个比较简单的原则,当在控制器中出现了非常多的#pragma mark的时候就需要考虑如果将控制器分解了。

总体原则

分解控制器的方式,基本思路是定义新的对象来单独处理控制器里面的业务逻辑,简单来说就是把控制器里面的代码通过各种设计搬到另一个类里面而已。

独立出数据源

在使用TableView的过程中,我们肯定需要一个数据源,通常情况下是一个NSArray或者NSMutableArray的数组,关于这点,我们可以定义一个数据源管理对象来管理关于数据源的操作。比如当数据更新的时候通知TableView刷新,快速获取IndexPath对应的元素等等。有时候我们不仅仅只有个Section,做得通用一点,应该把对应的section也传递过去。其实就是一个字典。字典的Key就是section,字典的值就是每个section的数组。或者简单一点,用一个section数组来实现,数组里面就是存的元素数组。

说了这么多来看看例子就知道了

举一个简单的例子

#import "DataSourceObject.h"

@interface DataSourceObject ()

/**
 *  section数组,里面存的是每个section的数组
 */
@property (nonatomic, strong) NSArray *sectionedObjects;

@end

@implementation DataSourceObject
/**
 *  初始化一个数据源对象(一般在网络请求完之后,传入解析之后的数组)
 *
 *  @param objects       section数组,注意数组里面的元素是section里面的数组
 *  @param sectioningKey 对传入数组的标识
 *
 *  @return 数据源对象
 */
- (instancetype)initWithObjects:(NSArray *)objects sectioningKey:(NSString *)sectioningKey {
    self = [super init];
    if (!self) return nil;
    
    [self sectionObjects:objects withKey:sectioningKey];
    
    return self;
}
- (void)sectionObjects:(NSArray *)objects withKey:(NSString *)sectioningKey {
    self.sectionedObjects = objects;
}

- (NSUInteger)numberOfSections {
    return self.sectionedObjects.count;
}

- (NSUInteger)numberOfObjectsInSection:(NSUInteger)section {
    return [self.sectionedObjects[section] count];
}

/**
 *  根据indexPatch返回具体的对象
 *
 *  @param indexPath indexPath
 *
 *  @return 具体的对象
 */
- (id)objectAtIndexPath:(NSIndexPath *)indexPath {
    return self.sectionedObjects[indexPath.section][indexPath.row];
}
@end

当数据源被设计为高度抽象之后,我们在项目里面很多地方都可以使用了。将数据和索引的管理独立开来或许是一种不错的方式。尤其是在一些动态的TableView,用一个数据源对象通知控制器去更新数据非常好。

其实在实际项目中,我们更多的是在网络请求完成之后,将解析的数据传入数据源对象初始化,然后通知TableView该刷新了。

控制器中包含子控制器

其实早在iOS5的时候,苹果就提供了控制器能够被控制器包含的API.如果控制器能够被分解成几个独立的逻辑单元,可以考虑使用这种我们不常用的方式。

比如一个控制器需要显示一个TalbeView和一个UICollection,这个时候我们可以通过懒加载来加在两个分解的子控制器,然后在viewDidLayoutSubviews方法中去布局两个子控制器。

简单示例代码

- (XLHeaderViewController *)headerViewController {
    if (!_headerViewController) {
        XLHeaderViewController *headerViewController = [[XLHeaderViewController alloc] init];
        
        [self addChildViewController:headerViewController];
        [headerViewController didMoveToParentViewController:self];
        
        [self.view addSubview:headerViewController.view];
        
        self.headerViewController = headerViewController;
    }
    return _headerViewController;
}

- (XLGridViewController *)gridViewController {
    if (!_gridViewController) {
        XLGridViewController *gridViewController = [[XLGridViewController alloc] init];
        
        [self addChildViewController:gridViewController];
        [gridViewController didMoveToParentViewController:self];
        
        [self.view addSubview:gridViewController.view];
        
        self.gridViewController = gridViewController;
    }
    return _gridViewController;
}
// Called just after the view controller's view's layoutSubviews method is invoked. Subclasses can implement as necessary. The default is a nop.
// 摘至API的解释
- (void)viewDidLayoutSubviews {
    [super viewDidLayoutSubviews];
    
    CGRect workingRect = self.view.bounds;
    
    CGRect headerRect = CGRectZero, gridRect = CGRectZero;
    CGRectDivide(workingRect, &headerRect, &gridRect, 44, CGRectMinYEdge);
    
    self.headerViewController.view.frame = tagHeaderRect;
    self.gridViewController.view.frame = hotSongsGridRect;
}

这种方式其实也有变体,如果这里我们不是用控制器来分解,而是直接通过UIView来分解会是怎么样呢?也就是我们把业务逻辑也可以写到子视图中,这种方式其实自己很早就用了。也就是没有按照严格的MVC方式来组织代码。仔细想想其实控制器和UIView的区别是什么就能够理解什么不能用子视图的方式来分解了。比如视图不能实现页面跳转,但是同样可以解决呀,大不了在每个子视图中定义个控制器来保存他所在的控制器就OK了。

一部小心就扯远了。实用就行了。如果按照这种分解的思路,一层一层下去,控制器根本不会臃肿。

减少在控制器定义视图属性

不知道大家有没有这种习惯,也就是在控制器中喜欢把上面的子视图定义在控制器中。这种方式并不是很好。常见的是把相关视图属性定义在一个新的视图中。然后在这个视图中初始化,布局的。然后控制器通过添加子视图的方式把新定义的视图添加的控制器的视图上,或者将新定义的视图在loadView的时候作为控制器的视图。

简单代码示例

@implementation XLProfileViewController

- (void)loadView {
    self.view = [XLProfileView new];
}
- (void)viewDidLoad {
    [super viewDidLoad];
    [self.view addSubview:[XLProfileView new]]
}

// 或者
//- (void)viewWillAppear:(BOOL)animated {
//    [super viewWillAppear:animated];
//    [self.view addSubview:[XLProfileView new]];
//}
@end

@implementation XLProfileView : NSObject

- (UILabel *)nameLabel {
    if (!_nameLabel) {
        UILabel *nameLabel = [UILabel new];
        //配置相关属性
        [self addSubview:nameLabel];
        self.nameLabel = nameLabel;
    }
    return _nameLabel;
}

- (UIImageView *)avatarImageView {
    if (!_avatarImageView) {
        UIImageView * avatarImageView = [UIImageView new];
        [self addSubview:avatarImageView];
        self.avatarImageView = avatarImageView;
    }
    return _avatarImageView
}

- (void)layoutSubviews {
    //布局
}

@end

让控制和模型数据独立

这种方式自己在项目中没有怎么用到,不过也是一种不错的参考。起核心思想就是在控制器和模型数据之间增加一层presenter对象。这样让控制器不能直接访问数据模型,而是通过presenter来获得需要显示的数据。好处在于这样的控制器更加复用并且数据模型的改变并不会对控制器造成多大的影响。

还有一点值得提的,那就是我们可以在presenter中对数据进行进一步处理,然后返回给控制器需要的,直接可以使用的数据。

还是来看例子

@implementation XLUserPresenter : NSObject

- (instancetype)initWithUser:(XLUser *)user {
    self = [super init];
    if (!self) return nil;
    _user = user;
    return self;
}
// 返回控制器需要的数据,控制得到关心的数据
- (NSString *)name {
    // 可以增加对数据合法性的过滤
    return self.user.name;
}

- (NSString *)followerCountString {
    if (self.user.followerCount == 0) {
        return @"";
    }
    return [NSString stringWithFormat:@"%@ followers", [NSNumberFormatter localizedStringFromNumber:@(_user.followerCount) numberStyle:NSNumberFormatterDecimalStyle]];
}

- (NSString *)followersString {
    NSMutableString *followersString = [@"Followed by " mutableCopy];
    [followersString appendString:[self.class.arrayFormatter stringFromArray:[self.user.topFollowers valueForKey:@"name"]];
     return followersString;
}
     
+ (TTTArrayFormatter*) arrayFormatter {
         static TTTArrayFormatter *_arrayFormatter;
         static dispatch_once_t onceToken;
         dispatch_once(&onceToken, ^{
             _arrayFormatter = [[TTTArrayFormatter alloc] init];
             _arrayFormatter.usesAbbreviatedConjunction = YES;
         });
         return _arrayFormatter;
}
     
@end

这种方式比较简单而且也比较实用。只不过稍微麻烦一点,代码多一点,但是从架构上还是值得参考的。

数据绑定

谈到数据绑定,自己都感觉有些高大上了,其实不然。非常好理解,由于Cocoa框架天生就有KVO,KVC这种机制,所以我们能够很简单的实现当数据更新之后,对应的视图也改变。通过使用KVC,能够从数据模型中读取或者写入属性这点在数据绑定中非常重要。很出名的ReactiveCocoa同样是属于数据绑定的方式,但是对应一些简单的需求来说太过于庞大了。

将数据绑定和上面讲的让控制和模型数据独立中间增加presenter结合,是不是可以发生些有趣的事情。使用一个对象来传递值,一个用来更新视图,这样的方式是不是可以玩一玩呢。O(∩_∩)O哈哈~

来看例子

@implementation XLProfileBinding : NSObject

// 通过present和需要绑定的视图初始化
- (instancetype)initWithView:(XLProfileView *)view presenter:(XLUserPresenter *)presenter {
    self = [super init];
    if (!self) return nil;
    _view = view;
    _presenter = presenter;
    return self;
}

// 绑定需要及时通知视图上控制更新的值,及其对应在present的属性
- (NSDictionary *)bindings {
    return @{
             @"name": @"nameLabel.text",
             @"followerCountString": @"followerCountLabel.text",
             };
}

// 更新视图
- (void)updateView {
    [self.bindings enumerateKeysAndObjectsUsingBlock:^(id presenterKeyPath, id viewKeyPath, BOOL *stop) {
        id newValue = [self.presenter valueForKeyPath:presenterKeyPath];
        [self.view setObject:newvalue forKeyPath:viewKeyPath];
    }];
}

@end

想想在什么时候我们使用KVO呢?相信你已经猜到,我们是检测数据改变,那直接在present的中使用KVO。然后在调用更新视图的方法就可以了。

剥离控制器中的代理

这种方式自己在项目中实际使用过。在控制中,臃肿的控制器大部分都出现了很多**.delegate = self类似的代码,把代理都放在了控制中实现。比如常见的代理,TableView的,ActionSheet的,TextView的,还有我们的一大堆自定义代理。

是不是有同感。

我们完全可以把这些代理的处理,定义为代理对象。然后再控制器中设置代理的时候就不是**.delegate = self而是**.delegate = 某某代理对象。注意这个时候的代理就需要用strong关键词了。具体原因自己想一下就知道了。😄

还有一点在代理中定义一个控制器属性存储代理是给哪个控制器用。因为在写代码方法中,我们很有可能需要访问控制器的某些属性。记住使用了这种方式的控制器需要用单例来获取哦

具体的代码这里就不上了。涉及到公司项目的一些源码。

还有一些不常用的方法

还是来看个简单的例子

@implementation XLProfileViewController

// 这是个点击了一个按钮之后需要弹出一个ActionSheet,之后根据ActionSheet点击的索引进一步厝里
- (void)followButtonTapped:(id)sender {
    // 初始化一个交互对象,其实就是一个把
    self.followUserInteraction = [[XLFollowUserInteraction alloc] initWithUserToFollow:self.user delegate:self];
    [self.followUserInteraction follow];
}

- (void)interactionCompleted:(XLFollowUserInteraction *)interaction {
    [self.binding updateView];
}

//...

@end

@implementation XLFollowUserInteraction : NSObject <UIAlertViewDelegate>

- (instancetype)initWithUserToFollow:user delegate:(id<InteractionDelegate>)delegate {
    self = [super init];
    if !(self) return nil;
    _user = user;
    _delegate = delegate;
    return self;
}

- (void)follow {
    [[[UIAlertView alloc] initWithTitle:nil
                                message:@"Are you sure you want to follow this user?"
                               delegate:self
                      cancelButtonTitle:@"Cancel"
                      otherButtonTitles:@"Follow", nil] show];
}

- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex {
    if  ([alertView buttonTitleAtIndex:buttonIndex] isEqual:@"Follow"]) {
        [self.user.APIGateway followWithCompletionBlock:^{
            [self.delegate interactionCompleted:self];
        }];
    }
}

似乎这种方式就是所谓的交互模式,对咬文嚼字不是很擅长,大致讲讲使用的场景吧。比如有一个代理在控制器中实现起来比较复杂,代码量比较多,就可以用这种代理转换的方式。换到其他代理中去执行。

个人感觉这种方式有时候还是挺有用的。

写在最后

如何分解臃肿的控制器方法应该有很多。但是本质都是减少控制器的职责,将这些职责放到其他对象中,比如上面讲的,分离代理,隔离数据源增加present等。只要抓住了本质,其实大体来看来都差不多。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,711评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,932评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,770评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,799评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,697评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,069评论 1 276
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,535评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,200评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,353评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,290评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,331评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,020评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,610评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,694评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,927评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,330评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,904评论 2 341

推荐阅读更多精彩内容