在iOS中使用MVVM

时间:2014-11-28 05:36:11

标签: ios mvvm model controller viewmodel

我是一名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
        }
    }
}
  1. 我的第一个问题是我的MVVM实现是否正确?我有这个疑问,例如我把登录按钮的点击事件(loginButtonPressed)放在控制器中。我没有为登录屏幕创建单独的视图,因为它只有几个文本字段和一个按钮。控制器是否可以将事件方法与UI元素绑定?

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

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

  4. 使用MVVM for iOS时有哪些其他绑定机制? KVO就是其中之一。但是我读到它并不适合大型项目,因为它需要很多样板代码(注册/取消注册观察者等)。还有什么其他选择?我知道ReactiveCocoa是一个用于此的框架,但我希望看看是否有其他原生的。

  5. 我在互联网上的MVVM上遇到的所有材料几乎没有提供有关这些部分的信息我想澄清,所以我非常感谢您的回复。

2 个答案:

答案 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,并为您完成大量此类管道,而无需自己添加所有样板代码。

此模式的一些优点:

  • 它只公开您需要的数据
  • 更好的可测试性
  • 减 用于将UI元素连接到数据的样板代码

<强>缺点:

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