🔥 FIREBASE CLOUD FIRESTORE - Guarda, Modifica y Borra en la BDD
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))
}
}
}
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)
}
}
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
}
}
}
}
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()
}
}
}
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/
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))
}
}
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))
}
}
}
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
}
}
}
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")
}
}
Vamos a crear otro método en nuestro LinkRepository:
func update(link: LinkModel) {
linkDatasource.update(link: link)
}
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)
}
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)
}
Aquí tienes más información sobre SwipeActions:
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()
}
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)
}
Y finalmente lo creamos en LinkViewModel:
func delete(link: LinkModel) {
linkRepository.delete(link: link)
}
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)
}
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.