UIViewController的瘦身計劃(iOS架構思想篇)

前言

這篇文章里會涉及如下幾個方面:

  • 1、代碼的組織結構,以及為何要這樣寫。
  • 2、那些場景適合使用子控制器,那些場景應該避免使用子控制器?
  • 3、分離UITableView的數據源和UITableViewDataSource協(xié)議。
  • 4、MVVM的重點是ViewModel,不是響應函數式。
  • 5、MVVM中,ReactiveCocoa或RXSwift實現數據綁定的帶來的弊端。
  • 6、用策略模式替代if-elseswitch這樣判斷比較多,不利于代碼閱讀的分支結構。并在特定場景下,用策略模式解決模塊調用問題。
  • 7、為什么要較少模塊間跨層數據交流。

一、代碼結構

在說控制器瘦身之前,首先要做的的是保證代碼結構的清晰化。良好的代碼結構有利于代碼的傳承、可讀性以及可維護性。通常筆者都是這樣控制代碼結構的:

#pragra mark - life cycle 

#pragra mark - notification 

#pragra mark - action 

#pragra mark - UITableViewDelegate
.....總之這里是各種代理就對了

#pragra mark - UI

#pragra mark - setter & getter
  • 1、不要在除了getter之外的結構中設置view基本坐標、屬性等。
  • 2、在viewDidAppear里面做Notification的監(jiān)聽之類的事情。
  • 3、每一個代理方法都對應上相應的協(xié)議,否則后期隨著代碼量的增加,很難找出某一代理方法對應的協(xié)議,不利于代碼的可讀性。
  • 4、gettersetter 方法寫在代碼的最后面。
  • 5、getter方法中不要添加比較重要重要的業(yè)務邏輯,重要的業(yè)務邏輯應該單獨拿出來,放在對應的pragra mark 下,否則對于代碼的閱讀者來說,比較難以定位邏輯的入口位置。實際開發(fā)中遇到過多次這樣的情況,焦頭爛額的尋找關鍵邏輯入口處,縱里尋她千百度,結果它卻躺在 getter方法中。
  • 6、UI布局可以說比較重要,也可以說不重要。重要是因為一個新手接手新項目,如果對布局還沒有了解,業(yè)務邏輯便無從談起;UI布局不重要是因為只要相關控件封裝的足夠好,頁面UI布局通常會很簡單;因為UI布局比較重要,所以筆者將它放在固定位置(setter&getter上面),因為UI布局通常比較簡單,所以將其放在代碼中比較靠后的位置。

二、 關于子控制器

對于相對比較復雜的界面,通常情況下還可以考慮添加子控制的實現方式。如實際開發(fā)中,在商品搜索模塊中,將歷史搜索標簽和推薦搜索標簽、搜索推薦詞條以及搜索結果用三個控制器分別承載不同的邏輯,是不同的代碼邏輯分離。

優(yōu)點:
把和該元素相關的業(yè)務邏輯分解一部分到子控制器中,主業(yè)務邏輯對應的代碼量瞬間減少很多,代碼封裝和分離十分清晰。

缺點:
這種做法最大的缺點就是父控制器和子控制器之間的消息傳遞有時需要做額外的處理,尤其是子控制器的消息回調。

所以,建議根據實際情況有選擇的考慮,如果父控制器和子控制器之間的消息交互較少,完全可以考慮此種方式。如果父控制器和子控制器之間的消息交互較多,建議仔細考慮清楚再做取舍。

實際開發(fā)中,蘋果專門提供了一個UITableViewController類,專門為tableView服務,但是實際開發(fā)中很少有人直接使用。該控制相對于普通的UIViewController的而言,直接實現了下拉刷新功能;除此之外,還能切換編輯模式、響應鍵盤通知。如果UITableViewController實現的標準剛好同你項目中的tableView一些需求很類似,就可以直接通過使用子控制器的方式,避免了寫那些重復的代碼。當然,實際開發(fā)中出現這種事情的概率非常小。這里僅是簡單提示下。

三、UITableView 的瘦身

絕大多數情況下,只要有控制器就會存在UITableView或UICollectionView(這里僅僅以tableView為例),所以對UITableView 的瘦身尤為重要。以下的分析主要參照該文章。

3.1 拆分出不重要的東西

