2014-11-28 24 views
24

Sono uno sviluppatore iOS e sono colpevole di avere Massive View Controller nei miei progetti, quindi ho cercato un modo migliore per strutturare i miei progetti e ho incontrato MVVM (Model-View -ViewModel) architettura. Ho letto molto MVVM con iOS e ho un paio di domande. Spiegherò i miei problemi con un esempio.Utilizzo di MVVM in iOS

Ho un controller di visualizzazione chiamato 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() 
    } 
} 

Non ha una classe modello. Ma ho creato un modello di visualizzazione chiamato LoginViewModel per inserire la logica di convalida e le chiamate di rete.

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. La mia prima domanda è semplicemente è la mia implementazione MVVM corretta? Ho questo dubbio perché, ad esempio, inserisco l'evento di tocco del pulsante di accesso (loginButtonPressed) nel controller. Non ho creato una vista separata per la schermata di accesso perché ha solo un paio di campi di testo e un pulsante. È accettabile che il controller abbia metodi evento legati agli elementi dell'interfaccia utente?

  2. La mia prossima domanda riguarda anche il pulsante di accesso. Quando l'utente tocca il pulsante, i valori del nome utente e della password devono essere passati a LoginViewModel per la convalida e, in caso di esito positivo, alla chiamata API. La mia domanda su come passare i valori al modello di vista. Devo aggiungere due parametri al metodo login() e passarli quando li chiamo dal controller di visualizzazione? O dovrei dichiarare le proprietà per loro nel modello di vista e impostare i loro valori dal controller di visualizzazione? Quale è accettabile in MVVM?

  3. Prendere il metodo validate() nel modello di vista. L'utente dovrebbe essere avvisato se uno di questi è vuoto. Ciò significa che dopo il controllo, il risultato deve essere restituito al controller della vista per intraprendere le azioni necessarie (mostrare un avviso). Stessa cosa con il metodo login(). Avvisa l'utente se la richiesta fallisce o passa al controller della vista successivo se ha successo. Come posso notificare il controller di questi eventi dal modello di visualizzazione? È possibile utilizzare meccanismi vincolanti come KVO in casi come questo?

  4. Quali sono gli altri meccanismi di associazione quando si utilizza MVVM per iOS? KVO è uno. Ma ho letto che non è adatto a progetti più grandi perché richiede un sacco di codice boilerplate (registrando/annullando la registrazione di osservatori, ecc.). Quali sono le altre opzioni? So che ReactiveCocoa è un framework usato per questo, ma sto cercando di vedere se ci sono altri nativi.

Tutti i materiali mi sono imbattuto su MVVM su Internet fornito poca o nessuna informazione su queste parti che sto cercando di chiarire, quindi mi piacerebbe davvero apprezzare le vostre risposte.

+0

È che solo io o qualcun altro non piacciono le richieste di rete fatte anche da un modello di visualizzazione? – SoftDesigner

+0

@SoftDesigner Concordo, è consigliabile non effettuare chiamate di rete nel modello di visualizzazione ma nell'esempio fornito la classe ApiHandler ha estratto correttamente le specifiche su come viene eseguito un accesso. A questo punto è solo una buona ipotesi che ci sia effettivamente una chiamata in rete. L'app potrebbe essere offline e accedere tramite un db locale. Non lo sappiamo e nemmeno il modello di vista (che è come dovrebbe essere). Sarebbe meglio se il tipo di dati per la variabile api fosse un protocollo implementato da ApiHandler. –

risposta

28

waddup dude!

1a- Stai andando nella giusta direzione. Hai messo loginButtonPremuto nel controller di visualizzazione e questo è esattamente dove dovrebbe essere. I gestori di eventi per i controlli devono sempre andare nel controller della vista, in modo che sia corretto.

1b - nel modello di visualizzazione sono presenti commenti che indicano "Mostra all'utente un avviso con l'errore".Non si desidera visualizzare tale errore dall'interno della funzione di convalida. Invece crea un enum che ha un valore associato (dove il valore è il messaggio di errore che vuoi mostrare all'utente). Cambia il tuo metodo di convalida in modo che restituisca quell'enumerazione. Quindi all'interno del tuo controller di visualizzazione puoi valutare quel valore di ritorno e da lì visualizzerai la finestra di avviso. Ricorda che vuoi utilizzare solo le classi relative a Uikit solo all'interno del controller di visualizzazione, mai dal modello di visualizzazione. Il modello di vista dovrebbe contenere solo la logica aziendale.

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 - questa è una questione di preferenza e in definitiva determinata dai requisiti della tua app. Nella mia app trasmetto questi valori tramite il metodo login() i.e. login (username, password).

3 - Creare un protocollo chiamato LoginEventsDelegate e poi avere un metodo all'interno di esso come tale:

func loginViewModel_LoginCallFinished(successful : Bool, errMsg : String) 

Tuttavia questo metodo deve essere utilizzato solo per notificare il controller di vista dei risultati effettivi di tentare di accedere al Server remoto. Non dovrebbe avere nulla a che fare con la parte di convalida. La tua routine di validazione verrà gestita come discusso sopra al punto # 1. Chiedi al tuo controller di visualizzazione di implementare LoginEventsDelegate. E creare una proprietà pubblica sul tuo modello di vista cioè

class LoginViewModel { 
    var delegate : LoginEventsDelegate? 
} 

Poi nel blocco di completamento per la vostra chiamata API è possibile notificare il controller della vista tramite il delegato cioè

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

e il controller di vista sarebbe simile this:

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

Alcuni direbbero che è possibile passare solo una chiusura al metodo di accesso e saltare del tutto il protocollo. Ci sono alcuni motivi per cui penso che sia una cattiva idea.

