协调员,面向协议的编程和MVVM; Swift的防弹架构

MVVM或模型,视图,视图模型的起源来自于2005年发布此文章的两名Microsoft开发人员: 用于构建WPF应用程序的Model / View / ViewModel模式简介

它修复了标准MVC或模型View Controller范例中的一些叙述空白,特别是消除了产生View Controller的副作用:
1)比他们应该了解的多得多
2)成为应用程序逻辑的重点
3)比他们指定的任务更加负责
View Controller变成了庞大的文件,难以维护,不是模块化且难以重新利用。

因此进入视图模型。 太从作者的角度出发

该术语的意思是“视图的模型”,可以被认为是视图的抽象,但是它也提供了视图可以用于数据绑定的模型的特殊化。 在后一个角色中,ViewModel包含将模型类型转换为视图类型的数据转换器,并且包含视图可用于与模型交互的命令。

通过改变逻辑和数据并将其放置在VM中,您可以在整个应用程序中重用视图的数据。 好吧,以后再深入。

面向协议的程序设计 。 苹果公司(源)一直在积极推动这个概念,我可以写一篇有关该主题的文章,但是要取得的成果以及我们将如何使用它非常简单。 为了创建具有严格功能限制的高度可重用的View模型,我们将创建协议来告知我们如何创建模型。 一旦有了该模型,我们将创建存储所有信息的视图模型,并将其传递给我们的视图控制器。

协调员 。 据我所知, 协调员一词是Soroush Khanlou在2015年创造的。我看到他的作品有很多风味,而其他出色的开发人员对此主题的看法也是如此。 如果想要裸露骨头,请严格按照POP方法检查Niels Van Hoorn,但到目前为止,我最喜欢的是AleksandarVacić。

协调器完全解决了UI层中的数据流。 它们不保存也不包含任何类型的应用程序数据-他们的主要关注点是将数据从中间件改组到前端UI。

他们做什么:

创建VC的实例

显示或隐藏VC

配置VC(设置DI属性)

加:

接收来自VC的数据请求

将请求路由到中间件

将结果路由回VC

他对协调器进行了更进一步的更新,进一步巩固了它们作为视图控制器和模型通信者的演示者的地位。 如果您查看Navigation Coordinator则它是UINavigationController子类。

协调员应通过其创建和管理的活动来命名,并应为包含这些活动的UI的VC呈现并触发信息的创建。

今天,我们将创建一个简单的登录应用程序。 具有LoginViewControllerSetupAccountViewControllerForgotPasswordViewController一个。 这些都是与用户的帐户信息和身份验证交互的叙述,并且从这些视图控制器馈入和处理的数据非常相似:

1)有些表格可以接受用户数据
2)然后需要验证和处理此数据
3)如果数据未经验证,则需要通知用户其信息不正​​确
4)如果数据经过验证,那么我们需要创建一些网络请求进行身份验证,为用户创建一个新帐户或检索用户的忘记密码

创建应用程序时,我会使用非常具体的工作流程,只要遵循它,遵循架构方案的复杂性就会变得非常简单。 事情很明显。

这是一个关于我们如何构建架构的非常简单的图形:

这是有关代码创建过程的更详细的图形:

按照此过程,跳入Xcode并登录应用程序。

您可以在此处下载入门项目

我添加了几个文件以开始我们的工作。

首先,我们有一个App Dependency文件,其中包含该应用程序所有各种API的需求。 在较大的文件中,它将包含诸如数据持久性,网络,资产管理和安全管理器之类的内容。 在我们自己的团队中,拥有Networking类,可以使该项目的范围缩小,实际上是一个虚拟的Networking Manager,它将为我们进行的每个呼叫吐出200个响应。 我们的需求依赖性协议将确保我们在整个协调器中传播依赖性,并在此处更多地使用“依赖性注入”来保持状态不变。

我们还将使用Swift Validator进行表单的验证,因为它很棒。

我们的用户将打开我们的应用程序,并被引入到设置帐户页面,如果他们拥有一个可以登录的帐户,并且他们拥有一个忘记密码的帐户便可以请求它。
总而言之,这是一个非常简单的应用程序,并且所需的代码比您想象的要少。

