王绵杰的个人博客

如何把View Controller瘦下来!

如何把View Controller瘦下来

  • 有时候View Controller由于做了太多的事情,而变得非常庞大。这里既有数据的收集,又有逻辑的处理,还有各种归属于该View Controller的控件内存分配。这里面哪些可以代理到其他模块呢?这篇博客就是探索项目的架构,目的是分离复杂的代码逻辑,让我们的代码可读性更强。
  • 在View Controller里,这些职责或许是被各种 #pragma mark 分组实现,如果你是这样的话,那么就可以考虑把这些部分拆分到不同的文件里。

Data Source

  • Data Source的方式是一种拆离View Controller里数据显示逻辑的方式,尤其是在一些复杂的table views里,这种方式可以有效地从View Controller里分离所有cells的数据显示逻辑。
  • Data Source对象可以遵守UITableViewDataSource协议,以实现数据的显示,但是我发现使用这些对象配置cells是一件可以独立出来的逻辑,所以可以把这部分逻辑也独立出来。下面一个很简单的例子:

    @implementation TBSectionedDataSource : 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 = objects //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
    
  • 这种data source的设计是为了抽象和重用,不要担心你的类仅仅在一个地方使用。从view controller里分离数据显示逻辑是一种管理懒加载的方式。特别是针对一个动态table views来说,这种方式很适合view controller来管理显示数据。

  • 这种方式也可以管理你的重用逻辑。在这里可以获取服务器端的数据,从而把网络访问模块给分离出去。
  • 如果你的界面是静态的话,那么你可以定制一个data source类用来专门显示这一块。在多个data source的情况下,每一个data source的子类都可以在自己的section里显示。
  • 使用这种方式可以避免很多事情,把数据逻辑拆分的同时还可以把网络访问模块拆出来。

