Un clon de Instagram usando SwiftUI y GraphQL – Login

En nuestro post anterior sobre cómo crear una aplicación clon de Instagram, aprendiste a configurar todo para tener SwiftUI funcionando en XCode 11, y creaste una vista de Sing Up completamente funcional con GraphQL.

Hoy vamos a aprender a crear una vista de inicio de sesión y hacer que el usuario cierre la sesión.

Necesitaremos el proyecto del post anterior, así que si no lo has seguido, te sugiero que lo hagas.

Abróchense los cinturones y ¡vamos allá!

Para un mejor aprendizaje, descarga el proyecto iOS Instagram Clone con el código fuente.

¿Quieres un comienzo rápido?

¡Clona esta App desde nuestro Hub y empieza a usarla sin complicaciones!

Creación de la vista de inicio de sesión

Nuestra vista Login será bastante similar a nuestra vista SignUp, incluso más simple en realidad.

En la mutación logInUser sólo necesitamos dos parámetros: nombre de usuario y contraseña:

Las consultas y mutaciones dependerán de la versión de Parse que hayamos elegido:

Parse 3.7.2:

mutación logInUser($nombredeusuario: ¡Cadena!, $contraseña: ¡Cadena!){
  usuarios{
    logIn(nombredeusuario: $nombredeusuario, contraseña: $contraseña){
      sessionToken
    }
  }
}

Parse 3.8:

mutación logInUser($nombredeusuario: ¡cadena!, $contraseña: ¡cadena!){
    logIn(nombredeusuario: $nombredeusuario, contraseña: $contraseña){
      sessionToken
    }
}

Parse 3.9:

mutación logInUser($nombredeusuario: ¡cadena!, $contraseña: ¡cadena!){
    logIn(nombredeusuario: $nombredeusuario, contraseña: $contraseña){
      sessionToken
    }
}

por lo que sólo tendremos que preguntar a nuestros usuarios.

Empecemos añadiendo una nueva vista SwiftUI yendo a Archivo > Nuevo > Archivo y seleccionando nuestra vista SwiftUI.

screen-shot-2019-08-26-at-11-08-58

Llamemos a esta vista LogInView.swift y abrámosla en nuestro proyecto:

screen-shot-2019-08-28-at-10-54-07

Y como ya aprendiste, crea tu VStack con los controles que necesitaremos:

  • TextField para nombre de usuario
  • SecureField para la contraseña
  • Button para realizar la acción

Para mantener el diseño consistente, he movido los colores que usaremos al AppDelegate.swift, así que he tenido que importar SwiftUI allí también:

import SwiftUI

let lightGreyColor = Color(rojo: 239.0/255.0, verde: 243.0/255.0, azul: 244.0/255.0, opacidad: 1.0)
let azul claroColor = Color(rojo: 36.0/255.0, verde: 158.0/255.0, azul: 235.0/255.0, opacidad: 1.0)

Recuerde eliminar las líneas de color de SignUpView.swift y LogInView.swift.

Además, con el fin de mantener la coherencia de los controles, me limité a copiar y pegar de nuestra vista SignUp y eliminé el TextField de correo electrónico y cambié los TextFields para reflejar las nuevas funcionalidades. Mi código terminó así

struct LogInView: View {
    @State var nombre de usuario: String = ""
    @Estado var password: String = ""
    
    @Estado private var showingAlert = false

    var body: alguna Vista {
       VStack{
           Texto("Entrar")
               .font(.largeTitle)
               .foregroundColor(lightBlueColor)
               .fontWeight(.semibold)
               .padding(.bottom, 20)
           TextField("Nombre de usuario", text: $nombredeusuario)
               .padding()
               .background(colorgris claro)
               .cornerRadius(5.0)
               .padding(.bottom, 20)
           SecureField("Contraseña", text: $contraseña)
               .padding()
               .background(colorgris claro)
               .cornerRadius(5.0)
               .padding(.abajo, 20)
           Button(acción: {
            
           }){
               Texto("¡Iniciar sesión!")
                .font(.titular)
                .foregroundColor(.blanco)
                .padding()
                .frame(anchura: 220, altura: 60)
                .background(colorazulclaro)
                .cornerRadius(15.0)
           }
       }.padding()
    }
}

