使用 FlowControllers 改进iOS应用架构

版权声明:此文章转载自极客头条

如需转载请联系听云College团队成员小尹 邮箱:yinhy#tingyun.com

问题

现代应用程序通常需要需要支持以多种方式展示相同的视图控制器。例如,在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 还是其他模式,如果应用中存在界面跳转,不妨试一试。

想阅读更多技术文章,请访问听云技术博客,访问听云官方网站感受更多应用性能优化魔力。

关于作者

郝淼emily

重新开始,从心开始

我要评论

评论请先登录,或注册