毫無疑問,在Controller層中協(xié)調View和Model的工作是無法拆除的。那么除此之外,不是必須有Controller層承載的內容便可以被拆除,比如tableView的數據源和UITableViewDataSource協(xié)議。下面分兩種情況說明,一種是將數據源和UITableViewDataSource協(xié)議都拆分出來,另一種是只拆分數據源。

3.1.1 單一的cell和數據源(拆分數據源和UITableViewDataSource協(xié)議)

關于這種情況,文章中實現代碼如下這樣。

//控制器中代碼
TableViewCellConfigureBlock configureCell = ^(PhotoCell *cell, Photo *photo) {
        [cell configureForPhoto:photo];
    };
    NSArray *photos = [AppDelegate sharedDelegate].store.sortedPhotos;
    self.photosArrayDataSource = [[ArrayDataSource alloc] initWithItems:photos cellIdentifier:PhotoCellIdentifier configureCellBlock:configureCell];
    self.tableView.dataSource = self.photosArrayDataSource;
    [self.tableView registerNib:[PhotoCell nib] forCellReuseIdentifier:PhotoCellIdentifier];
//抽離的數據源代碼
//.h文件
typedef void (^TableViewCellConfigureBlock)(id cell, id item);
@interface ArrayDataSource : NSObject <UITableViewDataSource>
- (id)initWithItems:(NSArray *)anItems
     cellIdentifier:(NSString *)aCellIdentifier
 configureCellBlock:(TableViewCellConfigureBlock)aConfigureCellBlock;
- (id)itemAtIndexPath:(NSIndexPath *)indexPath;
@end
//.m文件
#import "ArrayDataSource.h"
@interface ArrayDataSource ()
@property (nonatomic, strong) NSArray *items;
@property (nonatomic, copy) NSString *cellIdentifier;
@property (nonatomic, copy) TableViewCellConfigureBlock configureCellBlock;
@end
@implementation ArrayDataSource
- (id)init{
    return nil;
}
- (id)initWithItems:(NSArray *)anItems
     cellIdentifier:(NSString *)aCellIdentifier
 configureCellBlock:(TableViewCellConfigureBlock)aConfigureCellBlock{
    self = [super init];
    if (self) {
        self.items = anItems;
        self.cellIdentifier = aCellIdentifier;
        self.configureCellBlock = [aConfigureCellBlock copy];
    }
    return self;
}
- (id)itemAtIndexPath:(NSIndexPath *)indexPath{
    return self.items[(NSUInteger) indexPath.row];
}
#pragma mark UITableViewDataSource
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
    return self.items.count;
}

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

上述代碼將tableView的數據源以及UITableViewDataSource協(xié)議都抽離到ArrayDataSource中,而UITableViewDelegate依然保留在Controller層中。至于數據源的來源(實際中往往是從網絡獲取),這里主要是通過Store類獲取數據,具體體現代碼NSArray *photos = [AppDelegate sharedDelegate].store.sortedPhotos;,該行代碼你可以理解為實際項目中的網絡請求的偽代碼。

3.1.2 多種數據源和多種cell(僅拆分數據源和Protocols)

同時拆分數據源和UITableViewDataSource協(xié)議這種方式有一定的局限性,如果一個tableView中有多類型cell,下面的這個方法將很難設計,尤其是針對參數aCellIdentifieaConfigureCellBlock。所以針對這種情況僅僅將tableView 的dataSource拆出來即可,實際這種拆分情況就是MVVM模式中的ViewModel。

- (id)initWithItems:(NSArray *)anItems
     cellIdentifier:(NSString *)aCellIdentifier
 configureCellBlock:(TableViewCellConfigureBlock)aConfigureCellBlock;
3.1.3 網絡層放在那里?

按照該文章的意思,網絡層可以在封裝之后放在Cotroller中,這種方案當然是可以。除此之外,按照MVVM的設計模式,網絡層同樣可以放在ArrayDataSource類中,該類對外提供網絡請求接口,數據返回后,同步更新內部的數據源即可。

3.2 cell內部控制cell狀態(tài)

通??刂芻ell的狀態(tài)我們可以實現如下兩個代理方法。

- (void)tableView:(UITableView *)tableView
        didHighlightRowAtIndexPath:(NSIndexPath *)indexPath{
    PhotoCell *cell = [tableView cellForRowAtIndexPath:indexPath];
    cell.photoTitleLabel.shadowColor = [UIColor darkGrayColor];
    cell.photoTitleLabel.shadowOffset = CGSizeMake(3, 3);
}
- (void)tableView:(UITableView *)tableView
        didUnhighlightRowAtIndexPath:(NSIndexPath *)indexPath{
    PhotoCell *cell = [tableView cellForRowAtIndexPath:indexPath];
    cell.photoTitleLabel.shadowColor = nil;
}