Passare una chiusura dal Layer UI (UIL) al Business Logic Layer (BLL) interromperebbe Separation of Concerns (SOC). Il metodo Login() risiede in BLL, quindi essenzialmente diresti "hey BLL esegue questa logica UIL per me". Questo è un SOC no no!

BLL deve comunicare solo con UIL tramite notifiche di delegati. In questo modo BLL sta essenzialmente dicendo: "Ehi UIL, ho finito di eseguire la mia logica e qui ci sono alcuni argomenti sui dati che puoi usare per manipolare i controlli dell'interfaccia utente come necessario".

Quindi UIL non dovrebbe mai chiedere a BLL di eseguire la logica di controllo dell'interfaccia utente per lui. Dovrebbe solo chiedere a BLL di avvisarlo.

4 - Ho visto ReactiveCocoa e ho sentito cose positive ma non l'ho mai usato. Quindi non posso parlarci per esperienza personale. Vedrei come utilizzare la semplice notifica dei delegati (come descritto in # 3) funzioni per te nel tuo scenario. Se soddisfa la necessità allora è grandioso, se stai cercando qualcosa di un po 'più complesso allora forse guarda in ReactiveCocoa.

Btw, anche questo non è tecnicamente un approccio MVVM poiché l'associazione e i comandi non vengono utilizzati ma è solo "ta-may-toe" | "ta-mah-toe" pignoli IMHO. I principi SOC sono tutti uguali a prescindere dall'approccio MV * che usi.

+1

Davvero un'ottima risposta qui! Potresti spiegare un po 'di più sulla tua risposta a 3? la parte che dici: segui l'approccio delegato piuttosto che passare una chiusura al metodo di login e saltare il protocollo? o forse dare qualche link correlato di questo problema? Davvero apprezzato!! –

+0

Passare una chiusura dal Layer UI (UIL) al Business Logic Layer (BLL) interromperà Separation of Concerns (SOC). Il metodo Login() risiede in BLL, quindi essenzialmente diresti "hey BLL esegue questa logica UIL per me". Questo è un SOC no no! BLL dovrebbe comunicare con l'UIL solo tramite notifiche di delegati.In questo modo BLL sta essenzialmente dicendo: "Ehi UIL, ho finito di eseguire la mia logica e qui ci sono alcuni argomenti sui dati che puoi usare per manipolare i controlli dell'interfaccia utente come necessario". Quindi UIL non dovrebbe mai chiedere a BLL di eseguire la logica di controllo dell'interfaccia utente per lui. Dovrebbe solo chiedere a BLL di avvisarlo. –

+0

Ma cosa succede se la chiusura contiene una singola istruzione come self.refresh()? Com'è diverso da un metodo delegato che chiama anche lo stesso metodo self.refresh()? Alla fine, il controllo viene comunque trasferito in un metodo di aggiornamento del viewcontroller indipendentemente da un modo di comunicazione selezionato. – dieworld

8

MVVM in iOS significa creare un oggetto pieno di dati utilizzati dallo schermo, separatamente dalle classi del modello. Solitamente mappa tutti gli elementi dell'interfaccia utente che consumano o producono dati, come etichette, caselle di testo, origini dati o immagini dinamiche. Spesso convalida una leggera convalida dell'input (campo vuoto, e-mail valida o no, numero positivo, interruttore attivo o non).Questi validatori sono in genere classi separate non logiche inline.

Il livello View conosce questa classe VM e osserva le modifiche in esso per rifletterle e aggiorna anche la classe VM quando l'utente immette i dati. Tutte le proprietà nella VM sono legate agli elementi nell'interfaccia utente. Quindi, per esempio, un utente va alla schermata di registrazione di un utente, questa schermata ottiene una VM che non ha nessuna delle sue proprietà riempite eccetto la proprietà di stato che ha uno stato Incompleto. La vista sa che solo un modulo completo può essere inviato in modo tale da impostare il pulsante Invia inattivo ora.

Quindi l'utente inizia a compilare i suoi dettagli e commette un errore nel formato dell'indirizzo e-mail. Il Validatore per quel campo nella VM imposta ora uno stato di errore e la Vista imposta lo stato di errore (ad esempio il bordo rosso) e il messaggio di errore che si trova nel validatore VM nell'interfaccia utente.

Infine, quando tutti i campi richiesti all'interno della macchina virtuale ottengono lo stato Completare la VM è Completato, la Vista lo osserva e ora imposta il pulsante Invia su attivo in modo che l'utente possa inviarlo. L'azione del pulsante Invia è collegata al VC e il VC assicura che la VM venga collegata ai modelli giusti e salvata. A volte i modelli vengono utilizzati direttamente come VM, che potrebbe essere utile quando si hanno schermi CRUD più semplici.

Ho lavorato con questo modello in WPF e funziona davvero alla grande. Sembra un sacco di problemi nell'impostare tutti gli osservatori in Views e inserire molti campi nelle classi Model e nelle classi ViewModel, ma un buon framework MVVM ti aiuterà in questo. Hai solo bisogno di collegare gli elementi dell'interfaccia utente agli elementi VM del tipo giusto, assegnare i giusti validatori e molto di questo impianto idraulico viene fatto per te senza la necessità di aggiungere tu stesso tutto il codice boilerplate.

Alcuni vantaggi di questo modello:

  • Si espone solo i dati necessari
  • codice
  • Meglio testability
  • Meno boilerplate per collegare elementi dell'interfaccia utente ai dati

Svantaggi:

  • Ora è necessario mantenere sia la M e la VM
  • Ancora non può completamente aggirare utilizzando il VC iOS.