Base de datos iOS en Firebase, Cloud Firestore
Base de datos iOS en Firebase, Cloud Firestore

🔥 FIREBASE CLOUD FIRESTORE - Guarda, Modifica y Borra en la BDD

En Firebase Cloud Firestore podemos guardar, modificar o eliminar datos desde nuestra app iOS. Lo único que necesitamos es configurar nuestra base de datos para empezar a realizar operaciones CRUD en ella. Cloud Firestore iOS

SwiftBeta

Tabla de contenido


👇 SÍGUEME PARA APRENDER SWIFTUI, SWIFT, XCODE, etc 👇
Aprende sobre la base de datos Cloud Firestore de Firebase en iOS
Aprende sobre la base de datos Cloud Firestore de Firebase en iOS

En el anterior post vimos a cómo crear nuestra base de datos Cloud Firestore, guardamos un enlace añadiendo toda esta información en nuestra base de datos de forma manual y también vimos a cómo obtener esta información para mostrarla en nuestra app.
Hoy vamos a permitir a un user crear enlaces desde dentro de la app (sin tener que entrar a la consola de Firebase) y para ello crearemos un TextEditor con un Button, el user solo deberá pegar un enlace en este campo y tendremos una clase que se encargará de obtener el Title del enlace. Es decir, cuando se pulse el Button pasarán dos cosas:

  • Se buscará la información necesaria del enlace, para ello extraeremos solo el title usando LPMetadataProvider (esta clase es de uno de los Frameworks nativos de Apple). Si quisiéramos también podríamos obtener el thumbnail de la URL, pero para acortar el video solo recogeremos como metadatos el title asociado a un enlace.

    Es decir, al poner la siguiente URL:
    https://www.swiftbeta.com/firebase-authentication-email-y-password-con-swiftui/
    LPMetadataProvider me devolverá información como el título de mi artículo:
    FIREBASE Authentication - LOGIN con Email y CONTRASEÑA en SwiftUI y Xcode
  • Y una vez tenemos la información necesaria, crearemos nuestro modelo de dominio LinkModel y se lo pasaremos a una función nueva para que lo guarde en nuestra base de datos Cloud Firestore.

Para poder obtener los metadatos de la URL crearemos una clase para este propósito. Lo que vamos hacer ahora va a ser muy interesante, vamos a crear un nuevo Datasource con el único propósito de obtener los metadatos de una URL. El propósito de crear otro Datasource es para separar mejor las responsabilidades de clases:

import Foundation
import LinkPresentation

enum CustomMetadataError: Error {
    case badURL
}

final class MetadataDatasource {
    private var metadataProvider: LPMetadataProvider?
    
    func getMetadata(fromURL url: String, completionBlock: @escaping (Result<LinkModel, Error>) -> Void) {
        guard let url = URL(string: url) else {
            completionBlock(.failure(CustomMetadataError.badURL))
            return
        }
        metadataProvider = LPMetadataProvider()
        metadataProvider?.startFetchingMetadata(for: url) { metadata, error in
            if let error = error {
                print("Error retrieving metadata \(error.localizedDescription)")
                completionBlock(.failure(error))
                return
            }
            let linkModel = LinkModel(url: url.absoluteString,
                                      title: metadata?.title ?? "No title",
                                      isFavorited: false,
                                      isCompleted: false)
            completionBlock(.success(linkModel))
        }
    }
}
Clase MetadataDatasource para extraer el título de un enlace

Ahora crearemos una instancia de nuestro MetadataDatasource en LinkRepository y crearemos un método para llamar a nuestro nuevo datasource.

import Foundation

final class LinkRepository {
    private let linkDatasource: LinkDatasource
    private let metadataDatasource: MetadataDatasource
    
    init(linkDatasource: LinkDatasource = LinkDatasource(),
         metadataDatasource: MetadataDatasource = MetadataDatasource()) {
        self.linkDatasource = linkDatasource
        self.metadataDatasource = metadataDatasource
    }
    