Standard Composition

  • 这个可以理解为标准化组合,多个View Controller可以使用View Controller容器管理起来,如果你的view controller由多个逻辑单元组成,那么可以把这种复杂的逻辑拆分到多个view controller中。经验表明这种方式适合一个界面有多个table view或者是多个collection view的情况。
  • 比如在一个界面上包含一个header和一个网格类型的视图,那么我们可以使用懒加载的方式加载这两个view controller,当系统用到的时候再去加载资源。

    - (TBHeaderViewController *)headerViewController {
        if (!_headerViewController) {
            TBHeaderViewController *headerViewController = [[TBHeaderViewController alloc] init];
              [self addChildViewController:headerViewController];
            [headerViewController didMoveToParentViewController:self];
    
            [self.view addSubview:headerViewController.view];
    
            self.headerViewController = headerViewController;
            }
            return _headerViewController;
    }
    
    - (TBGridViewController *)gridViewController {
        if (!_gridViewController) {
               TBGridViewController *gridViewController = [[TBGridViewController 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;
    }
    
  • 在结果子视图里,其包含的每个collection view,都展示统一的数据类型,这样更便于管理和修改。

    Smarter Views

  • 如果你是在view controller类初始化你所有的子视图的话,那么你应该考虑使用更适合自己的View。UIViewController默认使用UIView,不过同样你可以自定义View实现重写。使用-loadView来达到这种效果,在这里你只需要把自定义的View设置给self.view即可。

    @implementation TBProfileViewController
    
    - (void)loadView {
        self.view = [[TBProfileView alloc] init];
    }
    
    //...
    
    @end
    
    @implementation TBProfileView : NSObject
    
    - (UILabel *)nameLabel {
        if (!_nameLabel) {
            UILabel *nameLabel = [[UILabel alloc] init];
            //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
    

    Presenter

  • Presenter(一系列get方法)是从Model中获取数据并提供给View层,Presenter还负责处理后台任务
  • 主导器一般包含着model对象,这里的model是用来展示的,所以属性都是暴露出来的。
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
@implementation TBUserPresenter : NSObject
- (instancetype)initWithUser:(TBUser *)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的看门人,保证了view controller不用避开主逻辑服务,而可以直接访问model层。这种架构减少了依赖性,由于 TBUser 的存在,使得model接触的类比较少,因此如果它改变,则牵涉的逻辑比较少。

Binding pattern

  • 在形式上,这种可以看做-configureView。当数据层发生改变的时候捆绑形式就会更新view。Cocoa本身就适合这个,因为KVO可以检测到model层的变动,而KVC可以从model层读取数据然后赋给view,两者实现完美结合。第三方库Reactive Cocoa也是采用了这种方式,但它有点太庞大。
  • 这种方式与主导器结合起来效果非常好,一个创建对象来传递值,而另一个去接受然后显示到view上。
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 TBProfileBinding : NSObject
- (instancetype)initWithView:(TBProfileView *)view presenter:(TBUserPresenter *)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

interaction pattern

  • 有时候View Controller过于庞大会带来很多你意想不到的问题。View Controller的角色是接受用户操作然后更新views和相应的model。如今的交互变得越来越复杂化,并且还造成了很大的代码冗余。
  • 交互常包括很多控件初始化,可选择性的信息输入,和一些事件,比如网络访问和状态改变。其实这种操作的生命周期是可以集成到交互对象里的。下面的例子就是讲button被按下时候的交互事件,但是把交互对象作为action的target,比如:[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
@implementation TBProfileViewController
- (void)followButtonTapped:(id)sender {
self.followUserInteraction = [[TBFollowUserInteraction alloc] initWithUserToFollow:self.user delegate:self];
[self.followUserInteraction follow];
}
- (void)interactionCompleted:(TBFollowUserInteraction *)interaction {
[self.binding updateView];
}
//...
@end
@implementation TBFollowUserInteraction : 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];
}];
}
}

键盘管理

  • 在键盘状态改变后更新视图也是一个需要考虑的点,之前有可能是放在了view controller里,但是这个功能可以很容易被移植到键盘管理对象里。当然有很多键盘管理的例子,然而,如果你觉得他们过于繁杂,可以尝试简单的版本:
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 TBNewPostKeyboardManager : 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
  • 你可以调用-beginObservingKeyboard-endObservingKeyboard,从开始-viewDidAppear到结束-viewWillDisappear或者其他适合的地方。

导航栏

  • 界面之间的转场正常情况下是通过-pushViewController:animated:。如果转场变得复杂了,那么可以考虑把这种操作代理到导航栏对象中,尤其在适用于iPhone/iPad通用的app,导航需要改变依赖于栈中的最顶端的size class。
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 TBUserNavigator <NSObject>
- (void)navigateToFollowersForUser:(TBUser *)user;
@end
@implementation TBiPhoneUserNavigator : NSObject<TBUserNavigator>
- (instancetype)initWithNavigationController:(UINavigationController *)navigationController {
self = [super init];
if (!self) return nil;
_navigationController = navigationController;
return self;
}
- (void)navigateToFollowersForUser:(TBUser *)user {
TBFollowerListViewController *followerList = [[TBFollowerListViewController alloc] initWithUser:user];
[self.navigationController pushViewController:followerList animated:YES];
}
@end
@implementation TBiPadUserNavigator : NSObject<TBUserNavigator>
- (instancetype)initWithUserViewController:(TBUserViewController *)userViewController {
self = [super init];
if (!self) return nil;
_userViewController = userViewController;
return self;
}
- (void)navigateToFollowersForUser:(TBUser *)user {
TBFollowerListViewController *followerList = [[TBFollowerListViewController alloc] initWithUser:user];
self.userViewController.supplementalViewController = followerList;
}
  • 这种方式凸显出了一大好处是,把大的对象拆成了很多小的模块。他们可能会被修改,重写或者是替换。相比那些复杂臃肿的view controller,你可以把导航栏设置为自定义的Navigator