Arquitectura Model-View-ViewModel (MVVM) en SwiftUI creamos una app desde cero
Arquitectura Model-View-ViewModel (MVVM) en SwiftUI creamos una app desde cero

Aprende a CREAR una APP de LISTA de TAREAS en SwiftUI con MVVM

Aprende SwiftUI y la aquitectura MVVM creando una app real. La app será una lista de tareas en iOS en SwiftUI. Usaremos Model View ViewModel para estructurar la app. Esta tareas podrán ser borradas o marcadas como favoritas por si las queremos priorizar. Aprende MVVM y SwiftUI con este post.

SwiftBeta

Tabla de contenido


👇 SÍGUEME PARA APRENDER SWIFTUI, SWIFT, XCODE, etc 👇
Creamos una app usando MVVM en SwiftUI
Creamos una app usando MVVM en SwiftUI

Hoy en SwiftBeta vamos a ver cómo usar la arquitectura MVVM en SwiftUI. MVVM son las siglas de Model-View-ViewModel. Esta arquitectura nos ayuda a separar nuestro código en:

  • Modelo, son datos, por ejemplo, información que mostramos en una Lista en SwiftUI. Otros ejemplos; structs con información de un User, por ejemplo, su token, su email, su id, etc.
  • View, representación visual de lo que queremos mostrar en la pantalla de nuestra app. Al final las vistas tendrán que tener datos para poder mostrar esta información a los users
  • ViewModel, representa el estado del modelo y se encarga de obtener, manipular este modelo. El ViewModel, genera un binding con la vista, para que cualquier cambio en el modelo haga que se refresque la vista mostrando los nuevos datos.

Al tener estos 3 tipos claramente identificados, podemos crear código más mantenible y testeable. Sobretodo si trabajamos en grandes equipos tocando la misma app.

La idea detrás de MVVM en SwiftUI es que cualquier cambio en un modelo, actualiza automáticamente la vista de SwiftUI. Para ver todo esto en el post de hoy vamos a crear una app muy útil, vamos a crear una Todo list app para ir añadiendo notas, recordatorios de tareas que queremos hacer. También tendremos la posibilidad de una vez creadas, poder borrarlas o marcarlas como favoritos.

MVVM en SwiftUI

Lo primero de todo que vamos hacer es crear un proyecto de cero en Xcode. Y crearemos el modelo de nuestra app. En nuestro caso va a ser muy sencillo, vamos a crear una struct NoteModel con 3 propiedades, y va a ser la primera parte de la arquitectura Model-View-ViewModel:

struct NoteModel: Codable {
    let id: String
    var isFavorited: Bool
    let description: String
    
    init(id: String = UUID().uuidString, isFavorited: Bool = false, description: String) {
        self.id = id
        self.isFavorited = isFavorited
        self.description = description
    }
}
Creamos el modelo en Swift

Lo siguiente que vamos hacer es una parte de la vista, vamos a crear el campo para que el user pueda añadir su nota y un button para que se lance la acción de crearla (de momento mostrará un print por consola, la conectaremos más tarde).
Nos vamos a enfocar en la segunda parte de la arquitectura Model-View-ViewModel

import SwiftUI

struct ContentView: View {
    @State var descriptionNote: String = ""
    
    var body: some View {
        NavigationView {
            VStack {
                Text("Añade una tarea")
                    .underline()
                    .foregroundColor(.gray)
                    .padding(.horizontal, 16)
                TextEditor(text: $descriptionNote)
                    .foregroundColor(.gray)
                    .frame(height: 100)
                    .overlay(
                        RoundedRectangle(cornerRadius: 8)
                            .stroke(.green, lineWidth: 2)
                    )
                    .padding(.horizontal, 12)
                    .cornerRadius(3.0)
                Button("Crear") {
                    descriptionNote = ""
                }
                .buttonStyle(.bordered)
                .tint(.green)
                Spacer()
            }
            .navigationTitle("TODO")
            .navigationBarTitleDisplayMode(.inline)
        }
    }
}
Creamos la vista en SwiftUI

El resultado es la siguiente imagen:

TextEditor y Button en SwiftUI