Esto ha sido sencillo. Veamos cómo queda.
Cambia tu ContentView.swift para mostrar esa vista en su lugar:

struct ContentView : View {
    var body: some View {
        EntrarVista()
    }
}

screen-shot-2019-08-28-at-11-16-13

¡Muy bonito!

¡Hagámoslo más ordenado añadiendo nuestro logo en la parte superior!
Nuestro logo consistirá en una imagen que integraremos en nuestro proyecto. Yo he utilizado ésta.

Arrastra y suelta esa imagen a tu carpeta Assets.xcassets en el proyecto:

screen-shot-2019-08-28-at-13-25-54

Debería verse así:

screen-shot-2019-08-28-at-13-27-30

A partir de ahora podemos referenciarla en nuestro código con ese nombre: logo-social.

Regístrate ahora en Back4App y empieza a crear tu aplicación clon de Instagram.

Nuestro logo consistirá en esa imagen, pero simplemente poner la imagen ahí se vería amateur. Lo haremos brillar: circular, con un tamaño fijo, algún trazo en los bordes y, por supuesto, sombra por… sombra.

El código para todo eso tiene este aspecto

Imagen("logo-social")
    .redimensionable()
    .aspectRatio(contentMode: .fit)
    .frame(anchura: 150, altura: 150)
    .clipShape(Círculo())
    .overlay(Circle().stroke(Color.blue, lineWidth: 2))
    .shadow(radio: 5)
    .padding(.bottom, 75)

Y va en la parte superior de nuestro VStack:

