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

🔥 FIREBASE CLOUD FIRESTORE - Base de datos iOS (Swift y Xcode) #5

Tutorial iOS Firebase Cloud Firestore para crear una base de datos en Firebase. Primeros pasos para guardar información de tu app iOS en Cloud Firestore. Conecta tu app a tu base de datos usando el framework de Cloud Firestore, guarda datos, crea queries, modifica campos, etc

SwiftBeta

Tabla de contenido

Base de Datos iOS en Firebase

Hoy en SwiftBeta vamos a ver a cómo crear una base de datos en Firebase para poder utilizarla desde nuestra app.

En Firebase podemos encontrar varios tipos de bases de datos, en el video de hoy usaremos la base de datos Cloud Firestore. Es la base de datos más nueva de Firebase para desarrollo móvil (es no SQL, es decir no es relacional, es escalable y está alojada en el Cloud). Pero no te preocupes, lo vamos a ver más claro en un momento.

También tenemos otro tipo de base de datos que se llama Realtime Database. Si no sabes cuál usar, Firebase tiene un cuestionario muy sencillo para orientarte y así guiarte para que uses la base de datos que mejor se adapte a tus necesidades.

En el siguiente enlace encontraras el cuestionario:
https://firebase.google.com/docs/database/rtdb-vs-firestore?authuser=0

Una vez has escogido qué base de datos utilizar puedes ir a la sección de pricing de Firebase para ver las cuotas. En este enlace verás una tabla muy parecida a la siguiente imagen:

Tabla de precios base de datos Cloud Firestore en Firebase
Tabla de precios base de datos Cloud Firestore en Firebase

Aquí vemos el límite gratuito, unas 20.000 operaciones de escritura al día y otras 20.000 de eliminación al día. Y 50.000 de lectura, con una salida de red de 10Gb al mes. A partir de estos límites se pagaría por consumo. Pero si llegáis a estos números quizás vuestra app ya está yendo lo suficientemente bien.


Vamos a seguir con la serie de post de Firebase y crearemos esta base de datos para almancenar enlaces (URLs), ¿esto que significa? imagínate que encuentras un enlace que es muy interesante (por ejemplo un enlace de la web de SwiftBeta) y lo quieres guardar para leerlo más tarde con calma.

Si quieres apoyar el canal puedes suscribirte a él y participar dando Likes o comentarios en los videos

Dentro de nuestra app almacenaremos estos enlaces para poderlos leer más tarde. Solo dejaremos que se guarden estos enlaces si el user está logueado (para ello debes haber visto los posts o videos anteriores).
Tendremos un campo donde el user pegará un enlace y lo guardaremos en nuestra base de datos. Si nos logueamos con otro iPhone veremos en tiempo real como recibimos ese nuevo enlace también en ese dispositivo.

Crear base de datos Cloud Firestore en Firebase

Para crear nuestra base de datos, vamos a ir a nuestra consola de Firebase. Allí navegamos a la sección de Firestore Database (que aparece en el menu lateral izquierdo).

Cómo crear una base de datos en Firebase
Cómo crear una base de datos en Firebase

Una vez estamos en la sección de Cloud Firestore pulsamos el Button de Crear base de datos. Allí nos aparecerá la siguiente ventana:

Reglas de nuestra base de datos en Firebase
Reglas de nuestra base de datos en Firebase

Cuando creamos una base de datos podemos añadir una serie de reglas para proteger los datos que se almacenen en ella. Ahora vamos a elegir el modo de prueba y más tarde añadiremos las reglas para proteger nuestra base de datos.

Seleccionamos las reglas de nuestra base de datos "modo de prueba"
Seleccionamos las reglas de nuestra base de datos "modo de prueba"

y en el siguiente paso escogemos la región en la que queremos nuestra base de datos. Esta acción no se puede modificar así que escoger una ubicación cercana a la tuya, ya que es donde estarán alojados los servidores físicos con tu base de datos. Te interesa que estén "cerca" para tener la menor latencia posible.

Escogemos la localización física de nuestro servidor en el que estará alojada nuestra base de datos
Escogemos la localización física de nuestro servidor en el que estará alojada nuestra base de datos

