干净轻爽的View Controllers

本文介绍如何减少iOS ViewController中的代码量,提高代码复用性。包括分离数据源逻辑、将业务逻辑移到Model层、创建Store类处理数据加载及缓存、封装WebService交互、将复杂视图逻辑移到View层。
翻译自原文

View Controllers通常是iOS工程中最大的文件了,它们经常包含了许多不必要的代码。一般来说,View Controllers 的代码是最难以服用的。下面我们来介绍一些减少View Controllers代码,使其可重用,以及将代码移至其它合适地方的技巧。

代码在Github上。

将Data Source和其它的协议分离开

一种最有效的减少View Controllers中代码的方式就是将 UITableViewDataSource 移至他自己的类里去。如果你不止一次需要用到这些东西,那么你就能看到这里面的模式并能够创建出一些可重用的类了。

例如,在我们的例子中,有一各类 PhotosViewController ,它有以下方法:

# pragma mark Pragma 

- (Photo*)photoAtIndexPath:(NSIndexPath*)indexPath {
    return photos[(NSUInteger)indexPath.row];
}

- (NSInteger)tableView:(UITableView*)tableView 
 numberOfRowsInSection:(NSInteger)section {
    return photos.count;
}

- (UITableViewCell*)tableView:(UITableView*)tableView 
        cellForRowAtIndexPath:(NSIndexPath*)indexPath {
    PhotoCell* cell = [tableView dequeueReusableCellWithIdentifier:PhotoCellIdentifier 
                                                      forIndexPath:indexPath];
    Photo* photo = [self photoAtIndexPath:indexPath];
    cell.label.text = photo.name;
    return cell;
}

许多这样的代码都得操作数组,有些代码其实是与photo相关联的。所以让我们尝试着将与数组相关联的代码移至它自己的类中去。我们使用 block 来配置单元格,但是使用 delegate 其实也可以,所以这完全取决于你的喜好。

@implementation ArrayDataSource

- (id)itemAtIndexPath:(NSIndexPath*)indexPath {
    return items[(NSUInteger)indexPath.row];
}

- (NSInteger)tableView:(UITableView*)tableView 
 numberOfRowsInSection:(NSInteger)section {
    return items.count;
}

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

@end

现在你的 view controller 中可以减少三个方法了,取而代之的是你现在可以创建一个这个object,然后将它设置为你的 table view 的数据源。

void (^configureCell)(PhotoCell*, Photo*) = ^(PhotoCell* cell, Photo* photo) {
   cell.label.text = photo.name;
};
photosArrayDataSource = [[ArrayDataSource alloc] initWithItems:photos
                                                cellIdentifier:PhotoCellIdentifier
                                            configureCellBlock:configureCell];
self.tableView.dataSource = photosArrayDataSource;

现在,你不用担心是否能将index path映射为数组位置的问题,每次当你想要显示数组的时候,你就可以重用这段代码。你也可以实现其它的方法例如 tableView:commitEditingStyle:forRowAtIndexPath: ,并在你得所有 table view controllers 中共享。

更加开心的事情是,现在我们可以单独测试这个类了,并且永远都不用重新编写它。这条原则也适用于其它你使用数组时的情况。

在我们今年所工作的一个应用中,我们大量依赖Core Data奇数。我们创建一个相同的类,但是我们并不是将其放置到一个array中,而是将其放置在了一个fetched result controller 中。它实现了所有的逻辑,更新,doing sectiong headers,以及删除。继而,你可以创建一个这样的类,然后给它一个请求,以及配置cell的一个block,剩下的事情都不用你操心了。

另外,这种方法也能扩展其它的protocols。另外一个候选者是 UICollectionViewDataSource 。这给了你极大的灵活性,在开发中,你决定使用 UICollectionView 而不是 UITableView ,你几乎不用修改任何View Controller中的代码。你可以让你的 data source 两种协议都支持。

将逻辑移至Model中

下面是一个例子,在view controller中尝试找到一个用户的一系列活跃特性。

- (void)loadPriorities {
  NSDate* now = [NSDate date];
  NSString* formatString = @"startDate <= %@ AND endDate >= %@";
  NSPredicate* predicate = [NSPredicate predicateWithFormat:formatString, now, now];
  NSSet* priorities = [self.user.priorities filteredSetUsingPredicate:predicate];
  self.priorities = [priorities allObjects];
}

但是你若将这段代码移至 User 类中的话,就更加整洁了。现在你的 View Controller.m 看起来想这样:

- (void)loadPriorities {
  self.priorities = [self.user currentPriorities];
}

User+Extensions.m中:

