MVVM + FlowControllers路径的动机–杰出发明–中

有许多种构建iOS应用程序的方法。 MVC(模型视图控制器),MVP(模型视图演示者),MVVM(模型视图模型),VIPER(视图交互器演示者实体路由),Redux……当然,您可以命名其他许多名称。 我已经编写iOS应用程序已有一段时间了,并且我注意到MVVM + FlowControllers方法对我来说非常有效。 在这篇文章中,我想重点介绍“为什么?”,而不是其他方法,以及“如何?”,我在自己的应用程序中使用了它。

为什么?

开始

开始iOS开发通常与学习基本模式有关,您可以使用这些基本模式来解决在尝试在应用程序中创建第一个屏幕时遇到的问题。 MVC很可能是第一个处理您的视图和逻辑的模式,该模式在iOS开发人员中非常流行(不仅限于它们)。 这主要是由于Apple在UIKit框架和您可以在其教程中找到的代码示例中大力推广了这种模式。

MVC不好吗?

是! 这是最糟糕的! 这会导致Massive ViewControllers出现在您的应用程序上……开个玩笑……

不,这不对。 当我最近阅读“关于iOS应用程序体系结构的很多话”一文时,我感到非常高兴。 我不能说我完全同意那里所说的一切,但是我可以肯定地认同某些部分。 为什么我很高兴阅读这篇文章? 因为这句话:

“没有人会强迫您在一个控制器中实现多个数据源。 在viewDidLoad中发起网络调用。 在UIViewController中解析JSON。 用Singleton实例硬连接View。”

许多开发人员将MVC的混乱和混乱归咎于MVC,而这实际上是由开发人员自己造成的。 如果您缺乏纪律,那么即使VIPER也无法为您提供帮助。 某些模式可能使更容易正确地构造代码并保持代码整洁,但是是否遵守纪律始终取决于您。

如果最后还不错,我应该使用MVC吗? 如果我是一名熟练的顾问,我可能应该说“这取决于”。 MVC显然有其优点和缺点,我敢打赌,您会发现许多很棒的文章将帮助您下定决心。

为什么选择MVVM?

如果MVC还不错,那么我遵循MVVM路径的动机是什么? 仅举几个例子:

  • 它使我可以将代码的很大一部分远离UIKit (这是一种可以更快地测试此代码的功能–作为macOS框架)。
  • 它使我能够测试驱动视图的逻辑。
  • 它与反应式编程方法非常有效(但您无需使用它们即可从MVVM中受益)

所以现在,在采用了MVVM模式的原理之后,我能够拥有被动视图(尽可能愚蠢)和驱动视图的逻辑,这些逻辑也与UIKit分离。

为什么不采用像VIPER这样的模式真正很好地定位的划分方法呢? 好吧…你可以做到的。 我以前从未在大型项目中使用过VIPER(很高兴听到您对此的看法!),但是,我想说这种模式对于中小型应用程序可能是一个简单的过大杀伤力。
我认为,如果您想让您的解决方案易于理解,同时又能够轻松测试您的代码,并与依赖于UIKit的代码区分开,那么MVVM会非常有效。

流量控制器

使用它们的动机是什么?
在ViewController“ A”内找到负责转换到屏幕“ B”的代码时,这并不稀奇。 这种方法的问题是,突然之间,您在这两个实体之间建立了紧密的耦合,如果您想在其他情况下拆分或重用它们,可能会遇到麻烦。
好吧……问一个问题总是很好–这是一个问题吗? 如果您正在做一些简单的事情,那么使用这种导航可能会很好,但是,如果您要处理更复杂的事情,添加flowController肯定会有所帮助。 请记住,添加flowController并不是一项高成本的任务-即使在简单的应用程序中,我也倾向于使用它们,因为它们可以帮助我更好地组织代码并让我对应用程序中导航的工作方式有个很好的了解。 将flowController添加到您的应用将帮助您:

  • 使屏幕彼此分开,这将使您可以模块化并轻松地重复使用它们。
  • 控制应用程序各部分的流程(您可能会有许多不同的flowControllers)
  • ViewModels依赖项注入位置

学科

一个纯粹的事实,就是您开始使用MVVM和FlowControllers不会立即使您的代码库清晰。 你猜怎么了? 您仍然可以使用Massive ViewModels结束! 是否要紧紧组织代码以及由您自己决定。

怎么样?

MVVM

好的,那我该如何使用MVVM? 如何保持ViewModels清洁? 如何使用FlowControllers ? 跳进来,让我们看一个简单的示例,它将使我们能够看到所使用的概念。
我必须承认,在迈入iOS开发的第一步时,我认为ViewModel是一个对象,该对象包含将由视图显示的值。 那时,我没有发现使用ViewModels有太多价值。 改变我观点的是您可以阅读有关Microsoft模式和实践的方法。 对我来说,核心信息是Microsoft的方法中的View层表示为XAML(引用:“带有有限的代码,其中不包含业务逻辑”)。 好的……这是否意味着ViewModel不仅要保留我们的视图表示的值? 它还可以包含驱动这些视图的逻辑吗? 是! 在iOS上采用这种方法之后,它鼓励您保持视图层简单(在MVVM的情况下,这将是UIViewUIViewController )并将逻辑移至ViewModel 。 这是第一步,它使我们能够获得更好的可测试性,并使我们的代码远离UIKit

让我们逐步讲解与FlowController一起使用的MVVM模式的快速示例。

让我们从AppDelegate开始。 我们有什么在这里?

 class AppDelegate: UIResponder, UIApplicationDelegate { 

var window: UIWindow?
var mainNavigationController: UINavigationController!
var mainFlowController: MainFlowController!

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {

mainNavigationController = UINavigationController()

window = UIWindow(frame: UIScreen.main.bounds)
window?.rootViewController = mainNavigationController
window?.makeKeyAndVisible()

mainFlowController = MainFlowController(rootNavigationController: mainNavigationController)
mainFlowController.startFlow()

return true
}

}