按照這種方式會純在兩個缺點:

  • 1、這兩個代理方法增加了Controller的代碼量。
  • 2、在Controller中顯示通過tableView獲取cell,再調用cell實現的細節(jié)方法。思路相對比較繞。
    綜上所述,文章中是在cell中控制cell的狀態(tài)。代碼如下。
- (void)setHighlighted:(BOOL)highlighted animated:(BOOL)animated{
    [super setHighlighted:highlighted animated:animated];
    if (highlighted) {
        self.photoTitleLabel.shadowColor = [UIColor darkGrayColor];
        self.photoTitleLabel.shadowOffset = CGSizeMake(3, 3);
    } else {
        self.photoTitleLabel.shadowColor = nil;
    }
}

3.3 cell 初始化和更新分離

另外一種好的做法就是將cell的根據model更新的方法,拆分到分類中完成。實際開發(fā)中,可能存在復雜的cell代碼量很大,此時可以借助分類的方法分離關注點。

3.4 簡單談談MVVM

  • 1、MVVM本質上也是從MVC中派生出來的思想,由M、V、VM、V四部分組成,主要是為了減少MVC中Controller承擔的負荷。
  • 2、借助ViewModel可以降低View和Model的耦合度。
  • 3、雖然ViewModel是MVVM組成的一部分,但是MVC中依然能用,上述分離tableView的dataSource就是很好的說明。MVVM的關鍵是要有ViewModel。而不是ReactiveCocoa、RXSwift或RXJava等。
  • 4、ReactiveCocoa或RXSwift只是能更好的體現能更好地體現MVVM的精髓。使用函數響應式框架能更好的實現數據和視圖的雙向綁定(ViewModel的數據可以顯示到View上,View上的操作同樣會引起ViewModel的變化),降低了ViewModel和View的耦合度。
  • 5、ReactiveCocoa或RXSwift不應該因為他本身難以被理解而被神化。通過這兩個框架可以實現ViewModel和View的雙向綁定,但同樣會存在幾個比較重大的問題。 首先,ReactiveCocoa或RXSwift的學習成本很高;其次,數據綁定使得 Bug 很難被調試,當界面出現異常,可能是View的問題,也可能是數據ViewModel的問題。而數據綁定會使一個位置的bug傳遞到其他位置,難以定位;最后,數據綁定是需要消耗更多的內存,對于大型項目更是如此。只是結合自己所學知識談談理解,如果對RXSwift感興趣,推薦這個鏈接。

四、合理拆分模塊

4.1 模塊拆分大小要合理

如果模塊被拆分的太粗糙,基本就是簡單的封裝,并沒有進一步細化,只是將所有的功能集中在一起,這樣做似乎沒有太大意義

如果模塊被拆分的很細,Controller中很執(zhí)行相關模塊的功能就要調用相關模塊代碼,似乎代碼量并不會減少太多。比如在做即時通信應用開發(fā)時,支持的消息類型有文字、語音、圖片、視頻消息。其中后三種消息類型同文字消息不同,后三者要求發(fā)送消息的時候,首先要像后臺請求上傳資源的權限,獲取上傳資源權限后,返回對應的字段(該字段以實際情況不同,可能是id,也可能是token之類的),上傳成功后獲取資源對應的URL,再把資源的URL通過類似文字消息的發(fā)送方式發(fā)送出去。此時,可以拆分成三個模塊數據發(fā)送(叫A模塊)、上傳資源申請(叫B模塊)、內容上傳(叫C模塊)。如果要發(fā)送文字消息,直接在Controller中調用模塊A即可;但是如果想發(fā)送其他消息,就要依次調用模塊B、模塊C、模塊A,按照這種調用方式,Controller必然會膨脹。

4.2 策略模式

在說合理拆分模塊之前,先簡單說下策略模式,因為接下來舉的例子中涉及策略模式。
策略模式一般是指:

1. 可以實現目標的方案集合;
2. 根據形勢發(fā)展而制定的行動方針和斗爭方法;
3. 有斗爭藝術,能注意方式方法。

