我是一名iOS开发人员,我在项目中拥有大规模视图控制器,所以我一直在寻找一种更好的方法来构建我的项目,并且遇到了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类。但我确实创建了一个名为LoginViewModel
的视图模型来放置验证逻辑和网络调用。
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
}
}
}
我的第一个问题是我的MVVM实现是否正确?我有这个疑问,例如我把登录按钮的点击事件(loginButtonPressed
)放在控制器中。我没有为登录屏幕创建单独的视图,因为它只有几个文本字段和一个按钮。控制器是否可以将事件方法与UI元素绑定?
我的下一个问题也是关于登录按钮。当用户点击按钮时,用户名和密码值应该传递到LoginViewModel进行验证,如果成功,则传递给API调用。我的问题如何将值传递给视图模型。我应该向login()
方法添加两个参数,并在从视图控制器调用它时传递它们吗?或者我应该在视图模型中为它们声明属性并从视图控制器设置它们的值? MVVM中哪一个可以接受?
在视图模型中使用validate()
方法。如果其中任何一个为空,则应通知用户。这意味着在检查之后,应将结果返回给视图控制器以采取必要的操作(显示警报)。 login()
方法也是如此。如果请求失败则提醒用户,如果成功则转到下一个视图控制器。如何从视图模型中通知控制器这些事件?在这种情况下是否可以使用像KVO这样的绑定机制?
使用MVVM for iOS时有哪些其他绑定机制? KVO就是其中之一。但是我读到它并不适合大型项目,因为它需要很多样板代码(注册/取消注册观察者等)。还有什么其他选择?我知道ReactiveCocoa是一个用于此的框架,但我希望看看是否有其他原生的。
我在互联网上的MVVM上遇到的所有材料几乎没有提供有关这些部分的信息我想澄清,所以我非常感谢您的回复。
答案 0 :(得分:35)
waddup老兄!
1a-你正朝着正确的方向前进。您将loginButtonPressed放在视图控制器中,这正是它应该的位置。控件的事件处理程序应始终进入视图控制器 - 这是正确的。1b - 在您的视图模型中,您有评论说明,“向用户显示错误警报”。您不希望在validate函数中显示该错误。而是创建一个具有关联值的枚举(其中值是您要向用户显示的错误消息)。更改您的验证方法,以便它返回该枚举。然后在视图控制器中,您可以评估该返回值,然后从那里显示警告对话框。请记住,您只想在视图控制器中使用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()方法传递这些值,即登录(用户名,密码)。
3 - 创建一个名为LoginEventsDelegate的协议,然后在其中包含一个方法:
func loginViewModel_LoginCallFinished(successful : Bool, errMsg : String)
但是,此方法仅应用于通知视图控制器尝试登录远程服务器的实际结果。它应该与验证部分无关。您的验证例程将按照上面#1中的讨论进行处理。让您的视图控制器实现LoginEventsDelegate。并在您的视图模型上创建一个公共属性,即
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)
}
}
}
有些人会说你只需将一个闭包传递给login方法并完全跳过协议。我认为这是一个坏主意有几个原因。
将UI层(UIL)中的闭包传递到业务逻辑层(BLL)会破坏关注点(SOC)。 Login()方法驻留在BLL中,所以基本上你会说“嘿BLL为我执行这个UIL逻辑”。那是SOC不行!
BLL应该只通过委托通知与UIL通信。这样BLL基本上就是说,“嘿UIL,我已经完成了我的逻辑执行,这里有一些数据参数,可以根据需要用来操作UI控件”。
所以UIL永远不应该要求BLL为他执行UI控制逻辑。应该只要求BLL通知他。
4 - 我见过ReactiveCocoa并听到了很多关于它的事情,但从未使用它。所以不能从个人经验中说出来。我会看到如何使用简单的委托通知(如#3中所述)在您的方案中适用于您。如果它满足了需求那么好,如果你正在寻找一些更复杂的东西,那么可以看看ReactiveCocoa。
顺便说一下,这在技术上也不是MVVM方法,因为绑定和命令没有被使用,但那只是“ta-may-toe”| “ta-mah-toe”挑剔恕我直言。无论您使用哪种MV *方法,SOC原则都是一样的。
答案 1 :(得分:11)
iOS中的MVVM意味着创建一个填充了屏幕使用数据的对象,与Model类分开。它通常映射UI中消耗或生成数据的所有项目,如标签,文本框,数据源或动态图像。它通常使用验证器对输入(空字段,是否有效电子邮件,正数,开关是否打开)进行一些轻微验证。这些验证器通常是单独的类,而不是内联逻辑。
您的View层了解此VM类并观察其中的更改以反映它们,并在用户输入数据时更新VM类。 VM中的所有属性都与UI中的项目相关联。因此,例如,用户进入用户注册屏幕,该屏幕获得除了具有不完整状态的状态属性之外没有填充其属性的VM。 View知道只能提交一个完整表单,因此它现在将“提交”按钮设置为非活动状态。
然后用户开始填写它的详细信息,并在电子邮件地址格式中出错。 VM中该字段的Validator现在设置错误状态,View设置错误状态(例如红色边框)和UI中VM验证器中的错误消息。
最后,当VM中的所有必填字段都获得状态“完成”,“虚拟机已完成”时,View会观察到该状态,现在将“提交”按钮设置为活动状态,以便用户可以提交。 “提交”按钮操作连接到VC,VC确保VM链接到正确的模型并保存。有时,模型直接用作VM,当您使用类似CRUD的屏幕时,这可能很有用。
我在WPF中使用过这种模式,效果非常好。在Views中设置所有观察者并在Model类和ViewModel类中放置很多字段听起来很麻烦但是一个好的MVVM框架会帮助你。您只需将UI元素链接到正确类型的VM元素,分配正确的Validators,并为您完成大量此类管道,而无需自己添加所有样板代码。
此模式的一些优点:
<强>缺点:强>