    func getAllLinks(completionBlock: @escaping (Result<[LinkModel], Error>) -> Void) {
        self.linkDatasource.getAllLinks(completionBlock: completionBlock)
    }
    
    func createNewLink(withURL url: String, completionBlock: @escaping (Result<LinkModel, Error>) -> Void) {
        self.metadataDatasource.getMetadata(fromURL: url,
                                            completionBlock: completionBlock)
    }
}
Creamos el método en el LinkRepository

Y este nuevo método lo vamos a llamar desde LinkViewModel:

import Foundation

final class LinkViewModel: ObservableObject {
    @Published var links: [LinkModel] = []
    @Published var messageError: String?
    private let linkRepository: LinkRepository
    
    init(linkRepository: LinkRepository = LinkRepository()) {
        self.linkRepository = linkRepository
    }
    
    func getAllLinks() {
        linkRepository.getAllLinks { [weak self] result in
            switch result {
            case .success(let linkModels):
                self?.links = linkModels
            case .failure(let error):
                self?.messageError = error.localizedDescription
            }
        }
    }
    
    func createNewLink(fromURL url: String) {
        linkRepository.createNewLink(withURL: url) { result in
            switch result {
            case .success(let link):
                self.links.append(link)
            case .failure(let error):
                self.messageError = error.localizedDescription
            }
        }
    }
}
Creamos el método en LinkViewModel

Y nos vamos a la vista LinkView para añadir un TextEditor en SwiftUI con un Button. También vamos a añadir que se muestre un mensaje de error si ha habido algún error al extraer los metadatos de una URL.
Estas vistas las vamos a añadir arriba, y quedará de la siguiente manera nuestra vista:

import SwiftUI

struct LinkView: View {
    @ObservedObject var linkViewModel: LinkViewModel
    @State var text: String = ""
    
    var body: some View {
        VStack {
            TextEditor(text: $text)
                .frame(height: 100)
                .overlay(
                    RoundedRectangle(cornerRadius: 8)
                        .stroke(.green, lineWidth: 2)
                )
                .padding(.horizontal, 12)
                .cornerRadius(3.0)
            Button(action: {
                linkViewModel.createNewLink(fromURL: text)
            }, label: {
                Label("Crear Link", systemImage: "link")
            })
            .tint(.green)
            .controlSize(.regular)
            .buttonStyle(.bordered)
            .buttonBorderShape(.capsule)
            if (linkViewModel.messageError != nil) {
                Text(linkViewModel.messageError!)
                    .bold()
                    .foregroundColor(.red)
            }
            List {
                ForEach(linkViewModel.links) { link in
                    VStack {
                        Text(link.title)
                            .bold()
                            .lineLimit(4)
                            .padding(.bottom, 8)
                        Text(link.url)
                            .foregroundColor(.gray)
                            .font(.caption)
                        HStack {
                            Spacer()
                            if link.isCompleted {
                                Image(systemName: "checkmark.circle.fill")
                                    .resizable()
                                    .foregroundColor(.blue)
                                    .frame(width: 10, height: 10)
                            }
                            if link.isFavorited {
                                Image(systemName: "star.fill")
                                    .resizable()
                                    .foregroundColor(.yellow)
                                    .frame(width: 10, height: 10)
                            }
                        }
                    }
                }
            }
        }
        .task {
            linkViewModel.getAllLinks()
        }
    }
}
Añadimos TextEditor y Button en SwiftUI

También hemos conectado que al pulsar el Button de Crear Link llame al método que toca en nuestro LinkViewModel. Una vez acabada la vista vamos a compilar nuestra app y la vamos a probar en el simulador, lo primero de todo que vamos hacer es poner una URL en nuestro TextEditor y vamos a ver qué ocurre al pulsar el Button de Crear Link. En mi caso voy a poner esta:

https://www.swiftbeta.com/como-crear-un-modulo-con-swift-package-manager-y-hacerlo-open-source/

App conectada a Firebase Cloud Firestore iOS
App conectada a Firebase Cloud Firestore iOS

Vemos que nuestro MetadataDatasource nos extrae el título de nuestro enlace. Y se refresca nuestra List con la nueva información del enlace.


