Bienvenido, soy Luis y en esta ocasión les traigo este nuevo post.
En esta serie, compartiré mi opinión sobre el desarrollo de aplicaciones multiplataforma utilizando SwiftUI y Google Firebase . Repasaremos el proceso de creación de una aplicación para tomar notas. El resultado final hacia el que se dirige esta serie está disponible en Github.
Una vez, mientras navegaba por las aguas salvajes de Internet, me topé con un artículo sobre un sistema innovador para tomar notas llamado Método Zettelkasten . En resumen, Zettelkasten es un método de gestión del conocimiento.
Las notas forman una estructura de árbol donde cada nueva nota expande la información de su padre , creando grupos de información con un tema similar. Todo esto parece algo de lo que una persona con memoria de pescado, como yo, podría beneficiarse.
Así que saqué mi título de CS, me preparé para ensuciarme las manos y emprendí una búsqueda para encontrar las últimas tecnologías para crear aplicaciones nativas. En mi certificación para Google Cloud Architect, aprendí sobre Firebase e implementé algunos proyectos de muestra con él, pero nunca nada más serio.
Firebase ofrece cuentas gratuitas con límites de niveles gratuitos bastante generosos. Donde Firebase es especialmente útil es la sincronización de datos en múltiples dispositivos. Una de las características de Firebase es Cloud Firestore, una base de datos en la nube NoSQL basada en documentos con sincronización en tiempo real.
Firestore se puede utilizar para sincronizar notas en el teléfono con notas en la computadora portátil sin implementar una sola línea de código. Cuando un dispositivo no está conectado a Internet, las actualizaciones se almacenan localmente y se sincronizan una vez que el dispositivo vuelve a estar en línea.
El estilo declarativo de SwiftUI es muy similar al de React Native. También admite la vista previa en vivo de su interfaz de usuario durante la edición, lo que permite tiempos de respuesta más rápidos.
Índice
Requerimientos de aplicación
Basta de hablar, vamos a trabajar. Esta es Zetten, la aplicación para tomar notas.
Las notas están organizadas en una estructura en forma de árbol donde una nota puede tener una nota secundaria, pero no estamos limitados a tener un solo árbol, ya que su lista de compras no se relaciona completamente con la última información de acciones, ¿o tal vez sí? Si se pregunta de dónde viene el nombre Zetten, consulte la historia de fondo.
Otro requisito es poder sincronizar notas en una computadora portátil y un teléfono; después de todo, vivimos en 2020, ¡no en la edad de piedra! Aquí es donde la sincronización en tiempo real de Firebase es útil.
Y por último, pero no menos importante, es importante poder buscar en su (con suerte) creciente número de notas breves en el sistema Zettelkasten . Para esto, necesitamos traer armas pesadas, búsqueda de texto completo con derivación e índices en el dispositivo. Afortunadamente, la última versión de GRDB tiene justo lo que necesitamos.
Entonces, ¿cómo se verá todo esto? Bueno, no tengo el mejor ojo para el diseño, pero digamos algo como esto:
Configurando Firebase
A primera vista, Firebase puede parecer difícil de configurar, sin embargo, estamos obteniendo mucho a cambio, así que tengan paciencia conmigo por un tiempo.
Las características más populares de Firebase incluyen autenticación, una base de datos de documentos NoSQL basada en la nube fácil de usar, informes de fallas, análisis, aprendizaje automático e integración de publicidad. Zetten usa autenticación, almacenamiento de documentos e informes de fallas.
Comencemos configurando la autenticación en Firebase. Primero, deberá crear un nuevo proyecto de Firebase. Puede usar el nivel gratuito (Spark) que tiene recursos más que suficientes para proyectos privados o de desarrollo. Después de aceptar los términos y condiciones, comenzamos en el panel de bienvenida.
El siguiente paso es agregar una nueva plataforma a su cuenta. Presionar el botón de iOS lo llevará a una serie de pasos para incluir su aplicación. Primero, debemos ingresar el ID del paquete de iOS , que debe ser el mismo que el ID del paquete en Xcode al crear un nuevo proyecto.
Luego, descargamos "GoogleService-Info.plist"
: este es un archivo de configuración que ayuda al SDK de Firebase a conectarse a su proyecto de Firebase. Lo necesitaremos en un segundo.
Iniciemos Xcode y seleccionemos crear un nuevo proyecto. Queremos crear un proyecto de vista única . En el siguiente paso, elegimos el nombre de la aplicación, su equipo (puede ser un equipo de desarrollo personal) y, lo más importante, queremos usar SwiftUI como motor de interfaz de usuario.
No necesitamos incluir ninguna prueba por ahora para que podamos desmarcar esas opciones.
Terminemos de crear el proyecto permitiendo que la aplicación se compile en las plataformas macOS e iOS. Esto usará Apple Catalyst al compilar para el destino Mac.
Para alimentar su curiosidad, Xcode 11.3
está cambiando el identificador de paquete para Mac con el prefijo maccatalyst, desde Xcode 11.4 Mac e iOS
comparten el mismo identificador. Ahora debería poder compilar la aplicación tanto para iOS como para Mac.
“GoogleService-Info.plist
”? Ahora es el momento de copiar el archivo a la raíz de su proyecto. En este caso, es Zetten/Zetten
porque el Zetten
directorio puede continuar con varios proyectos, por ejemplo, ZettenUI-test
.
El GoogleService-Info.plist
archivo le dice al SDK de Firebase cómo conectarse al proyecto de Firebase que acabamos de crear.
Firebase recomienda usar CocoaPods como administrador de paquetes para la plataforma Firebase en iOS. Si le faltan CocoaPods en su máquina, consulte la guía de instalación .
Una vez que estemos seguros de que CocoaPods está instalado correctamente, podemos navegar al directorio raíz de nuestro proyecto usando la terminal y ejecutar pod init
configurar los pods para nuestro proyecto.
Para agregar el SDK de Firebase a su proyecto, abra Podfile
y agregue las siguientes líneas al Zetten
destino:
pod 'Firebase/Analytics' pod 'FirebaseCore' pod 'Firebase/Auth' pod 'Firebase/Firestore' pod 'Firebase/Crashlytics' pod "FirebaseFirestoreSwift" pod 'GRDB.swift' pod 'Resolver' post_install do |installer| installer.pods_project.targets.select { |target| target.name == "GRDB.swift" }.each do |target| target.build_configurations.each do |config| config.build_settings['OTHER_SWIFT_FLAGS'] = "$(inherited) -D SQLITE_ENABLE_FTS5" end end end
Esto incluye todas las dependencias que necesitaremos más adelante, incluido un pequeño truco para compilar GRDB
con soporte FTS5
. FTS5
es un módulo de tabla virtual para SQLite
que nos permite construir índices inversos para nuestras notas y realizar búsquedas de texto completo en ellos.
El
Firebase/Analytics
pod solo se necesita para la configuración inicial para notificar a Firebase que todo está funcionando.Sin embargo, tener el
Analytics
pod como dependencia evitará que compile para macOS. Al compilar para macOS, es posible que desee eliminarlo y ejecutarlopod install
nuevamente.
Una vez finalizada la instalación del pod, el proyecto tendrá un nuevo zetten.xcworkspace
archivo. A partir de ahora, tendrá que usar el archivo xcworkspace
para abrir su proyecto Xcode
, de lo contrario, las dependencias del proyecto no se vincularán correctamente.
Cerremos Xcode
ahora y abramos el proyecto usando el nuevo archivo xcworkspace
.
Si es un usuario de terminal activo, puede utilizar
"xed"
. en el directorio raíz que primero abre el espacio de trabajo si existe; de lo contrario, vuelve a abrir un proyecto normal.
Si en algún momento se siente perdido, puede abrir la aplicación terminada en Github; contiene todos los archivos en los que trabajaremos.
Por último, Firebase Authentication
necesita permisos para acceder al llavero. Vaya a Xcode -> Zetten -> Objetivos -> Firma y capacidades. Presione + Capacidad y agregue la capacidad de Compartir llaveros.
Agregar permisos de llavero permite compartir información de la cuenta en todas las aplicaciones que pertenecen al mismo grupo de acceso. Esto se usa para iniciar sesión una vez y para iniciar sesión en todas las aplicaciones.
La alternativa es deshabilitar el uso compartido de credenciales; se pueden encontrar más detalles en la documentación.
A continuación, agregue el nombre del grupo de llaveros compartidos. El formato predeterminado {organisation}. zettenes
suficiente para nuestros propósitos. Con esto concluye la configuración de Firebase.
Vista de autenticación
Ugh, eso fue bastante trabajo, pero ahora estamos listos para usar todas las bibliotecas de la Fire Base
.
Comencemos por crear una página de inicio de sesión muy simple con correo electrónico y contraseña. SwiftUI
usa un declarativo enfoque muy similar a React
.
La representación visual de la vista se crea componiendo bloques de elementos de la interfaz de usuario. Los elementos de la interfaz de usuario son, de hecho, funciones que generan la vista respectiva, incluidas sus vistas secundarias.
Estas funciones de alto nivel se encargan de definir el diseño y las restricciones y proporcionan métodos adicionales para modificarlos aún más. Esto significa que las hermosas vistas nativas se pueden generar con muy pocas líneas de código sin repetición repetitiva.
En el lado derecho de la vista, podemos ver el Lienzo de vista previa: muestra en tiempo real cómo se verá la vista en un dispositivo real.
La vista previa se puede configurar utilizando el
PreviewProvider
al final del archivo, en nuestro caso en una estructura llamadaLoginView_Previews
.Se pueden generar múltiples vistas previas al mismo tiempo. A partir de ahora, las vistas previas solo son compatibles con los objetivos de iOS.
Cuando intente solicitar una vista previa para Mac, se encontrará con un mensaje bastante inútil donde, de hecho, el problema es el objetivo elegido (esquina superior izquierda).
import SwiftUI struct LoginView: View { @State var email: String = "" @State var password: String = "" var body: some View { VStack { Text("Login") .font(.title) TextField("Email", text: $email) .autocapitalization(.none) SecureField("Password", text: $password) .autocapitalization(.none) Button( "Login", action: {} ) Divider() Group { Text("Don't have an account?").foregroundColor(.gray) Text("Create an account") }.font(.footnote) Spacer() }.padding() } } struct LoginView_Previews: PreviewProvider { static var previews: some View { LoginView() } }
Implementación de la vista de inicio de sesión
Creemos un nuevo archivo en nuestro proyecto llamado LoginView.swift
y copie el código anterior en él. Cada vista de SwiftUI implementa el protocolo View
. Este protocolo requiere que proporciones una variable llamada cuerpo que devuelve some
.
Ver la palabra clave some
nos permite devolver varios tipos de vista, por ejemplo, una Text
vista o una Button
ver. Sin embargo, solo se puede devolver un tipo de vista de body
(más sobre esto un poco más tarde)
VStack
(pila vertical). VStack
nos permite combinar varias vistas juntas. VStack
es equivalente a VStack(content: )
, mientras que content
define lo que se va a generar dentro de la pila.
Dentro de VStack
primero incluimos un Text
función que genera una View
mostrando un texto simple. La función Text
devuelve un View
, este hecho se puede utilizar para modificar aún más la vista creada por el Text
función.
Las vistas se pueden modificar usando …, "modificadores” sí
. En este caso, .font
es un modificador de vista que le permite proporcionar argumentos de formato de fuente.
El modificador .font
nuevamente devuelve una vista modificada con cambios aplicados, esto significa que podemos realizar más cambios en la vista de Texto, por ejemplo, cambiar el color usando el .background
modificador.
TextField
es parecido a Text
pero permite al usuario escribir cierta información en el cuadro. Usaremos esto para obtener la dirección de correo electrónico del usuario.Parte de la definición de TextField
es proporcionar dónde se almacenará el resultado. En este caso al comienzo de la LoginView
hemos declarado dos variables: email
y password
.
Los @State
en la declaración y $
en TextField
dígale a Swift que el valor se actualiza dinámicamente. Lo que significa que los cambios realizados por el usuario en el TextField
será guardado en el $
variable y los cambios realizados por el programador a la variable se mostrarán al usuario utilizando TextField
.
SecureField
es lo mismo que TextField
pero el usuario no podrá ver el valor; esto es útil para los campos de contraseña.Después de completar toda la información, necesitamos una forma de realizar alguna acción. Un Button
es ideal para este caso de uso. Al definir botones, debemos proporcionar un Swift String
en lugar del elemento SwiftUI Text
.
Además, necesitamos proporcionar una acción que se realiza cuando se presiona el botón. Por ahora, usamos una función vacía , pero luego esto va a realizar el inicio de sesión.
Divider
y Spacer
. Divider
inserta una línea para separar visualmente las vistas. Spacer
genera un espacio, pero de una manera bastante divertida: Spacer
consume todo el espacio vacío y empuja el contenido hacia arriba o hacia abajo.Cuando tenemos varios espaciadores, por ejemplo Spacer1
, Text
, Spacer2
, Spacer3
, la vista calculará que el 90%
de la vista está vacía y luego dividirá el espacio vacío total por el número de espaciadores y cada espaciador obtendrá una parte proporcional del espacio, en este caso el 30%
. Lo que significa que nuestra vista de texto estará aproximadamente a 1/3
de la parte superior.
Vamos a abrir ContentView.swift
y reemplazar Text(“Hello, World!”)
con una llamada a nuestra vista de inicio de sesión LoginView()
. Esto nos mostrará nuestra vista recién creada al ejecutar nuestra aplicación simple.
Para ver esto en acción, ejecute la aplicación y verifique que puede ver la página de inicio de sesión. Si tiene algún problema, consulte la aplicación finalizada en Github.
Creemos un nuevo archivo CreateAccountView.swift
. La vista CreateAccount
es muy similar a la LoginView
anterior con una ligera modificación en el título, descripción y una acción diferente al presionar el botón crear.
Puede copiar el código proporcionado a continuación.
struct CreateAccountView: View { @State var email: String = "" @State var password: String = "" var body: some View { VStack { Text("Create an account") .font(.title) TextField("Email", text: $email) .autocapitalization(.none) SecureField("Password", text: $password) .autocapitalization(.none) Button( "Create", action: {} ) Divider() Text("An account allows to save and access notes across devices.") .font(.footnote) .foregroundColor(.gray) Spacer() }.padding() } } struct CreateAccountView_Previews: PreviewProvider { static var previews: some View { CreateAccountView() } }
Queremos poder mostrar CreateAccountView
después de presionar el Crear una cuenta botón en LoginView
. Volvamos a LoginView
y realice las siguientes modificaciones:
VStack
dentro NavigationView
.2. Antes de cerrar el
VStack
utilizar NavigationLink
con destino CreateAccountView
.struct LoginView: View { @State var email: String = "" @State var password: String = "" var body: some View { NavigationView { VStack { ... NavigationLink(destination: CreateAccountView()) { Text("Don't have an account?").foregroundColor(.gray) Text("Create an account") }.font(.footnote) Spacer() }.padding() } } }
Envolver VStack
en un NavigationView
nos permite usar el NavigationLink
interior y agrega una barra superior para la navegación. NavigationLink
crea una vista en la que se puede hacer clic, en este caso solo Text
, pero en general puede ser cualquier vista, por ejemplo Image
.
Después de presionar el contenido de a NavigationLink
, se abrirá una nueva vista deslizándose sobre la vista actual y agregando un botón de retroceso. NavigationLink
no se puede utilizar dentro de otro NavigationLink
. Sin embargo, hay formas de sortear esta restricción; las tratamos en la segunda parte.
Integrando la vista de autenticación con Firebase
Hemos creado una bonita vista de autenticación, sin embargo, por ahora, no se puede utilizar porque no está conectada a ningún código de autenticación. Integremos nuestras vistas con Firebase.
Comencemos por inicializar Firebase después del lanzamiento de la aplicación. Esto se hace AppDelegate.swift
en la application:didFinishLaunchingWithOptions
función. Primero agregue import Firebase
las importaciones de AppDelegate
.
Luego asegúrate de agregarlo FirebaseApp.configure()
al método application:didFinishLaunchingWithOptions
. El resultado debería verse así:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { // Override point for customization after application launch. FirebaseApp.configure() return true }
El método application:didFinishLaunchingWithOptions
AppDelegate
se usa a menudo para bibliotecas que requieren iniciarse después del lanzamiento de la aplicación y necesitan hacer un trabajo de configuración para funcionar correctamente.
Vamos a crear AuthenticationService.swift
. Este servicio se basa principalmente en funciones de Firebase.
import Firebase class AuthenticationService: ObservableObject { @Published var user: User? var cancellable: AuthStateDidChangeListenerHandle? init() { cancellable = Auth.auth().addStateDidChangeListener { (_, user) in if let user = user { logger.event("Got user: \(user.uid)") self.user = User( uid: user.uid, email: user.email, displayName: user.displayName ) } else { self.user = nil } } } func signUp( email: String, password: String, handler: @escaping AuthDataResultCallback ) { Auth.auth().createUser(withEmail: email, password: password, completion: handler) } func signIn( email: String, password: String, handler: @escaping AuthDataResultCallback ) { Auth.auth().signIn(withEmail: email, password: password, completion: handler) } func signOut() throws { try Auth.auth().signOut() self.user = nil } }
Servicio de autenticación
AuthenticationService
debe definirse como una clase ya que tiene que implementar el protocolo ObservableObject
. ObservableObject
permite que una vista supervise cambios en una clase.
Usaremos esto en algunas de nuestras vistas para verificar si un usuario ha iniciado sesión.
Primero definimos una variable para nuestro usuario y la marcamos con la @Published
que permite que las vistas reaccionen a los cambios. A continuación, definimos un constructor.
En el constructor, creamos un oyente para los cambios en el estado de autenticación . Siempre que cambie la autenticación en Firebase, se invocará nuestra función, lo que nos permitirá actualizar nuestra información sobre el usuario actual. Almacenamos el oyente en una variable para que podamos dejar de escuchar los cambios cuando queramos.
En muchos casos, los oyentes no funcionarán sin almacenar el identificador en una variable. Esto se debe a que Swift limpia automáticamente los recursos no utilizados.
Dado que Swift piensa que si no almacenó el controlador, no necesita escuchar los cambios. Esto puede resultar complicado y fácil de olvidar. Siempre que espere cambios pero nunca suceden, verifique primero si ha almacenado el identificador.
A continuación tenemos un trío de signIn
, signUp
,signOut
las funciones que se pide simplemente que llegue el servicio de autenticación Firebase.
El siguiente paso es registrar nuestro. Para este propósito, podemos usar la inyección de dependencia. La inyección de dependencia (DI) es una técnica en la que pones todos los recursos disponibles en una caja y cuando alguna parte del código necesita algo, puede ir a una caja y obtenerlo. AuthenticationService
.
Creemos un nuevo archivo . Esta será nuestra caja y queremos ponerla a disposición de otros usuarios. AppDelegate+Injection.swift
AuthenticationService
import Foundation import Resolver extension Resolver: ResolverRegistering { public static func registerAllServices() { register { AuthenticationService() }.scope(application) } }
Volvamos a nuestro AuthenticationView
y usemos nuestro nuevo servicio brillante.
Primero agregue la importación en la parte superior del archivo import Resolver
. Además CreateAccountView
, agregaremos dependencia en AuthenticationService
. El resultado será este:
struct CreateAccountView: View { @State var email: String = "" @State var password: String = "" @Injected var authenticationService: AuthenticationService ...
Ahora el servicio de autenticación está disponible para su uso y podemos modificar nuestro botón de creación de cuenta. Cambie la acción a:
action: { self.authenticationService.signUp(email: self.email, password: self.password, handler: {_,_ in }) }
Esto creará una nueva cuenta en Firebase. Hagamos algo similar con LoginView
. Primero, inyectemos el servicio de autenticación en nuestra vista:
@Injected var authenticationService: AuthenticationService
Y cambiemos el botón de inicio de sesión a:
action: { self.authenticationService.signIn(email: self.email, password: self.password, handler: {_,_ in }) }
Para saber que la autenticación está funcionando, cambiemos nuestro ContentView
para mostrar AuthenticationView
solo cuando el usuario no está autenticado, de lo contrario, muéstreles un cálido mensaje de bienvenida.
import SwiftUI import Resolver struct ContentView: View { @ObservedObject var authenticationService: AuthenticationService = Resolver.resolve() var body: some View { Group { if (authenticationService.user == nil) { LoginView() } else { Text("Signed in") } } } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } }
Nuevamente, declaramos una dependencia AuthenticationService
y verificamos si el usuario ha iniciado sesión o no. La sintaxis es ligeramente diferente porque queremos poder observar cambios en el usuario y no solo llamar a métodos.
Ahora podemos compilar nuestra aplicación y verificar que la autenticación funcione. No tiene la mejor experiencia pero funciona y encima de todo es completamente seguro ya que todas las partes importantes están controladas y administradas por Google.
Si tiene problemas para que la autenticación funcione, puede proporcionar un mejor controlador para las acciones en los botones que le muestran lo que ha sucedido, por ejemplo:
handler: {res,err in print(res) print(err) }
O si no puede resolver los problemas usted mismo, consulte el ejemplo terminado en Github.
También cubrimos los conceptos básicos de la composición de vistas en estilo declarativo en SwiftUI.
En la segunda parte , crearemos una vista de lista que muestra todas las notas. Conectaremos Zetten
a Cloud Firestore
y sincronizaremos notas en todos los dispositivos.
Gracias por llegar hasta el final. Espero que te haya sido de utilidad.
Añadir comentario