首先,让我们创建一个模型,该模型将为用户提供叙述信息。 考虑文件AccountType

 枚举AccountType { 
案例登录,设置,忘记密码
}

这是整个应用程序的蓝图,并将在整个过程中使用。 由于我们拥有适用于应用程序的线框,因此,我们可以为表单的模型等特定于体验的东西创建线框。

创建一个文件FormTextField

 协议FormTextField { 
var apiKey:字符串{get}
var占位符:字符串{get}
var textColor:UIColor {get}
varvalidationRules:[Rule] {get}
var keyboardType:UIKeyboardType {get}
}

相当标准的表单字段内容; 包含字段用途的占位符,稍后将使用的API密钥,一些样式和验证规则。
由于我们希望我们的文本字段看起来相似,因此我们应该使它们都具有相同的文本颜色。 无需在我们创建的每个字段的VM中列出它,我们可以通过添加扩展名在此处为协议创建默认值:

 扩展FormTextField { 
var keyboardType:UIKeyboardType {return .default}
var textColor:UIColor {返回UIColor.lightGray}
varvalidationRules:[Rule] {返回[RequiredRule()]}
}

我们还将为键盘添加一个默认值,并使简单的默认验证规则成为必需。

接下来,我们将为我们想要在应用程序中拥有的每个文本字段创建视图模型。 创建一个名为FormViewModel的文件并添加以下内容:

 私有结构UserNameField:FormTextField { 
var占位符:字符串=“用户名”
var apiKey:字符串=“用户名”
}
 私有结构PasswordField:FormTextField { 
var占位符:String =“ Password”
varvalidationRules:[Rule] = [MinLengthRule(长度:9,消息:“密码长度必须至少为9个字符”))]
var apiKey:字符串=“密码”
}
 私有结构EmailField:FormTextField { 
var占位符:字符串=“电子邮件地址”
varvalidationRules:[Rule] = [EmailRule()]
var apiKey:字符串=“ email_field”
}

现在,我们对表格的领域有了清晰,简洁,可测试的路线图。 我们将其设为私有,以确保它们不会被更改。 我们将在此代码上方为表单创建全视图模型:

  typealias FormFields = [FormTextField] 
 类FormViewModel:NSObject { 

私有惰性var验证程序= Validator()
  var栏位:FormFields { 
得到{
切换self.type {
案例.login:
返回[UserNameField(),PasswordField()]

case .setup:
返回[EmailField(),UserNameField(),PasswordField()]

案例.forgotPassword:
返回[EmailField()]
}
}
}

变量类型:AccountType

init(type:AccountType){
self.type =类型
super.init()
}
func validateForm(){
  } 
  } 
 扩展名FormViewModel:UITableViewDataSource { 
func tableView(_ tableView:UITableView,cellForRowAt indexPath:IndexPath)-> UITableViewCell {
  } 

func tableView(_ tableView:UITableView,numberOfRowsInSection部分:Int)-> Int {
  } 
}

首先,我们为字段数组创建一个类型typealias 。 请注意,我们的表单继承自UITableViewDataSource 。 这最通常放在VC中,但是使用MVVM,我们可以让这一类处理用户所有三种体验的数据源。 我们还将创建Validator的实例,并创建一个方法,该方法在调用时将验证表单。 我们暂时将其留空。

接下来是一个简单的switch语句,该语句用于初始化窗体的视图模型时获得的值。 它将根据我们想要的形式返回一组不同的字段。
然后,我们遵循UITableView的数据源必需的方法,这是构建表单的大部分方法。

完成我们的过程之后,我们现在将创建视图模型通知的UI。 我已经为您创建了FormTableViewCell ,因此只需添加以下内容:

  func setView(用于字段:FormTextField)-> UITextField { 
textField.placeholder = field.placeholder
textField.textColor = field.textColor
返回textField
}

回到FormViewModel让我们更新Tableview的数据源方法以创建一个单元格。

 扩展名FormViewModel:UITableViewDataSource { 
func tableView(_ tableView:UITableView,cellForRowAt indexPath:IndexPath)-> UITableViewCell {
var cell:FormTableViewCell!
单元格= tableView.dequeueReusableCell(withIdentifier:FormTableViewCell.reuseID,
的:indexPath)为? FormTableViewCell
?? FormTableViewCell()

让field = cell!.setView(用于:fields [indexPath.row])
validateator.registerField(field,
规则:fields [indexPath.row] .validationRules)
返回单元
}

func tableView(_ tableView:UITableView,numberOfRowsInSection部分:Int)-> Int {
返回fields.count
}
}