Una vez seguidos todos estos pasos le damos al Button de Habilitar

Creamos nuestra base de datos Cloud Firestore
Creamos nuestra base de datos Cloud Firestore

Esperamos unos segundos y deberíamos de tener acceso a nuestra base de datos complemente vacía.

Base de datos Cloud Firestore completamente vacía
Base de datos Cloud Firestore completamente vacía

Aquí tenemos distintas secciones:

  • Datos, aquí crearemos nuestras colecciones y dentro de las colecciones crearemos documentos. Estos documentos representaran el modelo de la información de nuestro enlace (de la URL) que queremos almacenar. Ahora entraremos más en detalle.
  • Reglas, estas son las reglas que aplicamos para proteger a nuestra app. Al final del video añadiremos una regla para que solo los usuarios registrados en nuestra app puedan guardar información.
  • Indices, aquí especificaremos cuáles son los indices de nuestros datos. Muy útil para que cuando hagamos alguna query a nuestra base de datos sea más rápida.
  • Uso, gráficas con métricas del uso de nuestra base de datos.

Volvemos a la sección de los datos, aquí vamos a crear una colección. La vamos a llamar Links.

Creamos nuestra primera colección, la llamamos links
Creamos nuestra primera colección, la llamamos links

Dentro de una colección van haber distintos documentos que serán los enlaces (las URLs) que un user va a ir guardando para poder leer más tarde. Cada documento, cada enlace, va a tener una serie de propiedades:

  • ID del documento (cada documento debe tener un ID único)
  • URL del enlace que será de tipo String
  • Título del artículo de tipo String
  • Favorito de tipo Bool (podremos marcar enlaces como favoritos)
  • Completado de tipo Bool (podremos marcar enlaces como leídos)

Guardar datos en base de datos Cloud Firestore

Vamos a crear un ejemplo de prueba, para poder hacerlo vamos a pulsar el Button de Firebase de ID automático, y le damos a Guardar

Generamos nuestro primer ID de documento para crear nuestros primeros datos en la base de datos
Generamos nuestro primer ID de documento para crear nuestros primeros datos en la base de datos

Una vez le hemos dado a guardar nos aparece la siguiente imagen (a ti te saldrá con otro ID en el documento):

Ahora agregamos campos a nuestro nuevo documento
Ahora agregamos campos a nuestro nuevo documento

Le damos a Agregar Campo y añadimos las propiedades que hemos listado antes con unos valores por defecto que te dejo a continuación:

Y el resultado debería ser el siguiente:

Campos agregados con una clave y un valor
Campos agregados con una clave y un valor

Ya tenemos nuestro primer Link guardado en nuestra base de datos Cloud Firestore. Lo hemos hecho manual, pero los siguientes links los guardaremos desde dentro de nuestra app.
Ahora lo que vamos hacer es configurar nuestra app para que pueda acceder a la información de nuestra base de datos y hacer una query para recibir la información que acabamos de guardar, es decir, deberíamos recibir la información de un enlace.

Integrar Firebase Cloud Firestore en Xcode

Lo primero de todo que vamos hacer es irnos a nuestro proyecto -> target -> general y allí vamos a la sección Frameworks, Libraries, and Embedded Content y añadimos:

  • FirebaseFirestore
  • FirebaseFirestoreSwift-Beta
Integramos los frameworks de iOS necesarios en Xcode para conectarnos con nuestra base de datos Cloud Firestore en Firebase
Integramos los frameworks de iOS necesarios en Xcode para conectarnos con nuestra base de datos Cloud Firestore en Firebase

Una vez los hemos añadido, nos vamos a nuestro listado de ficheros y vamos a crear una nueva carpeta llamada Links, vamos a organizar nuestro código ya que crearemos un nuevo datasource, repository y viewmodel para añadir el código relacionado con obtener y crear nuestros enlaces en la base de datos.
Así no tendremos nuestro código mezclado con la parte de autenticación de los otros posts. Quedaría algo parecido a lo siguiente:

Organizamos nuestro código añadiendo una nueva sección llamada Links
Organizamos nuestro código añadiendo una nueva sección llamada Links

