帶目錄版請移步紙簡書生
前言:如果你維護老項目,項目里面的那些臃腫的控制印象應該很深吧。在原來上千行代碼里修改,新加代碼那感覺簡直了。??,今天就來看看可以用哪些方法去分解臃腫的控制器。
控制器變得臃腫,事實上也就是我們項目中的業務在版本迭代中不斷增加而導致的。加上蘋果推薦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等。只要抓住了本質,其實大體來看來都差不多。