Uso de MVVM en iOS

Soy desarrollador de iOS y soy culpable de tener controlleres de Massive View en mis proyectos, así que he estado buscando una mejor forma de estructurar mis proyectos y encontré la architecture MVVM (Model-View-ViewModel). He estado leyendo una gran cantidad de MVVM con iOS y tengo un par de preguntas. Explicaré mis problemas con un ejemplo.

Tengo un controller de vista llamado 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() } } 

No tiene una class Model. Pero sí creo un model de vista llamado LoginViewModel para poner la lógica de validation y las llamadas de 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. Mi primera pregunta es ¿es correcta mi implementación de MVVM? Tengo esta duda porque, por ejemplo, pongo el evento tap del button de inicio de session ( loginButtonPressed ) en el controller. No creé una vista separada para la pantalla de inicio de session porque solo tiene un par de campos de text y un button. ¿Es aceptable que el controller tenga methods de evento vinculados a elementos de interfaz de usuario?

  2. Mi siguiente pregunta es también sobre el button de inicio de session. Cuando el usuario toca el button, los valores de nombre de usuario y contraseña deben pasar a LoginViewModel para su validation y, si tiene éxito, a la llamada API. Mi pregunta sobre cómo pasar los valores al model de vista. ¿Debo agregar dos parameters al método login() y pasarlos cuando lo llamo desde el controller de vista? ¿O debería declarar properties para ellos en el model de vista y establecer sus valores desde el controller de vista? ¿Cuál es aceptable en MVVM?

  3. Tome el método validate() en el model de vista. El usuario debe ser notificado si alguno de ellos está vacío. Eso significa que después de la comprobación, el resultado debe devolverse al controller de vista para tomar las acciones necesarias (mostrar una alerta). Lo mismo con el método de login() . Avise al usuario si la request falla o vaya al siguiente controller de vista si tiene éxito. ¿Cómo notifico al controller de estos events desde el model de vista? ¿Es posible usar mecanismos bindings como KVO en casos como este?

  4. ¿Cuáles son los otros mecanismos de enlace cuando se usa MVVM para iOS? KVO es uno. Pero leo que no es muy adecuado para proyectos más grandes porque requiere un gran número de código repetitivo (registrar / anular el logging de los observadores, etc.). ¿Cuáles son otras opciones? Sé que ReactiveCocoa es un marco utilizado para esto, pero estoy buscando para ver si hay otros nativos.

