如何解耦控制器(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閱讀 228,546評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,570評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,505評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,017評論 1 313
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,786評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,219評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,287評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,438評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,971評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,796評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,995評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,540評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,230評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,662評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,918評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,697評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,991評論 2 374

推薦閱讀更多精彩內容