iOS中使用MVVM

我是一个iOS开发人员,我在我的项目中拥有大量视图控制器,所以我一直在寻找一种更好的方式来构build我的项目,并遇到了MVVM(Model-View-ViewModel)架构。 我已经阅读了很多与iOS的MVVM,我有几个问题。 我会用一个例子来解释我的问题。

我有一个名为LoginViewController的视图控制器。

LoginViewController.swift

 import UIKit class LoginViewController: UIViewController { @IBOutlet private var usernameTextField: UITextField! @IBOutlet private var passwordTextField: UITextField! private let loginViewModel = LoginViewModel() override func viewDidLoad() { super.viewDidLoad() } @IBAction func loginButtonPressed(sender: UIButton) { loginViewModel.login() } } 

它没有Model类。 但是我创build了一个名为LoginViewModel的视图模型来放置validation逻辑和networking调用。

LoginViewModel.swift

 import Foundation class LoginViewModel { var username: String? var password: String? init(username: String? = nil, password: String? = nil) { self.username = username self.password = password } func validate() { if username == nil || password == nil { // Show the user an alert with the error } } func login() { // Call the login() method in ApiHandler let api = ApiHandler() api.login(username!, password: password!, success: { (data) -> Void in // Go to the next view controller }) { (error) -> Void in // Show the user an alert with the error } } } 
  1. 我的第一个问题就是我的MVVM实现正确吗? 我有这个疑问,因为例如我把loginbutton的龙头事件( loginButtonPressed )在控制器。 我没有为login屏幕创build一个单独的视图,因为它只有几个文本框和一个button。 事件方法绑定到UI元素是否可以接受?

  2. 我的下一个问题也是关于loginbutton。 当用户点击button时,用户名和密码值应该传递到LoginViewModel进行validation,如果成功,则传递给API调用。 我的问题是如何将值传递给视图模型。 我应该添加两个参数的login()方法,并通过他们,当我从视图控制器调用它? 还是应该在视图模型中为它​​们声明属性,并从视图控制器中设置它们的值? MVVM中哪一个是可以接受的?

  3. 采取视图模型中的validate()方法。 如果其中任何一个都是空的,应该通知用户。 这意味着在检查之后,结果应该返回到视图控制器以采取必要的行动(显示警报)。 与login()方法一样的东西。 如果请求失败,则提醒用户,如果成功则转到下一个视图控制器。 如何从视图模型中通知控制器这些事件? 在这种情况下是否可以使用像KVO这样的绑定机制?

  4. 什么是使用MVVM的iOS的其他绑定机制? KVO是一个。 但是我读了它不太适合大项目,因为它需要大量的样板代码(注册/注销观察者等)。 什么是其他选项? 我知道ReactiveCocoa是一个用于这个框架,但我期待看看是否有任何其他本地的。

我在互联网上的MVVM上遇到的所有资料几乎都没有提供有关这些部分的信息,所以我非常感谢你的回应。

waddup老兄!

1a-你正朝着正确的方向前进。 你把loginButtonPressed放在视图控制器中,这正是它应该在的地方。 控件的事件处理程序应该总是进入视图控制器 – 这是正确的。

1b – 在您的视图模型中,您有意见说明,“向用户显示错误警报”。 您不希望在validate函数中显示该错误。 而是创build一个具有关联值的枚举(其中值是要显示给用户的错误消息)。 改变你的validation方法,使其返回该枚举。 然后在您的视图控制器中,您可以评估该返回值,并从那里您将显示警报对话框。 请记住,您只想在视图控制器中使用UIKit相关的类,而不是视图模型。 视图模型应该只包含业务逻辑。

 enum StatusCodes : Equatable { case PassedValidation case FailedValidation(String) func getFailedMessage() -> String { switch self { case StatusCodes.FailedValidation(let msg): return msg case StatusCodes.OperationFailed(let msg): return msg default: return "" } } } func ==(lhs : StatusCodes, rhs : StatusCodes) -> Bool { switch (lhs, rhs) { case (.PassedValidation, .PassedValidation): return true case (.FailedValidation, .FailedValidation): return true default: return false } } func !=(lhs : StatusCodes, rhs : StatusCodes) -> Bool { return !(lhs == rhs) } func validate(username : String, password : String) -> StatusCodes { if username.isEmpty || password.isEmpty { return StatusCodes.FailedValidation("Username and password are required") } return StatusCodes.PassedValidation } 

2 – 这是一个优先事项,最终由您的应用程序的要求决定。 在我的应用程序中,我通过login()方法,即login(用户名,密码)传递这些值。

3 – 创build一个名为LoginEventsDelegate的协议,然后在其中有一个方法:

 func loginViewModel_LoginCallFinished(successful : Bool, errMsg : String) 

但是,此方法只能用于通知视图控制器试图在远程服务器上login的实际结果。 它应该与validation部分无关。 您的validation程序将按照#1中的讨论进行处理。 让你的视图控制器实现LoginEventsDelegate。 并在您的视图模型即创build一个公共财产

 class LoginViewModel { var delegate : LoginEventsDelegate? } 

然后在你的API调用的完成块中,你可以通过委托即通知视图控制器

 func login() { // Call the login() method in ApiHandler let api = ApiHandler() let successBlock = { [weak self](data) -> Void in if let this = self { this.delegate?.loginViewModel_LoginCallFinished(true, "") } } let errorBlock = { [weak self] (error) -> Void in if let this = self { var errMsg = (error != nil) ? error.description : "" this.delegate?.loginViewModel_LoginCallFinished(error == nil, errMsg) } } api.login(username!, password: password!, success: successBlock, error: errorBlock) } 

你的视图控制器看起来像这样:

 class loginViewController : LoginEventsDelegate { func viewDidLoad() { viewModel.delegate = self } func loginViewModel_LoginCallFinished(successful : Bool, errMsg : String) { if successful { //segue to another view controller here } else { MsgBox(errMsg) } } } 

有人会说,你可以传入一个closures的login方法,完全跳过协议。 有几个原因,我认为这是一个坏主意。

将UI层(UIL)中的闭包传递给业务逻辑层(BLL)会破坏问题分离(SOC)。 Login()方法驻留在BLL中,所以基本上你会说“嘿BLL为我执行这个UIL逻辑”。 这是一个SOC不是!

BLL只能通过委托通知与UIL进行通信。 这样,BLL本质上是说:“嗨,UIL,我完成了我的逻辑,这里有一些数据参数,你可以用它来操作UI控件。”

所以UIL不应该要求BLL为他执行UI控制逻辑。 只应要求BLL通知他。

4 – 我见过ReactiveCocoa,听过好东西,但从未使用过。 所以不能从个人的经验来讲话。 我会看到如何在您的scheme中使用简单的委托通知(如#3所述)。 如果它满足需求,那么很好,如果你正在寻找一些更复杂的东西,那么也许看看ReactiveCocoa。

顺便说一句,这在技术上也不是MVVM的方法,因为绑定和命令没有被使用,但这只是“ta-may-toe”| “ta-mah-toe”挑剔恕我直言。 无论您使用哪种MV *方法,SOC原则都是一样的。

iOS中的MVVM意味着创build一个填充了屏幕使用的数据的对象,与Model类分开。 它通常会映射UI中消耗或生成数据的所有项目,如标签,文本框,数据源或dynamic图像。 它经常会对validation器进行一些简单的inputvalidation(空字段,是否有效的电子邮件,正数,开关是否打开)。 这些validation器通常是独立的类,而不是内联逻辑。

你的视图层知道这个VM类,并观察它的变化,以反映它们,并在用户input数据时更新VM类。 虚拟机中的所有属性都绑定到UI中的项目。 因此,例如,用户进入用户注册屏幕,该屏幕获得除了状态属性具有未完成状态之外没有任何属性填充的VM。 该视图知道只有一个完整的表单可以提交,所以它现在提交button不活动。

然后用户开始填写它的细节,并在电子邮件地址格式中出错。 虚拟机中该字段的validation器现在设置错误状态,View设置错误状态(例如红色边框)和UI中VMvalidation器中的错误消息。

最后,当虚拟机内的所有必填字段获得状态完成虚拟机完成后,视图会观察到,现在将提交button设置为活动状态,以便用户可以提交。 “提交”button操作连线到VC,VC确保虚拟机链接到正确的模型并保存。 有时模型直接用作虚拟机,当你有更简单的类似CRUD的屏幕时,这可能是有用的。

我已经在WPF中使用这种模式,它工作真的很棒。 在Views中设置所有观察者,并在Model类和ViewModel类中放置很多字段,听起来像是很麻烦,但是一个好的MVVM框架将帮助你。 您只需要将UI元素链接到正确types的VM元素,分配正确的Validator,并为您完成了大量的configuration工作,而无需自己添加所有样板代码。

这种模式的一些优点:

  • 它只会公开你需要的数据
  • 更好的可测性
  • 较less的样板代码将UI元素连接到数据

缺点:

  • 现在你需要维护M和VM
  • 你仍然无法完全使用VC iOS。