我们还将注册该字段以进行验证。

现在,让我们通过Coordinator和ViewController传播帐户类型,以便我们可以根据呈现的Coordinator逻辑动态设置表单。 在AuthenticateUserVC添加数据源变量

var datasource: FormViewModel

并在super.init()之前的init方法中添加:

 数据源= FormViewModel(类型:类型) 

我们将在初始化VC时需要一个AccountType并基于该类型创建视图模型的实例。 现在,将数据源连接到UITableView的惰性init内的表单。

  tv.dataSource = self.datasource 

让我们看看我们在哪里。 VC几乎完全不了解涉及其数据源的细节。 我们有一个干净的演示者,即“ Account Coord ,它也不知道我们当前正在演示哪种帐户。 它只知道当前存在一个AccountViewController类型。
现在,我们的应用程序协调员拥有在应用程序开始时显示的内容的权限。 这使我们能够通过更改AccountType并因此更改启动时显示的UI来快速测试UI。

大! 现在,让我们对已验证的数据进行处理。 现在,我们在上面的流程图中的第三列。

添加到FormViewModel的顶部

  typealias ValidationCallback =(布尔,[字符串]?) 
 协议FormViewModelDelegate:类{ 
func formWasValidated(_成功:ValidationCallback)
}

当然还要添加对委托的引用:

var delegate: FormViewModelDelegate?

我们将发送成功或失败消息,如果失败,则将包含从ValidationDelegate返回给我们的一系列错误消息。 说到让我们的视图模型符合以下条件:

 扩展FormViewModel:ValidationDelegate { 
funcvalidationSuccessful(){
委托?.formWasValidated((true,nil))
}

func validateFailed(_错误:[(Validatable,ValidationError)]){
委托?.formWasValidated((false,errors.map({$ 0.1.errorMessage}))))
}
}

然后让我们在之前创建的helper方法中添加调用以验证表单:

  func validateForm(){ 
validateator.validate(自己)
}

最后,让我们设置登录按钮以触发验证功能:

  @objc 
私人函数onAcceptButtonTap(){
datasource.validateForm()
}

我们的应用程序应编译并加载表单。 点击“登录”按钮,您将根据输入内容获得打印声明。

