8种模式帮你告别Massive View Controller

查看原文

ViewController做了太多事情导致变得越来越庞大。键盘事件,用户输入,数据的转换,视图初始化等等,他们真的是ViewController应该做的么?哪些需要委托给其他对象?本文我们就研究下如何让他们各司其职,这可以把大量复杂的代码拆分开,让你的代码更具可读性。

在ViewController中,我们可能是通过#pragma mark来把代码块儿进行分组。如果真的是上述说的这样,那你是该想想如何解耦你的ViewController了。

Data Source(数据源)

数据源模式是隔离对象背后的逻辑代码的一种模式,尤其是在复杂的TableView中,移除ViewController里的大量的比如,“在这种条件下,哪些cell是可见的?” 逻辑代码,是很有用的。如果你的tableview代码里不断的比较[row count]和[section count]的话,那么你可以考虑采用这种模式。

Data Source对象可以遵循UITableViewDataSource协议,但我发现用这些对象来配置cell与管理indexPath,职责并不一样,所以我倾向于把他们分开,各司其职。

一个简单的处理分段逻辑的的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@implementation SKSectionedDataSource : NSObject
- (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 = //section the objects array
}
- (NSUInteger)numberOfSections {
return self.sectionedObjects.count;
}
- (NSUInteger)numberOfObjectsInSection:(NSUInteger)section {
return [self.sectionedObjects[section] count];
}
- (id)objectAtIndexPath:(NSIndexPath *)indexPath {
return self.sectionedObjects[indexPath.section][indexPath.row];
}
@end

虽然数据源设计成抽象并且可复用,不要担心你的数据源从同一处调用。从ViewController中分离indexPath逻辑是个很崇(diao)高(bao)的目标。尤其是高度动态化的tableView,用一个object来告诉ViewController类似于“hey,我有一个新对象在这个indexPath中”是非常棒的,然后ViewController就能够通过动画的形式来让tableview展示出来。

这种模式也可以封装你的检索逻辑,一个远程的数据源能够从API中获取数据对象集合。然而UIViewController是与UI打交道的控制器,把与UI无关的网络代码从你的ViewController中分离是非常不错的方式。

如果你的所有数据源的接口都是稳定的(例如通过协议),那么你可以写一个特定的数据源,由其他数据源抽象组成的,多个数据源中每个子数据源负责自己的部分。使用这种逻辑来结合数据源是一个很棒的方式,能够避免写出可怕的索引比较的代码。数据源将为你管理一切

Standard Composition

多个视图控制器的组合可以参考iOS5中曾介绍过的View Controller Containment APIs来设计,如果你的view controller由若干个逻辑单元组成,这些逻辑单元拥有他们独立的ViewController,考虑使用Composition来解耦他们。最简单实际立即见分晓的方法就是多tableview或者多collectionview的应用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
- (SKHeaderViewController *)headerViewController {
if (!_headerViewController) {
SKHeaderViewController *headerViewController = [[SKHeaderViewController alloc] init];
[self addChildViewController:headerViewController];
[headerViewController didMoveToParentViewController:self];
[self.view addSubview:headerViewController.view];
self.headerViewController = headerViewController;
}
return _headerViewController;
}
- (SKGridViewController *)gridViewController {
if (!_gridViewController) {
SKGridViewController *gridViewController = [[SKGridViewController alloc] init];
[self addChildViewController:gridViewController];
[gridViewController didMoveToParentViewController:self];
[self.view addSubview:gridViewController.view];
self.gridViewController = gridViewController;
}
return _gridViewController;
}
- (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;
}

这些subview型的controller,数据源统一,自己维护自己的collectionview。这是一种容易理解也更聪明的实现方式。

Smarter Views

如果你正在把subviews的初始化全放在ViewController里时,你可以考虑下Smart Views来实现。UIViewController默认使用UIView来作为控制器的view属性,但是你可以用你自己的view复写!通过调用-loadView方法来做这件事。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@implementation SKProfileViewController
- (void)loadView {
self.view = [SKProfileView new];
}
//...
@end
@implementation SKProfileView : NSObject
- (UILabel *)nameLabel {
if (!_nameLabel) {
UILabel *nameLabel = [UILabel new];
//configure font, color, etc
[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 {
//perform layout
}
@end

你仅仅需要重新声明一下你的view @property (nonatomic) SKProfileView *view,由于属于自定义UIView,iOS会假设self.view是SKProfileView类型,并正确的处理它,这就是所谓的Covariant return type(没找到中文翻译,大概意思是基类中某个函数在派生类中可以override,并且返回值得是基类中那个函数返回值的子类),这是一种很有效的设计模式。(注意:编译器需要知道你的类继承于UIView,所以请确保你的这个类头文件是import过来的而不是@class这样的前置声明!在Xcode6.3以后你也可以将property声明为dynamic,相应的在实现文件中变为@dynamic)

Presenter

Presenter模式包裹一层model对象,这个model对象可以变换属性来用于显示和传递消息。与总所周知的Presentation Model, the Exhibit pattern, 和 ViewModel异曲同工。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@implementation SKUserPresenter : NSObject
- (instancetype)initWithUser:(SKUser *)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

但是要注意,model对象本身不要被暴露出来。Presenter对于model来说更像是看门大爷,这保证了ViewController不能跨过Presenter来直接访问model。这种架构能够降低依赖关系,因为与SKUser有关系的类会很少,这样修改model对app造成的影响很小。

Binding pattern(绑定模式)

从方法形式上来看,可能会叫-configureView。 当数据变化时,绑定模式会根据这个变化更新view。Cocoa与生俱来,因为KVO能够检测model,KVC能够读取model信息,写入view。Cocoa Bindings是这种模式的AppKit版本。像Reactive-Cocoa的第三方页也是这种模式,但也有可能会大材小用。

这种模式结合Presenter模式配合起来很爽,用一个对象转换值,另一个来响应视图。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@implementation SKProfileBinding : NSObject
- (instancetype)initWithView:(SKProfileView *)view presenter:(SKUserPresenter *)presenter {
self = [super init];
if (!self) return nil;
_view = view;
_presenter = presenter;
return self;
}
- (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

(注意,上面的presenter并不必须支持KVO的,但可以设计成支持KVO)

你不必在一次尝试中就结合上述模式找到完美的架构。也不用区担心只适用于一种模式,目标不是去简单的复用代码,而是是我们的类简单清晰,这样才能易于维护和理解。

交互模式

使ViewController变得臃肿难以维护的另一个原因是简单的一行代码actionSheet.delegate = self。 在Smaltalk语言里,controller所充当的全部的角色仅仅是获取用户输入,响应视图与更新model。如今的交互模式的设计让ViewController重的代码更加的复杂,庞大。

交互设计通常包括用户输入(例如按钮的点击),选择性输入(例如提示框询问用户选择),和一些其他的行为活动,例如网络请求,状态变更等等。这整个的生命周期都可以囊括在Interaction Object里。下面的例子是当点击按钮出发时创建了一个Interaction Object,但是如果你把Interaction Object作为行为的响应(类似这种:[button addTarget:self.followUserInteraction action:@selector(follow)]),也是可行的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
@implementation SKProfileViewController
- (void)followButtonTapped:(id)sender {
self.followUserInteraction = [[SKFollowUserInteraction alloc] initWithUserToFollow:self.user delegate:self];
[self.followUserInteraction follow];
}
- (void)interactionCompleted:(SKFollowUserInteraction *)interaction {
[self.binding updateView];
}
//...
@end
@implementation SKFollowUserInteraction : 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];
}];
}
}
@end

