引言
开发iOS应用时,现在更应该避免在一个视图控制器中直接展示其他视图控制器。
为什么?
问题
现代应用程序通常需要需要支持以多种方式展示相同的视图控制器。例如,在iPhone上你 push 一个新的视图控制器,但是在 iPad 上,你会把它嵌入另一个视图控制器或者用 popover 展示出来。
另外,很多情况下,你可能想在不同的情景中重用同一个视图控制器。如 UIImagePickerController 可以在多个地方以不同的方式展示出来。
视图控制器应该不依赖于他们的展示样式,这就是 SizeClasses 出现的原因之一。
如果你从其他 VCs / ViewModels 展示视图控制器,你将写出来一堆if语句,你的代码将变成条件大面条(big spaghetti of conditions)。
我作为一个顾问,经常需要参与审查项目,并帮助团队制订更干净的解决方案。
我看到过很多的意大利面条代码。下面这个就是相当糟糕的一个例子,但是还没有接近我见过最差的:
func doneButtonTapped() {
let vc = NextViewController(prepareNeccesaryState())
if Device.isIPad() {
navigationController.pushViewController(vc, animated: true, completion: nil)
} else {
var nav = UINavigationController(rootViewController: vc)
nav.modalPresentationStyle = UIModalPresentationStyle.Popover
var popover = nav.popoverPresentationController
popoverContent.preferredContentSize = CGSizeMake(500, 600)
popover.delegate = self
popover.sourceView = self.view
popover.sourceRect = CGRectMake(100, 100, 0, 0)
presentViewController(nav, animated: true, completion: nil)
}
}
这个幼稚的实现存在很多问题:
不必要的依赖 —— 一个视图控制器不需要知道另一个
可重用性差
面条代码,如果你的应用需要以不同的方式展示视图 - 你需要写大量的控制流。
单例是诱人的,因为它们让你更容易编写代码。
测试更难,你的VC / VM会有很多的副作用。
我们怎样才能解决这个问题?
清理你的 ViewControllers / ViewModels
这可以应用到 MVVM,MVC 和许多其他的常见模式。当我谈到 VC / VM,思考一下你现在正在使用的那一个。
让我们使用代理或者基于 block 的接口,而不是对相关的控制器硬编码,来摆脱所有的依赖关系。
class MyViewController {
let onDone = (Void -> Void)?
func doneButtonTapped() {
onDone?(prepareNeccesaryState())
}
}
ViewController / ViewModel 应该:
不能引用其他界面
不使用任何 UIKit presentation 类或类似 UINavigationController 或 presentViewController 的方法
有允许其他的对象通过注册来获知这个功能正在运行的接口,例如,代理或 block
不引用任何单例,稍后你会看到用我的做法是如何容易实现这一要求的。
这个时候,我们已经提高了可测试性,因为我们现在可以测试我们的接口是否被触发了,且无需副作用。伪代码:
let vc = createVC()
var executed = false
vc.onDone = {
executed = true
}
//! add code here to trigger done state
expect(executed).toEventually(beTruthy())
但是,我们如何协调我们的应用程序视图控制器?
介绍 FlowControllers
一个 FlowController 是一个简单的对象,它将管理你的应用程序的一部分,我喜欢把它看成用例的一个子集。
FlowController 的三个主要角色是:
为视图控制器配置特定上下文 - 例如分别为从应用 CreatePost 界面弹出的 ImagePicker 和改变用户头像时弹出的设置不同的配置
监听每个 ViewController 中的重要事件,并用来协调它们之间的流程。
为视图控制器提供它需要的东西,从而移除VC中的单例
func configureProgramsViewController(viewController: ProgramsViewController, navigationController: UINavigationController) {
viewController.state = state
viewController.addProgram = { [weak self] barButton in
guard let strongSelf = self else { return }
let createVC = R.storyboard.createProgram.initialViewController!
strongSelf.configureCreateProgramViewController(createVC, navigationController: navigationController)
navigationController.pushViewController(createVC, animated: true)
}
}
常见带有 FlowControllers 的应用架构像这样:
-
每个应用程序都有至少一个 FlowController,Root FlowController 由 AppDelegate 创建。
*实际上是 AppDelegate 中的一个 ApplicationController 创建了它,作为一个经验法则,你永远不应该引用你的AppDelegate,永远。*
-
每个 FlowController 可以有子控制器。
*如果您的应用程序具有可被看作是一个整体,需要多个屏幕的用户故事的一些重要的子集(如创建新的锻炼计划),那么你可以为那一部分创建一个新的子控制器,并从主控制器展示它。*
-
VC / VM 不知道其他 VC / VM。
*这意味着他们可以在任何地方重复使用,如果一个步骤是从导入用户照片库里的东西,你可以在应用程序的不同部分重复使用这段代码,例如,EditProfile可以使用相同的选择器选择用户头像。*
流量控制器配置和协调不同的界面。
每个 VC / VM 都定义了可以监听它们的行为的接口
如果需要支持多个设备和不同的展示方式,程序中会有其他 FlowController 类,没有意大利面条的代码。
这个想法最初是 Jim 和 Sami 一年前介绍给我的,我们经常使用它。
尽管我们的应用剧烈改变了3次,我们的架构都轻松地应对了,我们能够重复使用大量的代码,也有不少控制器不需要任何改变。
使用这样的架构的好处是显而易见的:
界面之间没有依赖关系。
高复用率。
更简单的代码注入并移除了单例。
更干净的代码,我看到的唯一意大利面条代码是我做的。
以更有表现力的方式来导航。
能够在共用大多数代码时对不同的设备编写不同的流。
可以轻松分离测试每个 VC / VM,因为一切都可以注入。没有必要子类化。
现在,在一些架构中也有类似的概念,如 VIPER 有路由器。但它们通常是很复杂的,需要大量的前期成本,以适配到现有的应用程序中。
这个方法最棒的地方是它很简单直观,立刻就可以(在现有的项目中)使用它,无需等待新项目。它在小型和大型项目的效果一样好。
不管你使用 MVVM,MVC 还是其他模式,如果应用中存在界面跳转,不妨试一试。
作者信息
原文作者:Krzysztof Zabłocki
原文链接:http://merowing.info/2016/01/improve-your-ios-architecture-with-flowcontrollers/
翻译自 MaxLeap 团队_UX成员:Alex Sun
翻译首发链接:https://blog.maxleap.cn/archives/879
商业转载请联系作者获得授权,非商业转载请注明作者信息与出处。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。