var body: alguna Vista {
       VStack{
           Imagen("logo-social")
                .redimensionable()
                .aspectRatio(contentMode: .fit)
                .frame(anchura: 150, altura: 150)
                .clipShape(Círculo())
                .overlay(Circle().stroke(Color.blue, lineWidth: 2))
                .shadow(radio: 5)
                .padding(.bottom, 75)
           Texto("Entrar")
               .font(.largeTitle)
               .foregroundColor(azul claro)
               .fontWeight(.semibold)
               .padding(.bottom, 20)
...

screen-shot-2019-08-28-at-13-33-41

¿No es precioso?

El sessionToken

Nuestro proceso de login devolverá una cadena sessionToken. Debemos mantener ese sessionToken seguro ya que será utilizado durante nuestras operaciones: mientras ese sessionToken sea válido, tendremos acceso a nuestra aplicación. Cuando sea borrado o invalidado, nuestras llamadas serán denegadas.

Como esto está directamente relacionado con la seguridad, necesitaremos almacenarlo de forma segura. El lugar correcto para hacerlo es el Llavero en iOS.

Lo único que tiene el Llavero es que es difícil y aburrido de usar, así que decidí usar este wrapper para gestionarlo. Hace la vida mucho más fácil y como ya estamos usando Cocoapods, tiene todo el sentido.

Vamos a editar nuestro Podfile y añadir esto a nuestros pods:

pod 'SwiftKeychainWrapper'

luego actualicemos nuestros Pods con el comando

pod install

y finalmente, reabramos nuestro proyecto xcworkspace.

Ya estamos listos para utilizar nuestro motor Keychain para…

Almacenar el sessionToken

Según la documentación de nuestro nuevo pod, la forma de guardar un valor en el llavero es

KeychainWrapper.standard.set("SomeValue", forKey: "SomeKey")

pero vamos a añadirle algo de lógica.

Para empezar, vamos a importar nuestro wrapper:

import SwiftKeychainWrapper

Lo primero es que tendremos que llamar a nuestra mutación logInUser y cuando esta responda, almacenamos el sessionToken si lo hay. Si no, tenemos que notificar al usuario con una alerta.

Ahora, si recuerdas en nuestro artículo anterior, ya tenemos una alerta codificada incluyendo su estructura. Vamos a reutilizarlo quitando el código de abajo de nuestro SingUpView.swift y pasándolo a nuestro fichero AppDelegate.swift para que todas las vistas puedan acceder a él:

struct Mensaje {
    var alertTitle: String = ""
    var alertText: String = ""
}

var miMensaje = Mensaje()

Ahora, volviendo a nuestra lógica, lo primero que tenemos que hacer es determinar si el usuario ha rellenado las cajas de texto de nombre de usuario y contraseña. Si no, no hay información para el proceso de login y debemos notificar al usuario sobre eso.

Nuestro código de acción para el botón de inicio de sesión debe comprobarlo. Añadamos este trozo de código que comprueba el tamaño de la cadena de las variables de estado vinculadas a esos campos de texto:

           Button(action: {
            // Comprueba si hay una contraseña escrita
            if (self.password.count == 0 || self.username.count == 0){
                
                // Si no, debemos mostrar la Alerta
                myMessage.alertText = "Debe proporcionar un Nombre de usuario y una Contraseña".
                myMessage.alertTitle = "Oops..."
                self.showingAlert = true
            } else {
                // Si es así, podemos continuar
                
            }
           }){
               Texto("¡Iniciar sesión!")
                .font(.headline)
                .foregroundColor(.blanco)
                .padding()
                .frame(anchura: 220, altura: 60)
                .background(colorazulclaro)
                .cornerRadius(15.0)
           }
           .alert(isPresented: $showingAlert) {
               Alert(title: Text(miMensaje.alertTitle), mensaje: Text(myMessage.alertText), dismissButton: .default(Text("OK"))
           }

y pruébalo…

screen-shot-2019-09-02-at-10-22-15

¡Muy bien!

Ahora debemos llamar a nuestra mutación GraphQL para recuperar el sessionToken y, si lo obtenemos, almacenarlo en el Llavero.
Ya has aprendido a llamar a las Mutaciones así que vamos a hacerlo, pero esta vez si obtenemos el sessionToken, lo almacenaremos:

// Realiza la mutación LogInUser, pasándole los parámetros que acabamos de obtener de nuestros TextFields
apollo.perform(mutation: LogInUserMutation(username: self.username, password: self.password)){ result in
    // Cambiemos el resultado para poder separar un resultado exitoso de un error
    switch resultado {
        // En caso de éxito
        case .success(let graphQLResult):
            // Intentamos parsear nuestro resultado
            if let sessionToken = graphQLResult.data?.users?.logIn.sessionToken {
                myMessage.alertTitle = "¡Yay!"
                myMessage.alertText = "¡El usuario ha iniciado sesión!"
                
                self.showingAlert = true

                print ("Usuario sessionToken " + sessionToken)
                

                // Escribir el sessionToken en nuestro Llavero
                let _: Bool = KeychainWrapper.standard.set(sessionToken, forKey: "Back4Gram.sessionToken")
            }
            // pero en caso de cualquier error GraphQL presentamos ese mensaje
            else if let errors = graphQLResult.errors {
                // Errores GraphQL

                myMessage.alertTitle = "¡Ups!"
                myMessage.alertText = "Tenemos un error GraphQL: " + errors.description
                self.showingAlert = true

                print(errores)
            }
        // En caso de fallo, presentamos ese mensaje
        case .failure(let error):
            // Errores de red o de formato de respuesta
            myMessage.alertTitle = "¡Uy!"
            myMessage.alertText = "Tenemos un error: " + error.localizedDescription
            self.showingAlert = true
            
            print(error)
    }
}

¡Vamos a probarlo!

screen-shot-2019-09-02-at-13-11-02

¡Genial! ¿Pero cómo sabemos que realmente ha funcionado?
Podemos ir al Parse Dashboard de nuestra App y comprobar si hay un nuevo objeto Session escrito:

screen-shot-2019-09-02-at-13-11-34

¡Como un encanto!

Y ya que estamos aquí…

¿Qué tal si añadimos un botón para cerrar la sesión? Sólo para probar, así sabremos que todo va como la seda:

Button(action: {
    // Comprueba si hay sessionToken storeStringhould sólo cerrar sesión si se ha iniciado sesión.
    if (KeychainWrapper.standard.string(forKey: "Back4Gram.sessionToken") != nil) {
        print("¡Encontrado sessionToken! Podemos cerrar la sesión.")
        
        // Realiza la mutación LogOutUser
        apollo.perform(mutación: LogOutUserMutation()){ resultado en
            // Cambiemos el resultado para poder separar uno exitoso de un error
            switch resultado {
                // En caso de éxito
                case .success(let graphQLResult):
                    // Intentamos parsear nuestro resultado
                    if let resultado = graphQLResult.data?.users?.logOut {
                        if (resultado) {
                            myMessage.alertTitle = "¡Yay!"
                            myMessage.alertText = "¡El usuario ha cerrado sesión!"
                            
                            self.showingAlert = true
                            
                            // Borrar el sessionToken almacenado
                            let _: Bool = KeychainWrapper.standard.set("", forKey: "Back4Gram.sessionToken")
                        } else {
                            myMessage.alertTitle = "¡Ups!"
                            myMessage.alertText = "La operación de cierre de sesión del usuario ha devuelto False."
                            self.showingAlert = true
                        }
                    }
                    // pero en caso de cualquier error GraphQL presentamos ese mensaje
                    else if let errors = graphQLResult.errors {
                        // Errores GraphQL

                        myMessage.alertTitle = "¡Ups!"
                        myMessage.alertText = "Tenemos un error GraphQL: " + errors.description
                        self.showingAlert = true

                        print(errores)
                    }
                // En caso de fallo, presentamos ese mensaje
                case .failure(let error):
                    // Errores de red o de formato de respuesta
                    myMessage.alertTitle = "¡Uy!"
                    myMessage.alertText = "Tenemos un error: " + error.localizedDescription
                    self.showingAlert = true
                    
                    print(error)
            }
        }
    } else {
        // Errores de red o de formato de respuesta
        myMessage.alertTitle = "¡Ups!"
        myMessage.alertText = "Parece que el usuario no ha iniciado sesión."
        self.showingAlert = true
    }
}){
    Texto("Cerrar sesión")
        .font(.titular)
        .foregroundColor(.white)
        .padding()
        .frame(anchura: 220, altura: 60)
        .background(colorazulclaro)
        .cornerRadius(15.0)
}
.alert(isPresented: $showingAlert) {
    Alert(title: Text(miMensaje.alertTitle), mensaje: Text(myMessage.alertText), dismissButton: .default(Text("OK"))
}

Una vez más, ¡vamos a probarlo!

screen-shot-2019-09-02-at-13-51-50

¡Genial!

Más adelante moveremos el botón de cerrar sesión a otro lugar, pero por ahora funciona para mostrar que nuestro flujo está funcionando.

¿Pero qué pasa con nuestro objeto Session ahora que hemos cerrado la sesión?

screen-shot-2019-09-02-at-14-04-31

Se ha ido automáticamente, como esperábamos.

Conclusión

¡Enhorabuena! Ya tienes implementadas las funcionalidades de login y logout. ¡No solo eso, sino que aprendiste como llamar diferentes mutaciones, validar resultados y almacenar valores en el Llavero! ¡Es increíble!

En el próximo capítulo, comenzaremos a trabajar con múltiples vistas y a construir nuestra vista principal.

¡Permanece atento!

Referencia

Regístrate ahora en Back4App y comienza a construir tu Instagram Clone App.

¿Qué es SwiftUI?

SwiftUI es una nueva forma de crear interfaces de usuario para aplicaciones en plataformas Apple. Permite a los desarrolladores determinar la interfaz de usuario mediante código Swift.

¿Qué es un sessionToken?

El proceso de inicio de sesión que estamos desarrollando devolverá una cadena llamada sessionToken. Debe ser segura. Si mantenemos la validez del sessionToken, tendremos acceso a la aplicación; de lo contrario, lo perderemos. Esto se relaciona con la seguridad.

¿Qué es Keychain?

Sabemos que sessionToken está relacionado con la seguridad de tu aplicación. Por lo tanto, debe almacenarse en un lugar seguro. Ese lugar seguro se llama llavero. Su uso puede ser un poco complicado y también aburrido.


Leave a reply

Your email address will not be published.