Je suis développeur iOS et je suis coupable d'avoir des contrôleurs de vue massifs dans mes projets, j'ai donc cherché une meilleure façon de structurer mes projets et suis tombé sur l'architecture MVVM (Model-View-ViewModel). J'ai lu beaucoup de MVVM avec iOS et j'ai quelques questions. Je vais expliquer mes problèmes avec un exemple.
J'ai un contrôleur de vue appelé 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()
}
}
Il n'a pas de classe Model. Mais j'ai créé un modèle de vue appelé LoginViewModel
pour mettre la logique de validation et les appels réseau.
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
}
}
}
Ma première question est simplement mon implémentation MVVM est-elle correcte? J'ai ce doute car, par exemple, je mets l'événement tap du bouton de connexion (loginButtonPressed
) dans le contrôleur. Je n'ai pas créé de vue distincte pour l'écran de connexion car il ne contient que quelques champs de texte et un bouton. Est-il acceptable que le contrôleur ait des méthodes d'événement liées aux éléments de l'interface utilisateur?
Ma prochaine question concerne également le bouton de connexion. Lorsque l'utilisateur appuie sur le bouton, les valeurs de nom d'utilisateur et de mot de passe doivent être transmises au LoginViewModel pour validation et en cas de succès, puis à l'appel d'API. Ma question comment passer les valeurs au modèle de vue. Dois-je ajouter deux paramètres à la méthode login()
et les transmettre lorsque je l'appelle depuis le contrôleur de vue? Ou dois-je leur déclarer des propriétés dans le modèle de vue et définir leurs valeurs à partir du contrôleur de vue? Lequel est acceptable dans MVVM?
Prenez la méthode validate()
dans le modèle de vue. L'utilisateur doit être informé si l'un d'eux est vide. Cela signifie qu'après la vérification, le résultat doit être renvoyé au contrôleur de vue pour prendre les mesures nécessaires (afficher une alerte). Même chose avec la méthode login()
. Alertez l'utilisateur si la demande échoue ou passez au contrôleur de vue suivant si elle réussit. Comment informer le contrôleur de ces événements à partir du modèle de vue? Est-il possible d'utiliser des mécanismes de liaison comme KVO dans des cas comme celui-ci?
Quels sont les autres mécanismes de liaison lors de l'utilisation de MVVM pour iOS? Le KVO en est un. Mais j'ai lu que ce n'était pas tout à fait approprié pour des projets plus importants car cela nécessite beaucoup de code standard (enregistrement/désinscription des observateurs, etc.). Quelles sont les autres options? Je sais que ReactiveCocoa est un cadre utilisé pour cela, mais je cherche à voir s'il y en a d'autres natifs.
Tous les documents que j'ai rencontrés sur MVVM sur Internet ont fourni peu ou pas d'informations sur ces parties que je cherche à clarifier, donc j'apprécierais vraiment vos réponses.
mec waddup!
1a- Vous vous dirigez dans la bonne direction. Vous mettez loginButtonPressed dans le contrôleur de vue et c'est exactement là où il devrait être. Les gestionnaires d'événements pour les contrôles doivent toujours aller dans le contrôleur de vue - c'est donc correct.
1b - dans votre modèle de vue, vous avez des commentaires indiquant "afficher à l'utilisateur une alerte avec l'erreur". Vous ne voulez pas afficher cette erreur à partir de la fonction de validation. Au lieu de cela, créez une énumération qui a une valeur associée (où la valeur est le message d'erreur que vous souhaitez afficher à l'utilisateur). Modifiez votre méthode de validation afin qu'elle renvoie cette énumération. Ensuite, dans votre contrôleur de vue, vous pouvez évaluer cette valeur de retour et à partir de là, vous afficherez la boîte de dialogue d'alerte. N'oubliez pas que vous ne souhaitez utiliser les classes liées à UIKit que dans le contrôleur de vue - jamais à partir du modèle de vue. Le modèle de vue ne doit contenir que la logique métier.
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 - c'est une question de préférence et finalement déterminée par les exigences de votre application. Dans mon application, je transmets ces valeurs via la méthode login (), c'est-à-dire login (nom d'utilisateur, mot de passe).
3 - Créez un protocole nommé LoginEventsDelegate puis ayez une méthode en tant que telle:
func loginViewModel_LoginCallFinished(successful : Bool, errMsg : String)
Cependant, cette méthode ne doit être utilisée que pour informer le contrôleur de vue des résultats réels de la tentative de connexion au serveur distant. Cela ne devrait rien avoir à voir avec la partie validation. Votre routine de validation sera traitée comme discuté ci-dessus dans # 1. Demandez à votre contrôleur de vue d'implémenter LoginEventsDelegate. Et créez une propriété publique sur votre modèle de vue, c'est-à-dire.
class LoginViewModel {
var delegate : LoginEventsDelegate?
}
Ensuite, dans le bloc d'achèvement de votre appel API, vous pouvez informer le contrôleur de vue via le délégué, c'est-à-dire.
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)
}
et votre contrôleur de vue ressemblerait à ceci:
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)
}
}
}
Certains diraient que vous pouvez simplement passer une fermeture à la méthode de connexion et ignorer complètement le protocole. Il y a plusieurs raisons pour lesquelles je pense que c'est une mauvaise idée.
Passer une fermeture de la couche UI (UIL) à la couche logique métier (BLL) romprait la séparation des préoccupations (SOC). La méthode Login () réside dans BLL, donc vous diriez essentiellement "hey BLL exécute cette logique UIL pour moi". C'est un SOC non non!
BLL ne doit communiquer avec l'UIL que via les notifications des délégués. De cette façon, BLL dit essentiellement: "Hé UIL, j'ai fini d'exécuter ma logique et voici quelques arguments de données que vous pouvez utiliser pour manipuler les contrôles de l'interface utilisateur comme vous le souhaitez".
UIL ne doit donc jamais demander à BLL d'exécuter la logique de contrôle de l'interface utilisateur pour lui. Ne devrait demander qu'à BLL de le notifier.
4 - J'ai vu ReactiveCocoa et j'en ai entendu de bonnes choses mais je ne l'ai jamais utilisé. Je ne peux donc pas en parler par expérience personnelle. Je verrais comment l'utilisation de la simple notification de délégué (comme décrit au point 3) fonctionne pour vous dans votre scénario. Si cela répond au besoin, tant mieux, si vous cherchez quelque chose d'un peu plus complexe, alors regardez peut-être dans ReactiveCocoa.
Btw, ce n'est pas non plus techniquement une approche MVVM puisque la liaison et les commandes ne sont pas utilisées mais c'est juste "ta-may-toe" | "ta-mah-toe" piqûre à mon humble avis. Les principes SOC sont les mêmes quelle que soit l'approche MV * que vous utilisez.
MVVM dans iOS signifie créer un objet rempli de données que votre écran utilise, séparément de vos classes de modèle. Il mappe généralement tous les éléments de votre interface utilisateur qui consomment ou produisent des données, comme les étiquettes, les zones de texte, les sources de données ou les images dynamiques. Il effectue souvent une légère validation de l'entrée (champ vide, est un e-mail valide ou non, nombre positif, interrupteur est activé ou non) avec des valideurs. Ces validateurs sont généralement des classes distinctes et non une logique en ligne.
Votre couche View connaît cette classe VM et en observe les changements pour les refléter et met également à jour la classe VM lorsque l'utilisateur entre des données. Toutes les propriétés de la VM sont liés aux éléments de l'interface utilisateur. Ainsi, par exemple, un utilisateur accède à un écran d'enregistrement utilisateur, cet écran obtient un VM qui n'a aucune de ses propriétés remplies) à l'exception de la propriété de statut qui a le statut Incomplet. La vue sait que seul un formulaire complet peut être soumis, ce qui rend le bouton Soumettre inactif maintenant.
Ensuite, l'utilisateur commence à remplir ses détails et fait une erreur dans le format de l'adresse e-mail. Le validateur pour ce champ dans le VM définit maintenant un état d'erreur et la vue définit l'état d'erreur (bordure rouge par exemple) et le message d'erreur qui se trouve dans le VM = validateur dans l'interface utilisateur.
Enfin, lorsque tous les champs obligatoires à l'intérieur du VM obtiennent le statut Terminer le VM est terminé, la vue observe cela et définit maintenant le bouton Soumettre comme actif pour l'utilisateur peut le soumettre. L'action du bouton Soumettre est câblée au VC et au VC s'assure que le VM obtient liés au (x) bon (s) modèle (s) et enregistrés. Parfois, les modèles sont utilisés directement en tant que machine virtuelle, ce qui peut être utile lorsque vous disposez d'écrans plus simples de type CRUD.
J'ai travaillé avec ce modèle dans WPF et cela fonctionne vraiment bien. Cela ressemble à beaucoup de mal à configurer tous ces observateurs dans les vues et à mettre beaucoup de champs dans les classes Model ainsi que dans les classes ViewModel, mais un bon framework MVVM vous y aidera. Il vous suffit de lier les éléments de l'interface utilisateur aux éléments VM du bon type, attribuez les bons validateurs et une grande partie de cette plomberie est effectuée pour vous sans avoir à ajouter vous-même tout ce code passe-partout.
Quelques avantages de ce modèle:
Inconvénients:
L'architecture MVVM dans iOS peut être facilement implémentée sans utiliser de dépendances tierces. Pour la liaison de données, nous pouvons utiliser une combinaison simple de fermeture et de didSet pour éviter les dépendances de tiers.
public final class Observable<Value> {
private var closure: ((Value) -> ())?
public var value: Value {
didSet { closure?(value) }
}
public init(_ value: Value) {
self.value = value
}
public func observe(_ closure: @escaping (Value) -> Void) {
self.closure = closure
closure(value)
}
}
Un exemple de liaison de données de ViewController:
final class ExampleViewController: UIViewController {
private func bind(to viewModel: ViewModel) {
viewModel.items.observe(on: self) { [weak self] items in
self?.tableViewController?.items = items
// self?.tableViewController?.items = viewModel.items.value // This would be Momory leak. You can access viewModel only with self?.viewModel
}
// Or in one line:
viewModel.items.observe(on: self) { [weak self] in self?.tableViewController?.items = $0 }
}
override func viewDidLoad() {
super.viewDidLoad()
bind(to: viewModel)
viewModel.viewDidLoad()
}
}
protocol ViewModelInput {
func viewDidLoad()
}
protocol ViewModelOutput {
var items: Observable<[ItemViewModel]> { get }
}
protocol ViewModel: ViewModelInput, ViewModelOutput {}
final class DefaultViewModel: ViewModel {
let items: Observable<[ItemViewModel]> = Observable([])
// Implmentation details...
}
Plus tard, il peut être remplacé par SwiftUI et Combine (lorsqu'une version iOS minimale de votre application est de 13)
Dans cet article, il existe une description plus détaillée de MVVM https://tech.olx.com/clean-architecture-and-mvvm-on-ios-c9d167d9f5b