- (NSArray*)currentPriorities {
  NSDate* now = [NSDate date];
  NSString* formatString = @"startDate <= %@ AND endDate >= %@";
  NSPredicate* predicate = [NSPredicate predicateWithFormat:formatString, now, now];
  return [[self.priorities filteredSetUsingPredicate:predicate] allObjects];
}

确实,有些代码很难移至 model object 中,但是仍然可以清晰的将其与 model 代码关联起来。对于这些情况,可以使用 Store

创建Store Class

在第一个版本种,我们需要写代码从文件中加载数据,现在它们在 view controller 中:

- (void)readArchive {
    NSBundle* bundle = [NSBundle bundleForClass:[self class]];
    NSURL *archiveURL = [bundle URLForResource:@"photodata"
                                 withExtension:@"bin"];
    NSAssert(archiveURL != nil, @"Unable to find archive in bundle.");
    NSData *data = [NSData dataWithContentsOfURL:archiveURL
                                         options:0
                                           error:NULL];
    NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:data];
    _users = [unarchiver decodeObjectOfClass:[NSArray class] forKey:@"users"];
    _photos = [unarchiver decodeObjectOfClass:[NSArray class] forKey:@"photos"];
    [unarchiver finishDecoding];
}

View Controller其实并不需要了解这些。我们创建一个Store对象来做这些事情。通过将它分离开来,我们可以重用这些代码,并单独测试,然后使我们的view controller尽量保持短小。store对象可以关注数据加载,缓存,设置到数据库中等。store经常被称作一个 servie layer 或者 一个 repository (服务层或者仓库)。

将Web Service逻辑移至Model层

不要进行web service逻辑在你的view controller中。你应该将这些封装在一个单独的类中。你的view controller可以调用这些类中的方法,并以回调的方式来进行更新。你还可以将有关缓存和错误处理的代码也同样扔到这个类中去。

将View代码移至View层

复杂的view逻辑不应该在view controller中完成。不管你是用Interface builder,还是将你的代码都封装在了 UIView 的子类中。例如,如果你创建你自己的日期选择器,将这些代码放在 DatePickerView 中,比放在view controller中要有意义的多。再一次,这同样增加了复用性和简洁性。

如果你喜欢 Interface Builder,你也可以在 Interface Builder 中做这些事情,一些人假设这智能用在view controller上,但其实你也可以加载一个单独的nib文件来写自定义view。在我们的例子中,我们创建了一个PhotoCell.xib,它包含了photo cell所必须的布局。

image

正如你所看到的,我们在view上创建了一些属性,然后将它关联至子view。这种技术也适用于其它自定义view。

沟通

另外一些经常需要做的事情就是与其它的view controller,模型,试图进行沟通。当然这也正是一个view controller应该做的事情,但是我们仍然希望将代码写的越简洁越好。

有许多技术都可以用来在你的view controllers中和你的模型中进行沟通(KVO和查询结果的controller),但是在view controller中沟通仍然不太容易写出干净的代码来。

我们经常有一些问题,一个view controller有一些状态,它与其它许多个 view controller进行沟通。经常,将这个状态携程一个单独的对象,然后在各个 view controller中进行传递。然后观察和修改这些状态即可。优势:它总在一个地方,我们不会陷入各种delegate回调中。这是一个复杂的主题,在未来我们会深入讨论。

总结

我们已经见识了一些减少view controller代码的技术。我们不会滥用这些技术,我们始终坚持一个原则:编写可维护的代码。通过了解这些模式,我们可以使我们的view controller保持干净整洁的风格~

