如何解耦控制器(iOS)

作者: 纸简书生 | 来源:发表于2016-04-22 00:53 被阅读1180次

    带目录版请移步纸简书生

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

    控制器变得臃肿,事实上也就是我们项目中的业务在版本迭代中不断增加而导致的。加上苹果推荐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等。只要抓住了本质,其实大体来看来都差不多。

    相关文章

      网友评论

      • 资料库:能给个联系方式吗?有点问题想问一下
      • 52a49ce3c50a:用VIPER模式开发,可满足你的需求

      本文标题:如何解耦控制器(iOS)

      本文链接:https://www.haomeiwen.com/subject/fktxrttx.html