Una vez tenemos creadas estas clases (que están vacías), vamos a ir rellenándolas poco a poco con nuestro código. La primera funcionalidad que vamos hacer es recuperar los links que hay en nuestra base de datos.

Lo primero de todo que haremos será irnos al LinkDatasource, e importaremos los siguiente frameworks:

import FirebaseFirestore
import FirebaseFirestoreSwift	
En nuestro LinkDatasource, importamos estos Frameworks

A continuación crearemos una struct que será un modelo. El modelo lo vamos a llamar LinkModel y será nuestro modelo de dominio. Será una struct con las mismas propiedades que nuestro documento que hemos creado hace un momento en nuestra base de datos de Firebase:

struct LinkModel: Decodable, Identifiable {
    @DocumentID var id: String?
    let url: String
    let title: String
    let isFavorited: Bool
    let isCompleted: Bool
}
Creamos un nuevo modelo. Creamos una struct llamada LinkModel

Algo curioso es el @DocumentId, al añadir este property wrapper lo que estamos haciendo es decirle a nuestro código que transforme directamente el ID del documento de Firebase a nuestra propiedad id. Vamos a crear el método para obtener los links de nuestra base de datos y transformar la información de Firebase a nuestro nuevo modelo de dominio LinkModel.


final class LinkDatasource {
    private let database = Firestore.firestore()
    private let collection = "links"
    
    func getAllLinks(completionBlock: @escaping (Result<[LinkModel], Error>) -> Void) {
        database.collection(collection)
            .addSnapshotListener { query, error in
                if let error = error {
                    print("Error retrieving all links \(error.localizedDescription)")
                    completionBlock(.failure(error))
                    return
                }
                guard let documents = query?.documents.compactMap({ $0 }) else {
                    completionBlock(.success([]))
                    return
                }
                let links = documents.map { try? $0.data(as: LinkModel.self) }
                                     .compactMap { $0 }
                completionBlock(.success(links))
            }
    }
}
Creamos nuestra clase LinkDatasource con un método para extraer todos los links de nuestra base de datos

Lo siguiente que vamos hacer es crear el método en LinkRepository que llamará a nuestro método del LinkDatasource que acabamos de crear:

import Foundation

final class LinkRepository {
    private let linkDatasource: LinkDatasource
    
    init(linkDatasource: LinkDatasource = LinkDatasource()) {
        self.linkDatasource = linkDatasource
    }
    
    func getAllLinks(completionBlock: @escaping (Result<[LinkModel], Error>) -> Void) {
        self.linkDatasource.getAllLinks(completionBlock: completionBlock)
    }
}
Creamos un método en el repositorio que llama al método que acabamos de crear en nuestro LinkDatasource

y finalmente creamos nuestro LinkViewModel para que llame a nuestro getAllLinks() de LinkRepository:

import Foundation

final class LinkViewModel: ObservableObject {
    private let linkRepository: LinkRepository
    
    init(linkRepository: LinkRepository = LinkRepository()) {
        self.linkRepository = linkRepository
    }
    
    func getAllLinks() {
        linkRepository.getAllLinks { result in
            switch result {
            case .success(let linkModels):
                // TODO
            case .failure(let error):
                // TODO
            }
        }
    }
}
Creamos nuestro LinkViewModel y llamamos al método de LinkRepository

Al llamar a nuestro método del repositorio getAllLinks() necesitamos avisar a la vista en 2 escenarios posibles:

  • Success, necesitamos almacenar los links en una propiedad @Published para que la vista que esté mostrando los links se refresque con la nueva información.
  • Failure, necesitamos mostrar un error por pantalla para avisar al user de lo que está ocurriendo.

Pues vamos a crear dos propiedades nuevas, y vamos a asignar los valores dentro del case que le corresponde. Nuestro LinkViewModel quedaría de la siguiente manera:

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
            }
        }
    }
}
Al obtener todos los links los asignamos a una propiedad @Published