switch,if-else之類的分支語句,此類語句給人的直觀感覺是判斷條件明確,代碼層次清晰,缺點可能是代碼繁瑣,雜亂無章,而且拆分困難。特別是到后期維護代碼的時候,這種狀況往往令人有食之無味,棄之可惜的感覺。使用策略模式可以代替switch或if-else之類的代碼。
舉個例子,以下是小明的計劃安排:

    周一打籃球
    周二逛街
    周三洗衣服
    周四打游戲
    周五唱歌
    其他休息

借助策略模式我們可以這樣實現代碼:

@interface XiaoMing : NSObject
- (void)doSomethingWithDayStr:(NSString *)dayStr params:(NSDictionary *)paramsDict;
@end
#import "XiaoMing.h"
@interface XiaoMing()
@property(nonatomic,copy)NSDictionary *strategyDict;//策略
@property(nonatomic,copy)NSDictionary *paramDict;//參數
@end
@implementation XiaoMing
- (void)doSomethingWithDayStr:(NSString *)dayStr params:(NSDictionary *)paramsDict{
    self.paramDict = paramsDict;
    if (self.strategyDict[dayStr]){
        NSInvocation *doWhat = self.strategyDict[dayStr];
        [doWhat invoke];
    }else{
        [self sleep];
    }
}
- (NSInvocation *)invocationWithMethod:(SEL)selector{
    NSMethodSignature*signature = [[self class] instanceMethodSignatureForSelector:selector];
    if (signature == nil) {
        NSString *reason = [NSString stringWithFormat:@"提示:The method[%@] is not find", NSStringFromSelector(selector)];
        @throw [NSException exceptionWithName:@"錯誤" reason:reason userInfo:nil];
    }
    NSInvocation*invocation = [NSInvocation invocationWithMethodSignature:signature];
    invocation.target = self;
    invocation.selector = selector;
    NSDictionary *param = self.paramDict;
    //index表示第幾個參數,注意0和1已經被占用了(self和_cmd),所以我們傳遞參數的時候要從2開始。
    [invocation setArgument:&(param) atIndex:2];
    return invocation;
}
- (void)playBasketball:(NSDictionary *)dict{
    NSLog(@"方法:%s 參數:%@",__FUNCTION__,dict);
}
- (void)shopping:(NSDictionary *)dict{
    NSLog(@"方法:%s 參數:%@",__FUNCTION__,dict);
}
- (void)washClothes:(NSDictionary *)dict{
    NSLog(@"方法:%s 參數:%@",__FUNCTION__,dict);
}
- (void)playGames:(NSDictionary *)dict{
    NSLog(@"方法:%s 參數:%@",__FUNCTION__,dict);
}
- (void)sing:(NSDictionary *)dict{
     NSLog(@"方法:%s 參數:%@",__FUNCTION__,dict);
}
- (void)sleep{
     NSLog(@"這是其他情況:%s",__FUNCTION__);
}
- (NSDictionary *)strategyDict{
    if (_strategyDict == nil) {
        _strategyDict = @{
                          @"day1" : [self invocationWithMethod:@selector(playBasketball:)],
                          @"day2" : [self invocationWithMethod:@selector(shopping:)],
                          @"day3" : [self invocationWithMethod:@selector(washClothes:)],
                          @"day4" : [self invocationWithMethod:@selector(playGames:)],
                          @"day5" : [self invocationWithMethod:@selector(sing:)]
             };
    }
    return _strategyDict;
}
@end

外部調用可以完全不再使用if-else的判斷了。

XiaoMing *xm = [[XiaoMing alloc]init];
    //各種情況直接賦值給dayStr即可。
    NSString *dayStr = @"day3s";
    [xm doSomethingWithDayStr:dayStr params:@{@"key":@"test"}];

4.3 合理應用策略模式和組合方式解決上述4.2問題

關于上述問題我們可以通過組合和策略模式解決。首先創(chuàng)建一個MessageManager類。對外提供的接口大概是這樣的:

typedef NS_ENUM (NSUInteger, MessageSendStrategy){
    MessageSendStrategyText = 0,
    MessageSendStrategyImage = 1,
    MessageSendStrategyVoice = 2,
    MessageSendStrategyVideo = 3
}
@protocol MessageManagerDelegate<NSObject>
  @required
      - (void)messageSender:(MessageSender *)messageSender
      didSuccessSendMessage:(BaseMessage *)message
                   strategy:(MessageSendStrategy)strategy;

      - (void)messageSender:(MessageSender *)messageSender
         didFailSendMessage:(BaseMessage *)message
                   strategy:(MessageSendStrategy)strategy
                      error:(NSError *)error;
