ios - tutorial - mvvm swift 4
Uso de MVVM en iOS (2)
Soy un desarrollador de iOS y soy culpable de tener Massive View Controllers en mis proyectos, así que he estado buscando una mejor manera de estructurar mis proyectos y encontré la arquitectura MVVM (Model-View-ViewModel). He estado leyendo mucho MVVM con iOS y tengo un par de preguntas. Explicaré mis problemas con un ejemplo.
Tengo un controlador 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 clase de modelo. Pero creé un modelo de vista llamado LoginViewModel
para poner la lógica de validación y las llamadas de red.
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
}
}
}
Mi primera pregunta es simplemente ¿es correcta mi implementación de MVVM? Tengo esta duda porque, por ejemplo, puse el evento tap del login (
loginButtonPressed
) en el controlador. No creé una vista separada para la pantalla de inicio de sesión porque solo tiene un par de campos de texto y un botón. ¿Es aceptable que el controlador tenga métodos de eventos relacionados con los elementos de la interfaz de usuario?Mi siguiente pregunta también es sobre el botón de inicio de sesión. Cuando el usuario toca el botón, los valores de nombre de usuario y contraseña deben pasarse al modelo LoginView para su validación y, si tienen éxito, a la llamada API. Mi pregunta sobre cómo pasar los valores al modelo de vista. ¿Debo agregar dos parámetros al método de
login()
y pasarlos cuando lo llamo desde el controlador de vista? ¿O debería declarar propiedades para ellos en el modelo de vista y establecer sus valores desde el controlador de vista? ¿Cuál es aceptable en MVVM?Tome el método
validate()
en el modelo de vista. El usuario debe ser notificado si alguno de ellos está vacío. Eso significa que después de la verificación, el resultado debe devolverse al controlador de vista para tomar las medidas necesarias (mostrar una alerta). Lo mismo con el método delogin()
. Avise al usuario si la solicitud falla o vaya al siguiente controlador de vista si tiene éxito. ¿Cómo notifico al controlador de estos eventos desde el modelo de vista? ¿Es posible usar mecanismos de enlace como KVO en casos como este?¿Cuáles son los otros mecanismos de vinculación al usar MVVM para iOS? KVO es uno. Pero leí que no es muy adecuado para proyectos más grandes porque requiere mucho código repetitivo (registro / anulación del registro de observadores, etc.). ¿Cuáles son otras opciones? Sé que ReactiveCocoa es un marco utilizado para esto, pero estoy buscando 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 aclarar, por lo que realmente agradecería sus respuestas.
MVVM en iOS significa crear un objeto lleno de datos que utiliza su pantalla, por separado de sus clases de modelo. Por lo general, asigna todos los elementos en su UI que consumen o producen datos, como etiquetas, cuadros de texto, fuentes de datos o imágenes dinámicas. A menudo hace una validación leve de la entrada (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 clases separadas, no lógica en línea.
Su capa de Vista conoce esta clase de VM y observa los cambios en ella para reflejarlos y también actualiza la clase de VM cuando el usuario ingresa datos. Todas las propiedades en la máquina virtual están vinculadas a elementos en la interfaz de usuario. Entonces, por ejemplo, un usuario va a una pantalla de registro de usuario, esta pantalla obtiene una VM que no tiene ninguna de sus propiedades ocupadas, excepto la propiedad de estado que tiene un estado Incompleto. La vista sabe que solo se puede enviar un formulario completo por lo que establece el botón Enviar inactivo ahora.
Luego, el usuario comienza a completar sus detalles y comete un error en el formato 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 mensaje de error que está en el validador de VM en la UI.
Finalmente, cuando todos los campos requeridos dentro de la VM obtengan el estado Completar, la VM está Completa, la Vista lo observa y ahora establece el botón Enviar en activo para que el usuario pueda enviarlo. La acción del botón Enviar está conectada al VC y el VC se asegura de que la VM se vincule con los modelos correctos y se guarde. A veces, los Modelos se usan directamente como VM, que pueden ser útiles cuando tiene pantallas más simples similares a CRUD.
He trabajado con este patrón en WPF y funciona realmente genial. Parece una gran cantidad de problemas configurar todos los observadores en Vistas y poner muchos campos en las clases de Modelo, así como las clases de ViewModel, pero un buen marco MVVM te ayudará con eso. Solo necesita vincular los elementos de la interfaz de usuario con los elementos de la VM del tipo correcto, asignar los validadores correctos y muchas de estas conexiones se realizan por usted sin la necesidad de agregar usted mismo todo el código repetitivo.
Algunas ventajas de este patrón:
- Solo expone los datos que necesita
- Mejor capacidad de prueba
- Menos código repetitivo para conectar los elementos de la interfaz de usuario a los datos
Desventajas:
- Ahora necesita mantener tanto la M como la VM
- Aún no puedes moverte por completo usando el VC iOS.
waddup amigo!
1a- Te diriges en la dirección correcta. Pones loginButtonPressed en el controlador de vista y es exactamente donde debería estar. Los controladores de eventos para los controles siempre deben ir al controlador de vista, por lo que es correcto.
1b: en su modelo de vista tiene comentarios que dicen "mostrar al usuario una alerta con el error". No desea mostrar ese error desde dentro de la función de validación. En su lugar, cree una enumeración que tenga un valor asociado (donde el valor es el mensaje de error que desea mostrar al usuario). Cambie su método de validación para que devuelva esa enumeración. Luego, dentro de su controlador de vista puede evaluar ese valor de retorno y desde allí visualizará el cuadro de diálogo de alerta. Recuerde que solo desea usar clases relacionadas con UIKit solo dentro del controlador de vista, nunca desde el modelo de vista. El modelo de vista solo debe contener lógica comercial.
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: esta es una cuestión de preferencia y, en última instancia, está determinada por los requisitos de su aplicación. En mi aplicación, paso estos valores a través del método de inicio de sesión (), es decir, inicio de sesión (nombre de usuario, contraseña).
3 - Cree un protocolo llamado LoginEventsDelegate y luego tenga un método dentro de él como tal:
func loginViewModel_LoginCallFinished(successful : Bool, errMsg : String)
Sin embargo, este método solo debe utilizarse para notificar al controlador de vista los resultados reales de intentar iniciar sesión en el servidor remoto. No debería tener nada que ver con la parte de validación. Su rutina de validación se manejará como se discutió anteriormente en el n. ° 1. Haga que su controlador de vista implemente el evento LoginEventsDelegate. Y cree una propiedad pública en su modelo de vista, es decir,
class LoginViewModel {
var delegate : LoginEventsDelegate?
}
Luego, en el bloque de finalización para su llamada de API puede notificar al controlador 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 controlador 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án que simplemente puede pasar un cierre al método de inicio de sesión y omitir el protocolo por completo. Hay algunas razones por las que creo que es una mala idea.
Pasar un cierre desde la Capa UI (UIL) a la Capa lógica empresarial (BLL) rompería la Separación de preocupaciones (SOC). El método Login () reside en BLL, así que esencialmente estarías diciendo "hey BLL ejecutar esta lógica UIL para mí". Eso es un SOC no no!
BLL solo debe comunicarse con el UIL por medio de notificaciones de delegados. De esta forma, BLL básicamente dice: "Oye, UIL, he terminado de ejecutar mi lógica y aquí hay algunos argumentos de datos que puedes usar para manipular los controles de la interfaz de usuario como necesites".
Entonces UIL nunca debería pedirle a BLL que ejecute la lógica de control de la interfaz de usuario para él. Solo debería pedirle a BLL que le notifique.
4 - He visto ReactiveCocoa y escuché cosas buenas sobre él, pero nunca lo he usado. Entonces no puedo hablar por experiencia personal. Vería cómo funciona la notificación simple de delegado (como se describe en el n. ° 3) en su caso. Si satisface la necesidad, entonces genial, si estás buscando algo un poco más complejo, entonces tal vez busques en ReactiveCocoa.
Por cierto, esto tampoco es técnicamente un enfoque MVVM ya que el enlace y los comandos no se usan, pero eso es solo "ta-may-toe" | "ta-mah-toe" nitpicking en mi humilde opinión. Los principios de SOC son todos iguales independientemente del enfoque MV * que use.