让我们连线一个委托,以通知响应者我们已经收到并处理了来自用户的输入,并对他们有响应。 将此添加到AuthenticateUserVC

 扩展AuthenticateUserViewController:FormViewModelDelegate { 
func formWasValidated(_成功:ValidationCallback){
警卫让errorMessage =成功。1else {
//没有验证错误!
返回
}

let message = errorMessage.reduce(into:“”,{$ 0 =“ \($ 0 +” \ n“ + $ 1)”})
让alertVC = UIAlertController(title:“表单无效”,
消息:消息,
preferredStyle:.alert)
alertVC.addAction(UIAlertAction(title:“ Rodger!”,
风格:破坏性的
处理程序:无)
目前(alertVC,动画:true,完成:nil)
}
}

不要忘记将委托分配给VC。 在super.init()之后的init方法中执行此操作,因为我们正在调用self

datasource.delegate = self

在这里,我们确保表单无效,将错误消息简化为单个消息,并将其作为警报呈现给用户。

如果没有任何错误,我们现在需要通知协调员该VC的目的已实现,并更新导航堆栈。

现在,我们完成了流程的最后一项任务,如果响应者收到有效的用户凭据,则需要通知演示者更新导航堆栈。

 协议AuthenticateUserViewControllerDelegate:类{ 
func userProvidedValidated(凭证:凭证,类型:AccountType)
}

凭证是[String: Any]的类型typealias
现在,让我们遍历文本字段和viewModel的api键的字段数据,以创建Credential对象,并将其添加到formWasValidated方法中:

 警卫让errorMessage =成功。1else { 
var凭证= Credentials()
for(index,key)in datasource.fields.enumerated(){
守卫让单元格= form.cellForRow(at:IndexPath(item:index,section:0))为? FormTableViewCell else {返回}
凭证[key.apiKey] = cell.textField.text
}

委托?.userProvidedValidated(凭证:凭证,类型:数据源。类型)
返回
}

让我们在configure方法中将AccountCoord分配给VC的委托,然后在扩展中遵循该委托。

 扩展AccountCoordinator:AuthenticateUserViewControllerDelegate { 
func userProvidedValidated(凭证:凭证,类型:AccountType){
依赖关系?.networking.perform(call:类型,with:凭据,callback:{(response)in
切换响应{
案例(“成功”,200):
打印(“用户已登录!”)
打破
默认:
//发出处理请求
打破
}
})
}
}

到此为止! 我们已经创建了大多数用户首次使用其凭据登录的叙述。

现在介绍另外两个叙述。 我们比您想象的更接近完成它们。

让我们添加一个委托方法,用于用户何时希望离开登录屏幕。 将此添加到AuthenticateUserViewControllerDelegate

  func updateResponder(输入:AccountType) 

现在,通过添加新方法来符合AccountCoord

  func updateResponder(键入:AccountType){ 
配置(用于:类型)
}

这将使用每个用户故事上显示的所有三个按钮,以不同的形式重新配置相同的VC。 为了使UI到达我们想要的位置,在SignUpForgotPassword上只有一个按钮应该很简单,因为我们已经完成了繁重的工作。

由于我们的Login屏幕的用户界面最复杂,而其他两个屏幕之间的唯一区别在于它们的形式。 让我们为登录过程创建AuthenticateUserVC的子类。

创建一个名为LoginVC的文件,然后为注册按钮和忘记密码按钮添加UI。

 最后的类LoginViewController:AuthenticateUserViewController { 

惰性变量signUpButton:UIButton = self.buttonFactory(with:.signup)

惰性变量forgotPassButton:UIButton = self.buttonFactory(with:.forgotpass)


在里面(){
super.init(类型:.login)
}
是否需要初始化?(编码器aDecoder:NSCoder){fatalError()}

覆盖func loadView(){
super.loadView()
signUpButton.bottomAnchor.constraint(equalTo:acceptButton.topAnchor,常量:-15).isActive = true
signUpButton.leadingAnchor.constraint(equalTo:acceptButton.leadingAnchor).isActive = true
signUpButton.trailingAnchor.constraint(equalTo:acceptButton.trailingAnchor).isActive = true
signUpButton.heightAnchor.constraint(equalToConstant:ButtonViewModel.largeButtonHeight).isActive = true

forgotPassButton.topAnchor.constraint(等于:form.bottomAnchor,常数:15).isActive = true
forgotPassButton.leadingAnchor.constraint(equalTo:acceptButton.leadingAnchor).isActive = true
forgotPassButton.trailingAnchor.constraint(equalTo:acceptButton.trailingAnchor).isActive = true
forgotPassButton.heightAnchor.constraint(equalToConstant:ButtonViewModel.largeButtonHeight).isActive = true
}
}

返回AuthenticateUserViewController将其添加到onForgotPasswordTap(:

 保护datasource.type!= .forgotPassword else { 
datasource.validateForm()
返回
}
委托?.updateResponder(发送至:.forgotPassword)

onSetupTap(:

 保护datasource.type!= .setup else { 
datasource.validateForm()
返回
}
委托?.updateResponder(到:.setup)

如果您还没有真正了解按钮的创建方式,那几乎就是我们表单的确切过程! 由于仅在由于延迟初始化而在应用程序的生命周期中调用它们时才创建按钮,因此当我们将代码添加到LoginVC ,将仅在VC中创建这些按钮。

剩下要做的就是更新AccountCoord以在我们的createViewController方法而不是AuthenticateUserVC创建LoginVC的实例。

 私人功能createViewController(用于类型:AccountType)-> AuthenticateUserViewController { 
警卫类型!= .login else {
返回LoginViewController()
}
返回AuthenticateUserViewController(type:type)
}

现在,如果您运行该应用程序,您应该已经完成​​了一个用户故事。

这是一个不断发展的概念,我很想听听您认为可以在哪些方面进行改进! 绝对欢迎对此概念进行一般性评论。

额外信用:

创建一个实现视图模型,用于验证失败时向用户显示的警报。 在您所在的屏幕上显示待处理的其他消息。