@end

@interface MessageManager:NSObject
@property (nonatomic, weak) id<MessageSenderDelegate> delegate;
@property(nonatomic,copy)NSDictionary *strategyDict;//主要在這里定義策略,內部通過Invoke喚起對應方法。
- (void)sendMessage:(BaseMessage *)message withStrategy:(MessageSendStrategy)strategy;
@end

外部調用形式大概是這樣的,除此之外還要遵守MessageManagerDelegate協(xié)議并實現協(xié)議方法。

[self.messageManager sendMessage:message withStrategy:MessageSendStrategyText];

MessageManager.m文件實現大概是這樣的:

@interface MessageManager()

@end

@implementation MessageManager
- (void)sendMessage:(BaseMessage *)message withStrategy:(MessageSendStrategy)strategy{
      .....
     if (self.strategyDict[@(strategy)]){
        NSInvocation *doWhat = self.strategyDict[@(strategy)];
        [doWhat invoke];
    }
    ......
    ......
}
......
......
@end

總的來說,基本形式和上面舉出的例子類似。

4.4 減少跨層數據交流

前面注意事項。那么接下來就是要使用模塊了,使用模塊的時候同樣有需要注意的地方:減少跨層數據交流。舉個例子,假如有模塊一、模塊二、模塊三,按照正常的調用方式是外部使用模塊一調用模塊二的方法,模塊二的方法再調用模塊三的方法。但是隨著模塊功能的完善,突然有一天出現模塊一直接調用模塊三的情況,那么后續(xù)就很難避免其他開發(fā)人員可能直接拿模塊一調用模塊三方法。類似這種跨層的數據交流很不利于項目的后續(xù)維護。

五、其它

除此之外,控件的合理拼裝也能在很大程度上減少控制器中的代碼。另外還有一個要注意的地方,就是UIViewController繼承的問題。關于這個問題,可以在筆者之前寫的這篇文章的第二部分內容找到答案。

六、補充一

在講 UIResponder 之前可以先溫習下響應鏈。腦海中想象一下這樣的視圖層級結構:一個 UIViewController 上放置一個 tableView(supTableView) , supTableView 的 cell 上放置一個 tableView(subTableView) , subTableView 的 cell 上有一個 UIButton 。即:

UIViewController -> SuperTable -> SuperCell -> SubTable -> SubCell -> UIButton

如果想在點擊UIButton的時候在UIViewController中產生回調,一般可以借助delegate或block實現,但是由于層級太深,這樣做的話會很繁瑣。明智的方式是借助UIResponder。

只需要一個 UIResponder 的 category 就行

@interface UIResponder (Router)
- (void)routerEventWithSelectorName:(NSString *)selectorName object:(id)object userInfo:(NSDictionary *)userInfo;
@end
@implementation UIResponder (Router)
- (void)routerEventWithSelectorName:(NSString *)selectorName object:(id)object userInfo:(NSDictionary *)userInfo {
    [[self nextResponder] routerEventWithSelectorName:selectorName object:object userInfo:userInfo];
}
@end

UIButton點擊事件

- (IBAction)btnClick:(UIButton *)sender {
    [self routerEventWithSelectorName:@"btnClick:userInfo:" object:sender userInfo:@{@"key":@"value"}];
}

七、補充二

在 Cell 內部獲取父控制器,在 Cell 內部調用控制器的一些耦合性比較小的代碼,一定程度上也能達到瘦身的目的。如在 Cell 中有個返回按鈕,需要當前父視圖控制器返回 Push 到它之前的控制器,那么就需要在自定義 Cell 中拿到當前的父視圖控制器做 Pop 操作。

- (UIViewController *)viewController {
    for (UIView* next = [self superview]; next; next = next.superview) {
        UIResponder *nextResponder = [next nextResponder];
        if ([nextResponder isKindOfClass:[UIViewController class]]) {
            return (UIViewController *)nextResponder;
        }
    }
    return nil;
}

小結

在說UIViewController的瘦身計劃之前,第一部分先說了合理的代碼結構;第二部分單提了下關于子控制器,并簡單的用UITableViewController舉了個例子;第三部分重點介紹了UITableView的瘦身,并因此引申出了MVVM的一些內容;第四部分主要介紹了一些模塊拆分中遇到的一個問題和解決方案,除此還說明了模塊跨層數據交流的問題;最后,提了下控件的拼裝和UIViewController繼承的問題。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容