Y ahora vamos a crear la última parte de la arquitectura Model-View-ViewModel en SwiftUI. Vamos a crear el ViewModel que va a ser el encargado de guardar las notas, obtenerlas, borrarlas, marcarlas como favoritas, etc.

Primero vamos a crear una propiedad @Published llamada notes que será del tipo NotesModel y a continuación crearemos el método de guardar notas.

Las notas queremos guardarlas entre sesiones, entre lanzamientos de la app, es por eso que utilizaremos UserDefaults que nos hará de base de datos. Así esta información no la perderemos.
import Foundation

final class NotesViewModel: ObservableObject {
    @Published var notes: [NoteModel] = []
    
    private let userDefaults: UserDefaults = .standard
    
    init() {
        notes = getAllNotes()
    }
    
    func saveNote(description: String) {
        let newNote = NoteModel(description: description)
        notes.insert(newNote, at: 0)
        encodeAndSaveAllNotes()
    }
    
    private func encodeAndSaveAllNotes() {
        if let encoded = try? JSONEncoder().encode(notes) {
            userDefaults.set(encoded, forKey: "notes")
        }
    }
}
Creamos el ViewModel en Swift para guardar las notas

Si ahora creamos las notas no tendría sentido, ya que el Button de la vista no hace absolutamente nada, solo borrar lo que contiene el TextEditor. Antes de conectar el Button con nuestro ViewModel vamos a crear un método que obtenga todas las notas que hemos almacenado.

Finalmente, nuestro NotesViewModel queda de la siguiente manera, con el método de guardar notas y el método de recuperar todas las notas:

import Foundation

final class NotesViewModel: ObservableObject {
    @Published var notes: [NoteModel] = []
    
    private let userDefaults: UserDefaults = .standard
    
    init() {
        notes = getAllNotes()
    }
    
    func saveNote(description: String) {
        let newNote = NoteModel(description: description)
        notes.insert(newNote, at: 0)
        encodeAndSaveAllNotes()
    }
    
    private func encodeAndSaveAllNotes() {
        if let encoded = try? JSONEncoder().encode(notes) {
            userDefaults.set(encoded, forKey: "notes")
        }
    }
    
    func getAllNotes() -> [NoteModel] {
        if let notesData = userDefaults.object(forKey: "notes") as? Data {
            if let notes = try? JSONDecoder().decode([NoteModel].self, from: notesData) {
                return notes
            }
        }
        return []
    }
}
Creamos método para obtener todas las notas

Ahora iremos a la vista ContentView y crearemos una instancia de NotesViewModel para poder acceder a sus métodos y a la propiedad @Published notes. Conectaremos el Button de crear la nota con el método de saveNote y crearemos un listado en SwiftUI para poder mostrar todas nuestras notas. Utilizaremos un List y dentro un ForEach, mostrando un simple Text (con la información de la nota):

import SwiftUI

struct ContentView: View {
    @State var descriptionNote: String = ""
    @StateObject var notesViewModel = NotesViewModel()
    
    var body: some View {
        NavigationView {
            VStack {
                Text("Añade una tarea")
                    .underline()
                    .foregroundColor(.gray)
                    .padding(.horizontal, 16)
                TextEditor(text: $descriptionNote)
                    .foregroundColor(.gray)
                    .frame(height: 100)
                    .overlay(
                        RoundedRectangle(cornerRadius: 8)
                            .stroke(.green, lineWidth: 2)
                    )
                    .padding(.horizontal, 12)
                    .cornerRadius(3.0)
                Button("Crear") {
	                notesViewModel.saveNote(description: descriptionNote)
                    descriptionNote = ""
                }
                .buttonStyle(.bordered)
                .tint(.green)
                Spacer()
                List {
                    ForEach(notesViewModel.notes, id: \.id) { nota in
                        HStack {
                            Text(nota.description)
                        }
                    }
                }
            }
            .navigationTitle("TODO")
            .navigationBarTitleDisplayMode(.inline)
        }
    }
}
Conectamos la View en SwiftUI con el ViewModel en Swift