Ahora lo que haremos será ir a la vista HomeView y crear una propiedad nueva para que cree una instancia de LinkViewModel, ¿por qué crearemos aquí la instancia? Uno de los requisitos para poder guardar enlaces es que el user tenga una sesión creada, y la vista de HomeView solo la mostramos a los user logueados.

import SwiftUI

struct HomeView: View {
    @ObservedObject var authenticationViewModel: AuthenticationViewModel
    @StateObject var linkViewModel: LinkViewModel = LinkViewModel()
    
    var body: some View {
        NavigationView {
            TabView {
                VStack {
                    Text("Bienvenido \(authenticationViewModel.user?.email ?? "No user")")
                        .padding(.top, 32)
                    Spacer()
                    LinkView(linkViewModel: linkViewModel)
                }
                .tabItem {
                    Label("Home", systemImage: "house.fill")
                }
                ProfileView(authenticationViewModel: authenticationViewModel)
                    .tabItem {
                        Label("Profile", systemImage: "person.fill")
                    }
            }
            .navigationBarTitleDisplayMode(.inline)
            .navigationTitle("Home")
            .toolbar {
                Button("Logout") {
                    authenticationViewModel.logout()
                }
            }
        }
    }
}
Creamos una nueva vista llamada LinkView y le pasamos el LinkViewModel instanciado

En HomeView hemos creado:

  • Una nueva propiedad @StateObject que instancia LinkViewModel
  • Debajo del Spacer hemos añadido LinkView y le hemos pasado una instancia de LinkViewModel

Si intentamos compilar fallará, ya que la vista LinkView no la tenemos creada, es el siguiente paso. Así todo el código que creemos relacionado con los Links estará en esta vista:

import SwiftUI

struct LinkView: View {
    @ObservedObject var linkViewModel: LinkViewModel
    
    var body: some View {
        Text("Hello, World!")
    }
}

struct LinkView_Previews: PreviewProvider {
    static var previews: some View {
        LinkView(linkViewModel: LinkViewModel())
    }
}
Creamos la vista LinkView en SwiftUI

Lo primero de todo que vamos hacer es crear la propiedad linkViewModel que nos servirá para obtener el listado de links almacenados en la base de datos. Y más tarde nos servirá para crear estos enlaces.

Ahora vamos a crear la celda que contendrá la información de cada enlace:

import SwiftUI

struct LinkView: View {
    @ObservedObject var linkViewModel: LinkViewModel
    
    var body: some View {
        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()
        }
    }
}
En LinkView añadimos un List para mostrar todos los enlaces (links guardados en nuestra base de datos de Firebase)

Si compilamos nuestra app deberíamos ver lo siguiente:

Resultado al compilar nuestra app. Obtenemos los enalces (los links) almacenados en nuestra base de datos Cloud Firestore en Firebase
Resultado al compilar nuestra app. Obtenemos los enalces (los links) almacenados en nuestra base de datos Cloud Firestore en Firebase

Lo curioso de esto es que si nos vamos a Firebase y cambiamos cualquier valor en nuestra base de datos, automáticamente se ve reflejado en nuestra app sin hacer absolutamente nada .
Para comprobarlo vamos a cambiar el valor de isCompleted a true y también vamos a añadir emojis a nuestro título.

Probamos nuestro código y vemos que los cambios en nuestra base de datos se ven reflejados automáticamente en nuestra app
Probamos nuestro código y vemos que los cambios en nuestra base de datos se ven reflejados automáticamente en nuestra app

Una vez podemos obtener todos los enlaces de nuestra base de datos y mostrarlos dentro de nuestra app, vamos a dejar que el user pueda pegar un enlace (una URL) en un TextEditor, este nuevo TextEditor lo añadiremos encima de nuestro listado de enlaces, en la vista LinkView. Pero lo dejaremos para el próximo post.

Conclusión

Hoy hemos aprendido a cómo crear nuestra primera base de datos Cloud Firestore en Firebase. Hemos añadido datos de forma manual desde la consola de Firebase y finalmente hemos configurado Xcode y nuestra app para poder obtener la información almacenada en la base de datos.

Si quieres seguir aprendiendo sobre SwiftUI, Swift, Xcode, o cualquier tema relacionado con el ecosistema Apple