Todos los materiales que encontré en MVVM en Internet proporcionaron poca o ninguna información sobre estas partes que estoy buscando para aclarar, así que realmente apreciaría sus respuestas.

    waddup amigo!

    1a- Te diriges en la dirección correcta. Pones loginButtonPressed en el controller de vista y eso es exactamente donde debería estar. Los manejadores de events para controles siempre deben entrar en el controller de vista, por lo que es correcto.

    1b: en su model de vista, tiene comentarios que indican "mostrar al usuario una alerta con el error". No desea mostrar ese error desde la function de validation. En su lugar, cree una enumeración que tenga un valor asociado (donde el valor es el post de error que desea mostrar al usuario). Cambie su método de validation para que devuelva esa enumeración. Luego, dentro de su controller de vista, puede evaluar ese valor de retorno y, desde allí, mostrará el cuadro de dialog de alerta. Recuerde que solo desea usar classs relacionadas con UIKit solo dentro del controller de vista, nunca desde el model de vista. El model de vista solo debe contener lógica empresarial.

     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 requinetworking") } return StatusCodes.PassedValidation } 

    2: esta es una cuestión de preference y, en última instancia, determinada por los requisitos de su aplicación. En mi aplicación paso estos valores a través del método de inicio de session (), es decir, inicio de session (nombre de usuario, contraseña).

    3 – Cree un protocolo denominado LoginEventsDelegate y luego tenga un método como tal:

     func loginViewModel_LoginCallFinished(successful : Bool, errMsg : String) 

    Sin embargo, este método solo debería utilizarse para notificar al controller de vista los resultados reales de intentar iniciar session en el server remoto. No debería tener nada que ver con la porción de validation. Su rutina de validation se manejará como se discutió anteriormente en el # 1. Haga que su controller de vista implemente LoginEventsDelegate. Y cree una propiedad pública en su model de vista, es decir

     class LoginViewModel { var delegate : LoginEventsDelegate? } 

    Luego, en el bloque de finalización de su llamada api, puede notificar al controller de vista a través del delegado, es decir,

     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) } 

    y su controller de vista se vería así:

     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) } } } 

    Algunos dirían que puede pasar un cierre al método de inicio de session y omitir el protocolo por completo. Hay algunas razones por las cuales creo que es una mala idea.

    Pasar un cierre de la Capa de UI (UIL) a la Capa Lógica de Negocio (BLL) rompería la Separación de Preocupaciones (SOC). El método Login () reside en BLL, por lo que básicamente dirías "Hey BLL, ejecuta esta lógica UIL para mí". ¡Eso es un SOC no, no!

    BLL solo debe comunicarse con el UIL mediante notifications de delegates. De esa manera, BLL básicamente dice: "Hey UIL, termino de ejecutar mi lógica y he aquí algunos arguments de datos que puedes usar para manipular los controles de la UI como necesites".

    Por lo tanto, UIL nunca debe pedirle a BLL que ejecute la lógica de control de la interfaz de usuario para él. Solo debe pedirle a BLL que le notifique.

    4 – He visto ReactiveCocoa y escuché cosas buenas sobre él, pero nunca lo he usado. Por lo tanto, no puede hablar de ello por experiencia personal. Vería cómo usar la notificación de delegado simple (como se describe en el n. ° 3) le funciona en su escenario. Si se encuentra con la necesidad entonces genial, si estás buscando algo un poco más complejo, entonces quizás mires a ReactiveCocoa.

    Por cierto, esto tampoco es técnicamente un enfoque MVVM ya que no se utilizan commands y commands, pero eso es solo "ta-may-toe" | "ta-mah-toe" nitpicking humildemente. Los principios de SOC son todos iguales independientemente del enfoque de MV * que uses.

    MVVM en iOS significa crear un object lleno de datos que su pantalla usa, independientemente de las classs de su Modelo. Por lo general, los maps de todos los elementos en su interfaz de usuario que consumen o producen datos, como tags, cuadros de text, fonts de datos o imágenes dinámicas. A menudo hace alguna validation de luz de input (campo vacío, es correo electrónico válido o no, número positivo, el interruptor está activado o no) con validadores. Estos validadores generalmente son classs separadas y no tienen lógica en línea.

    Su capa de vista conoce esta class de VM y observa cambios en ella para reflejarlos y también actualiza la class VM cuando el usuario ingresa datos. Todas las properties en la máquina virtual están vinculadas a elementos en la interfaz de usuario. Así, por ejemplo, un usuario va a una pantalla de logging de usuario, esta pantalla obtiene una máquina virtual que no tiene ninguna de sus properties, excepto la propiedad de estado que tiene un estado Incompleto. La vista sabe que solo se puede enviar un formulario completo, por lo que ahora se desactiva el button Enviar.

    Luego, el usuario comienza a completar sus detalles y comete un error en el formatting de la dirección de correo electrónico. El Validador para ese campo en la VM ahora establece un estado de error y la Vista establece el estado de error (borde rojo, por ejemplo) y el post de error que está en el validador VM en la UI.

    Finalmente, cuando todos los campos requeridos dentro de la máquina virtual obtienen el estado Completar la máquina virtual completa, la vista lo observa y ahora activa el button Enviar para que el usuario pueda enviarlo. La acción del button Enviar está conectada al VC y el VC se asegura de que la VM se vincula con el (los) model (s) correcto (s) y se guarda. A veces, los models se utilizan directamente como una máquina virtual, lo que podría ser útil cuando tenga pantallas más simples similares a CRUD.

    He trabajado con este patrón en WPF y funciona realmente bien. Parece un montón de problemas configurar todos esos observadores en Vistas y poner muchos campos en las classs de models, así como en las classs de ViewModel, pero un buen marco MVVM te ayudará con eso. Solo necesita vincular elementos de interfaz de usuario a elementos de VM del tipo correcto, asignar los validadores correctos y muchas de estas conexiones se realizan por usted sin la necesidad de agregar todo ese código de repetición.

    Algunas ventajas de este patrón:

    • Solo expone los datos que necesita
    • Mejor comprobabilidad
    • Menos código repetitivo para conectar elementos de interfaz de usuario a datos

    Desventajas:

    • Ahora necesita mantener la M y la VM
    • Aún no puedes desplazarte por completo con el VC iOS.