Guardamos en la base de datos Cloud Firestore

Esto está muy bien, pero ahora nos falta guardarlo en nuestra base de datos de Firebase. Lo que estamos haciendo ahora es recuperar esta información y mostrarla directamente en nuestro listado. Lo que significa que está en memoria y por lo tanto si compilamos la app otra vez perdemos esta información.

Vamos a nuestro LinkDatasource donde crearemos un método nuevo para almacenar nuestro LinkModel en la base de datos de Firebase.

    func createNew(link: LinkModel, completionBlock: @escaping (Result<LinkModel, Error>) -> Void) {
        do {
            _ = try database.collection(collection).addDocument(from: link)
            completionBlock(.success(link))
        } catch {
            completionBlock(.failure(error))
        }
    }
Añadimos método en LinkDatasource para guardar en nuestra base de datos Cloud Firestore

Este método lo vamos a llamar desde nuestro LinkRepository. ¿Pero desde dónde? lo vamos a llamar desde el método createNewLink, si hemos podido obtener el title nos interesa guardarlo en la base de datos de Firebase. Haríamos lo siguiente:

func createNewLink(withURL url: String, completionBlock: @escaping (Result<LinkModel, Error>) -> Void) {
        self.metadataDatasource.getMetadata(fromURL: url) { [weak self] result in
            switch result {
            case .success(let linkModel):
                self?.linkDatasource.createNew(link: linkModel,
                                               completionBlock: completionBlock)
            case .failure(let error):
                completionBlock(.failure(error))
            }
        }
    }
En LinkRepository llamamos a nuestro nuevo método dentro del completionBlock de MetadataDatasource
Aquí podríamos usar async/await, la nueva funcionalidad añadida en Swift para evitar este anidamiento de closures. Pero lo veremos en otro video

Y por último, antes de compilar, nos vamos a nuestro LinkViewModel y ya no añadimos el link a nuestro array de links, ¿por qué? porque cualquier nuevo enlace que añadamos a nuestra base de datos, se verá reflejado automáticamente desde nuestra app. Así que hacemos lo siguiente:

    func createNewLink(fromURL url: String) {
        linkRepository.createNewLink(withURL: url) { result in
            switch result {
            case .success(let link):
                // Aplicar la lógica que quieras, mostrar un alert?
                print("✅ New link \(link.title) added")
            case .failure(let error):
                self.messageError = error.localizedDescription
            }
        }
    }
Creamos el método en LinkViewModel

Ahora podemos añadir nuevos enlaces en nuestro TextEditor y si todo va bien aparecen en el listado de enlaces. Lo que vamos hacer a continuación es poder actualizar que un enlaces sea favorito o esté completado.

Modificar valores de nuestra base de datos

Para hacer esta parte deberemos crear un nuevo método para que actualice los campos de la base de datos con la información que queremos, en nuestro caso dejaremos que un user modifique dos valores:

  • Modificar la propiedad isFavorited
  • Modificar la propiedad isCompleted

Pues como siempre, nos vamos a nuestra capa inferior y añadimos el siguiente método en nuestro LinkDatasource

    func update(link: LinkModel) {
        guard let documentId = link.id else {
            return
        }
        do {
            _ = try database.collection(collection).document(documentId).setData(from: link)
        } catch {
            print("Error updating is favorited in database")
        }
    }
Método para modificar valores de nuestro LinkModel en la base de datos Cloud Firestore

Vamos a crear otro método en nuestro LinkRepository:

    func update(link: LinkModel) {
        linkDatasource.update(link: link)
    }
Creamos el método en LinkRepository

Y finalmente creamos dos métodos en el LinkViewModel:

    func updateIsFavorited(link: LinkModel) {
        let updatedLink = LinkModel(id: link.id,
                                url: link.url,
                                title: link.title,
                                    isFavorited: link.isFavorited ? false : true,
                                isCompleted: link.isCompleted)
        linkRepository.update(link: updatedLink)
    }
    
    func updateIsCompleted(link: LinkModel) {
        let updatedLink = LinkModel(id: link.id,
                                url: link.url,
                                title: link.title,
                                    isFavorited: link.isFavorited,
                                isCompleted: link.isCompleted ? false : true)
        linkRepository.update(link: updatedLink)
    }