Si ahora compilamos, podemos ver que al crear las notas, automáticamente aparecen en el List inferior. Mola bastante que con muy pocas líneas de código tenemos algo muy funcional.

Acciones: Borrar notas o marcarlas como favoritas

Nos vamos al NotesViewModel y vamos a crear dos métodos nuevos. Primero crearemos un método que borre una nota:

    func removeNote(withId id: String) {
        notes.removeAll(where: { $0.id == id })
        encodeAndSaveAllNotes()
    }
Creamos el método para borrar una nota en Swift

muy sencillo, ahora crearemos otro para marcar una nota como favorita:

    func updateFavoriteNote(note: Binding<NoteModel>) {
        note.wrappedValue.isFavorited = !note.wrappedValue.isFavorited
        encodeAndSaveAllNotes()
    }
Creamos el método para marcar una nota como favorita en Swift

Para poder usar estos métodos, vamos a usar lo que ya vimos en otro post. Los swipeActions en SwiftUI. Deslizaremos las celdas de nuestra List en SwiftUI para borrar y para actualizar un nota como favorita.

El resultado de ContentView sería:

import SwiftUI

struct ContentView: View {
    @State var descriptionNote: String = ""
    @StateObject var notesViewModel = NotesViewModel()
    
    var body: some View {
        NavigationView {
            VStack {
                Text("Añade una tarea")
                    .underline()
                    .foregroundColor(.gray)
                    .padding(.horizontal, 16)
                TextEditor(text: $descriptionNote)
                    .foregroundColor(.gray)
                    .frame(height: 100)
                    .overlay(
                        RoundedRectangle(cornerRadius: 8)
                            .stroke(.green, lineWidth: 2)
                    )
                    .padding(.horizontal, 12)
                    .cornerRadius(3.0)
                Button("Crear") {
                    notesViewModel.saveNote(description: descriptionNote)
                    descriptionNote = ""
                }
                .buttonStyle(.bordered)
                .tint(.green)
                Spacer()
                List {
                    ForEach($notesViewModel.notes, id: \.id) { $nota in
                        HStack {
                            if nota.isFavorited {
                                Text("⭐️")
                            }
                            Text(nota.description)
                        }
                        .swipeActions(edge: .trailing) {
                            Button {
                                notesViewModel.updateFavoriteNote(note: $nota)
                            } label: {
                                Label("Favorito", systemImage: "star.fill")
                            }
                            .tint(.yellow)
                        }
                        .swipeActions(edge: .leading) {
                            Button {
                                notesViewModel.removeNote(withId: nota.id)
                            } label: {
                                Label("Borrar", systemImage: "trash.fill")
                            }
                            .tint(.red)
                        }
                    }
                }
            }
            .navigationTitle("TODO")
            .navigationBarTitleDisplayMode(.inline)
        }
    }
}
Añadimos SwipeActions en SwiftUI

Hemos aprovechado y hemos añadido una ⭐️ para identificar solo las notas marcadas como favoritas.

Listado y ForEach de notas en SwiftUI

Si compilamos la app y la probamos vemos que podemos:

  • Añadir notas
  • Listar todas las notas
  • Borrar notas
  • Añadir/Eliminar notas como favoritos

Antes de finalizar, me gustaría mostrar el número de notas creadas, para ello en el ViewModel voy a crear un nuevo método, muy sencillo:

func getNumberOfNotes() -> String {
    "\(notes.count)"
}
Método para obtener el número de notas

Y en la vista del ContentView lo voy a añadir en la NavigationView

.toolbar {
    Text(notesViewModel.getNumberOfNotes())
}
Mostramos el número de notas en la Toolbar
Marcamos como favoritas las notas en SwiftUI con SwipeActions

Conclusión

Fíjate que hemos estructurado la app usando MVVM, Model-View-ViewModel en SwiftUI. Hemos separado responsabilidades y hemos podido añadir código que tiene un próposito muy claro.
De forma muy resumida, podemos decir que el modelo son los datos de nuestra app, la vista nos permite mostrar estos datos por pantalla y el ViewModel sería el cerebro de todo, el que se encarga de orquestar esta información (obtenerla, manipularla, guardarla, aplicar distintas estrategias, etc).