老的alertview和actionsheet的代理模式能够让这种模式更容易展示出优势,但在iOS8UIAlertController APIs下也能玩的很6

Keyboard Manager

当keyboard状态改变更新视图,这是大家在ViewController中要处理的典型的棘手问题,但是如果转换成Keyboard Manager会容易不少。像GitHub上的IQKeyboardManager这种,能够工作在app的任何页面上,但是仍要注意,如果是大材小用,那么你大可以自己写一个符合自身业务的mini版本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@implementation SKNewPostKeyboardManager : NSObject
- (instancetype)initWithTableView:(UITableView *)tableView {
self = [super init];
if (!self) return nil;
_tableView = tableView;
return self;
}
- (void)beginObservingKeyboard {
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardDidHide:) name:UIKeyboardDidHideNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillShow:) name:UIKeyboardWillShowNotification object:nil];
}
- (void)endObservingKeyboard {
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardDidHideNotification object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillShowNotification object:nil];
}
- (void)keyboardWillShow:(NSNotification *)note {
CGRect keyboardRect = [[note.userInfo objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue];
UIEdgeInsets contentInsets = UIEdgeInsetsMake(self.tableView.contentInset.top, 0.0f, CGRectGetHeight(keyboardRect), 0.0f);
self.tableView.contentInset = contentInsets;
self.tableView.scrollIndicatorInsets = contentInsets;
}
- (void)keyboardDidHide:(NSNotification *)note {
UIEdgeInsets contentInset = UIEdgeInsetsMake(self.tableView.contentInset.top, 0.0f, self.oldBottomContentInset, 0.0f);
self.tableView.contentInset = contentInset;
self.tableView.scrollIndicatorInsets = contentInset;
}
@end

你可以在-viewDidAppear-viewWillDisappear调用-beginObservingKeyboard-endObservingKeyboard方法,或者任何合适的地方。

屏幕间的切换一般通过-pushViewController:animated:的调用来实现,由于这种切换变得越来越繁琐,你可以委托给Navigator对象来完成。
尤其是在iPad和iPhone通用的app里,navigation可能需要根据你的设备适配。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@protocol SKUserNavigator <NSObject>
- (void)navigateToFollowersForUser:(SKUser *)user;
@end
@implementation SKiPhoneUserNavigator : NSObject<SKUserNavigator>
- (instancetype)initWithNavigationController:(UINavigationController *)navigationController {
self = [super init];
if (!self) return nil;
_navigationController = navigationController;
return self;
}
- (void)navigateToFollowersForUser:(SKUser *)user {
SKFollowerListViewController *followerList = [[SKFollowerListViewController alloc] initWithUser:user];
[self.navigationController pushViewController:followerList animated:YES];
}
@end
@implementation SKiPadUserNavigator : NSObject<SKUserNavigator>
- (instancetype)initWithUserViewController:(SKUserViewController *)userViewController {
self = [super init];
if (!self) return nil;
_userViewController = userViewController;
return self;
}
- (void)navigateToFollowersForUser:(SKUser *)user {
SKFollowerListViewController *followerList = [[SKFollowerListViewController alloc] initWithUser:user];
self.userViewController.supplementalViewController = followerList;
}

上述代码的一个好处是将一个大的对象分解成很多小对象,更容易快速的修改,重写。取代了贯穿你ViewController的条件判断代码,你只需要创建一个self.navigator = [SKiPadUserNavigator new],然后调用相同的-navigateToFollowersForUser:方法即可。

最后

从历史来看,Apple的SDK仅仅包含最小限度的组件,他们的API则是Massive View Controller的元凶。通过分析梳理ViewController的职责,将代码抽象分离出来,创建真正的只有单一职责的对象,这样我们就能够开始把控那些庞大复杂的类,让他们便于管理了。

显示评论