重要的第一件事是我创建了MainFlowController ,它将负责控制应用程序流。 我在这里传递的唯一依赖关系是主导航控制器,它是主窗口的根控制器。 我还看到了将UIWindow直接传递给flowController的不同方法,但是,如果您不需要它,那么我更希望使用这个“更轻”的对象,它具有更明确的职责。

那么MainFlowController本身呢?

 class MainFlowController { 

private let rootNavigationController: UINavigationController

private lazy var entryViewModel: EntryViewModel = {
let fetcher = FakeUserFetcher()
let viewModel = EntryDefaultViewModel(userFetcher: fetcher)
viewModel.onUserNameSelected = self.onUserNameSelected
return viewModel
}()
private lazy var entryViewController = EntryViewController(viewModel: entryViewModel)

init(rootNavigationController: UINavigationController) {
self.rootNavigationController = rootNavigationController
}

func startFlow() {
rootNavigationController.pushViewController(entryViewController, animated: true)
}

func onUserNameSelected(userName: String) {
print("name: \(userName)");
// Show next screen using selected user name
}

}

FlowController的核心元素肯定是startFlow方法,在这种情况下,它将新的ViewController推送到我们的导航堆栈。 该ViewController是使用ViewModel作为参数创建的。 viewModel在一段时间内对我们非常重要,但是,从FlowController's角度来看,我们对onUserNameSelected闭包特别感兴趣。 这是FlowController可能感兴趣的ViewModel的输出。例如,选择用户名后,我们可以打开下一个屏幕,该屏幕允许我们选择生日,姓氏,喜欢的宠物,或者只是简单地显示名称办法。 您的FlowController允许您控制应用程序的流程,并使ViewModel不了解其使用的上下文ViewModel有其工作要做,完成后,我们将被通知FlowController

ViewController

 class EntryViewController: UIViewController { 

private let viewModel: EntryViewModel

private lazy var fetchUserButton: UIButton = {
let button = UIButton()
button.addTarget(self, action: #selector(onFetchUserButtonTapped), for: .touchUpInside)
// ...setup button
return button
}()

private lazy var selectUserButton: UIButton = {
let button = UIButton()
button.addTarget(self, action: #selector(onSelectUserButtonTapped), for: .touchUpInside)
// ...setup button
return button
}()

private let currentUserLabel = UILabel()

init(viewModel: EntryViewModel) {
self.viewModel = viewModel
super.init(nibName: nil, bundle: nil) // Layout created programamtically, sorry for that ;(
}

required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

override func loadView() {
super.loadView()
setupView()
setupObservers()
}

private func setupObservers() {
currentUserLabel.reactive.text <~ self.viewModel.userName // bind viewModels output to your UI
}

@objc func onFetchUserButtonTapped() {
viewModel.fetchUser()
}

@objc func onSelectUserButtonTapped() {
viewModel.selectUser()
}

private func setupView() {
// setup constraints etc
}

}

就我而言, ViewController是MVVM中View层的一部分。 这里发生的是:

  • ViewModel's输出绑定到UI组件
  • ViewModel调用方法
  • 如果需要,将生命周期方法传递给ViewModel
  • 布局设置

最后但并非最不重要的-ViewModel

 protocol EntryViewModel { 
func fetchUser()
func selectUser()

var onUserNameSelected: ((String)->Void)? {get set} // If you're using Reactive frameworks, you can also implement this as a stream
// var onNextSelected: (()->Void)? // Other callbacks could exist here, they do not need to pass data

var userName: Property {get} // ReadOnly property that allows others to observe its changes, but not change the property from outside of viewModel
}

class EntryDefaultViewModel: EntryViewModel {

private let userFetcher: UserFetcher
private var mutableUserName: MutableProperty = MutableProperty(nil)
lazy var userName: Property = Property(self.mutableUserName)
weak var onUserNameSelected: ((String)->Void)?

// injecting dependencies to your viewModel
init(userFetcher: UserFetcher) {
self.userFetcher = userFetcher
}

func selectUser() {
if let userName = userName.value {
onUserNameSelected?(userName)
}
}

func fetchUser() {
// Bind the result of fetchUser() function to a mutableUserName property
mutableUserName <~ userFetcher.fetchUser().map { $0.name }
}

}

ViewModel's一部分是View层的真正跳动的心脏。 它将允许View观察其公开的属性的变化。 它还将包含各种行为的逻辑。 请记住,您不需要将整个逻辑保留在ViewModel ,可以轻松地将其提取并添加为依赖项–这是userFetcher会发生的情况。 这可以使我们保持ViewModels整洁且易于测试,因为我们可以模拟依赖项。

摘要

遵循MVVM + FlowControllers的方法,帮助我在分离/组织代码方面做了很多工作,并使其更具可测试性。 如果我必须指出使用这种方法对我来说最有价值的三个好处,那就是:

  • 可以在不同场景中轻松重用的ViewControllers
  • 舒适地测试包含您的逻辑且未与UIKit耦合的ViewModels
  • 一处定义了导航的应用程序模块

如果您从未使用过类似的方法来处理您的应用程序,那么我强烈建议您尝试一下!

您想了解更多有关此主题的信息吗? 检查这些:
使用FlowController改善您的iOS体系结构
Swift By Sundell –“男孩,对此我有很多想法”,特别来宾Soroush Khanlou
协调员— Soroush Khanlou

最初发布于 brightinventions.pl

明亮发明的软件工程师Eliasz Sawicki

个人博客电子邮件Github

本文与 我的个人博客 交叉发布