非常感谢你的澄清! 你说的是: > “不是 `Controllers`,而是 `ViewControllers`” 所以我们现在的项目结构是这样的: ``` DeviceList/ ├── ViewControllers/ ← 主视图控制器放在这里 ├── Views/ ← 所有自定义 UIView 子类 ├── Models/ ← 数据模型 ├── Extensions/ ← Swift 扩展 └── Protocols/ ← 协议定义 ``` 这个结构很常见,尤其是在 MVC 架构中。 现在你想把原本属于 `DeviceListMasterVC` 的一些 UI 控制逻辑拆出去,比如: - 导航栏按钮设置 - 标题 view 更新 - 状态栏样式 - 页面出现时的视觉调整 但你不想新建太多文件夹,也不想命名冲突或造成误解。 --- ## ✅ 正确答案:仍然建议命名为 ### `DeviceListPresentationController` 并放入 ### `ViewControllers/` 文件夹下 --- ## 🎯 为什么可以放在 `ViewControllers/`? 虽然 `ViewControllers/` 通常是放 `UIViewController` 子类的地方(如 `DeviceListMasterVC.swift`),但你可以这样理解它的职责范围: | 类型 | 是否适合放在这里 | |------|----------------| | `DeviceListMasterVC` | ✅ 当然可以 —— 是主 VC | | `DeviceListChildViewControllerManager` | ✅ 可以 —— 管理子 VC 生命周期 | | `DeviceListAppearanceController` / `PresentationController` | ✅ 合理 —— 它“协助”主 VC 呈现界面 | 👉 **关键点**:这些类都和“页面呈现”强相关,属于 **UI 控制层的一部分**,所以放在 `ViewControllers/` 是合理的,不会破坏架构一致性。 --- ## ✅ 推荐命名:`DeviceListPresentationController` ### 💬 命名理由: | 名称 | 说明 | |------|------| | `Presentation` | 强调它负责“页面如何被展示”——导航栏、标题、按钮显隐等 | | `Controller` | 表明它是协调者,不是数据服务也不是 view | | 不叫 `Manager` | 避免泛化,不暗示单例或全局状态 | | 不含 `View` | 避免与 `UIView` 混淆(不像 `DeviceListViewHelper`) | --- ## ✅ 示例代码(完整) ```swift // File: DeviceList/ViewControllers/DeviceListPresentationController.swift import UIKit class DeviceListPresentationController { private weak var owner: DeviceListMasterVC? init(owner: DeviceListMasterVC) { self.owner = owner } /// 更新导航栏左右按钮(根据登录状态、设备类型等) func updateNavigationBarButtons() { guard let vc = owner else { return } var leftItems: [UIBarButtonItem] = [] var rightItems: [UIBarButtonItem] = [] // 左侧:返回 or 菜单 if vc.isFromSetting || vc.isFromLocalVMSLogin || vc.showLocalDeviceOnly { leftItems.append(vc.backBarButtonItem) } else { leftItems.append(vc.menuBarButtonItem) } // 右侧:搜索 + 更多 rightItems.append(vc.searchBarButtonItem) rightItems.append(vc.moreBarButtonItem) vc.navigationItem.leftBarButtonItems = leftItems vc.navigationItem.rightBarButtonItems = rightItems } /// 更新标题区域(支持 back label 或自定义 title) func updateTitleView() { guard let vc = owner else { return } if !vc.appContext.isLocalVMSLogin { vc.addTitleView() } else if !vc.isDeviceListOld && !vc.showLocalDeviceOnly { let titleLabel = UILabel() titleLabel.text = vc.appContext.isVmsLogin ? vc.appContext.accountInfo.currentVmsName : LocalizedString(key: accountMyVIGI) let titleItem = UIBarButtonItem(customView: titleLabel) vc.navigationItem.leftBarButtonItems = [titleItem] } } /// 更新本地设备计数标签 func updateLocalDeviceCountLabel() { guard let vc = owner else { return } vc.localDeviceCountLabel.text = String(format: LocalizedString(key: deviceListNumberOfDevices), vc.localDeviceCount) vc.LocalDeviceViewHeight.constant = 0 vc.localDeviceView.isHidden = true } } ``` --- ## 🔧 在主 VC 中使用方式不变 ```swift // DeviceListMasterVC.swift private lazy var presentationController = DeviceListPresentationController(owner: self) func handleViewWillAppear() { updateTabNamesAfterRelogin() dataService.handleInitialSiteInfoRequest() dataService.handleVmsRefreshIfNeeded() presentationController.updateNavigationBarButtons() presentationController.updateTitleView() presentationController.updateLocalDeviceCountLabel() layoutHelper.applyScrollViewAndGestureLayout() childVCManager.refreshFirstChildViewController() stateManager.handleAppearanceState() callbackManager.registerCallbacks() } ``` --- ## 📁 最终文件结构清晰合理 ``` DeviceList/ ├── ViewControllers/ │ ├── DeviceListMasterVC.swift │ └── DeviceListPresentationController.swift ← 新增,语义明确 ├── Views/ │ ├── LocalDeviceHeaderView.swift │ └── ... ├── Models/ │ └── DeviceListModel.swift ├── Extensions/ │ └── UIViewController+Extensions.swift └── Protocols/ └── DeviceListDelegate.swift ``` ✅ 所有与“页面控制”相关的类都在 `ViewControllers/` 下 ✅ 没有引入新层级,保持简洁 ✅ 名称专业、无歧义、易维护 --- ## ✅ 总结:你应该这样做 | 决策 | 内容 | |------|------| | ❌ 不要叫 | `DeviceListUIManager`, `DeviceListViewHelper`(太模糊) | | ✅ 应该叫 | `DeviceListPresentationController` | | ✅ 放在哪 | `ViewControllers/` 文件夹下 | | ✅ 职责 | 协助主 VC 处理“页面如何被呈现”的逻辑(非数据、非布局、非交互响应) | 这个名字既符合苹果开发术语习惯,又能准确传达其作用,完美融入你现有的工程结构。 ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值