Creamos dos método en LinkViewModel

Ahora lo único que nos falta es conectar la vista con los nuevos métodos de nuestro LinkViewModel. Nos vamos a LinkView y vamos a añadir dos swipe actions en SwiftUI, justo en el VStack que está dentro del ForEach:

.swipeActions(edge: .trailing) {
    Button(action: {
        linkViewModel.updateIsFavorited(link: link)
    }, label: {
        Label("Favorito", systemImage: "star.fill")
    })
    .tint(.yellow)
    Button(action: {
        linkViewModel.updateIsCompleted(link: link)
    }, label: {
        Label("Completado", systemImage: "checkmark.circle.fill")
    })
    .tint(.blue)
}
Usamos el modificador .swipeActions para llamar a los métodos de nuestro LinkViewModel

Aquí tienes más información sobre SwipeActions:

SwipeActions y Refreshable en SwiftUI en Español
swipeActions en SwiftUI y refreshable en SwiftUI son dos modificadores que usamos para poder lanzar acciones. swipeActions en SwiftUI lo usamos para añadir acciones en nuestras celdas.refreshable en SwiftUI lo usamos para realizar una acción en nuestra List.

Si compilamos nuestra app, y hacemos un swipe (deslizar hacía la izquierda) en un enlace y pulsamos cualquiera de las dos opciones, vemos como el valor se actualiza 🎉

Borrar datos de nuestra base de datos

Por último, vamos a ver a cómo información en nuestra base de datos. Imagina que has perdido el interés en uno de los enlaces que tienes almacenados. Ahora vamos a ver a cómo borrar información. Usaremos otro swipe action pero en lugar de arrastrar la celda hacía la izquierda, la arrastraremos hacía la derecha para mostrar un icono nuevo que sea una basura.

Lo primero de todo es crear un método en nuestro LinkDatasource:

    func delete(link: LinkModel) {
        guard let documentId = link.id else {
            return
        }
        database.collection(collection).document(documentId).delete()
    }
Método para borrar un LinkModel en nuestra base de datos Cloud Firestore

Creamos otro método en LinkRepository que llame al nuevo método que acabamos de crear en LinkDatasource:

    func delete(link: LinkModel) {
        linkDatasource.delete(link: link)
    }
Creamos el método en nuestro LinkRepository

Y finalmente lo creamos en LinkViewModel:

    func delete(link: LinkModel) {
        linkRepository.delete(link: link)
    }
Creamos el método en nuestro LinkViewModel

Ahora este método lo vamos a llamar desde la vista. Para ello nos vamos al modificador swipeActions que hemos añadido hace un momento y añadimos otra vez otro modificador swipeActions, es decir, nos quedaría de la siguiente manera:

.swipeActions(edge: .trailing) {
    Button(action: {
        linkViewModel.updateIsFavorited(link: link)
    }, label: {
        Label("Favorito", systemImage: "star.fill")
    })
    .tint(.yellow)
    Button(action: {
        linkViewModel.updateIsCompleted(link: link)
    }, label: {
        Label("Completado", systemImage: "checkmark.circle.fill")
    })
    .tint(.blue)
}
.swipeActions(edge: .leading) {
    Button {
        linkViewModel.delete(link: link)
    } label: {
        Label("Borrar", systemImage: "trash.fill")
    }
    .tint(.red)
}
Añadimos el modificador .swipeActions en SwiftUI para poder llamar al método de borrar un enlace (link)

Si compilamos nuestra app vamos a probar que podemos eliminar enlaces de nuestra base de datos.

Conclusión

Hoy hemos aprendido a guardar, modificar y eliminar datos de nuestra base de datos Cloud Firestore de Firebase. Para ello hemos utilizado una app para almacenar enlaces, y hemos podido ver en tiempo real todos los cambios en nuestra base de datos.