Cómo hacer un buen onboarding

¡Hola 🙋‍♂️!

Hoy vamos a dar los primeros pasos de la serie “De cero al App Store”, creando una pantalla de Onboarding que muestre a nuestros usuarios las principales características y beneficios de Silence mientras ofrecemos una muy buena experiencia de usuario.

¡Al lío!

Lo básico

La pantalla de onboarding es lo primero que el usuario va a ver de tu app, y las primeras impresiones cuentan, y mucho.

Imagina que vas andando por la calle y, de repente, te topas de frente con el restaurante nuevo del que te habló tu amigo y decides entrar a probarlo. Abres la puerta ilusionado y… un olor asqueroso a fritanga inunda tu nariz. El suelo está sucio y pegajoso, la música está tan alta que la gente no puede ni hablar, y un camarero te pega un grito desde la otra punta del local para que entres rápido y no molestes en la puerta. Tu amigo te dijo que la comida era buena, pero… ¿te sentarías a cenar?

Ahora imagina que al abrir la puerta te encuentras un local limpio y ordenado, con un olor suave, una música agradable y un camarero muy educado te está esperando en la puerta para acompañarte hasta tu mesa. La comida es la misma pero, como decía antes, las primeras impresiones cuentan, y mucho.

¿Qué impresión quieres causar al usuario cuando abra por primera vez tu app? No subestimes la importancia de una pantalla de bienvenida. Es fácil liarla y causar rechazo si no sigues los consejos básicos que vamos a ver hoy.

¿Qué tiene que tener un buen onboarding?

  • Debe mostrar solo lo esencial. Mantén el mensaje directo y simple, y céntrate en las funcionalidades clave de la app.
  • Tiene que ser rápido y fluido, sin loaders ni interrupciones que puedan causar frustración a las primeras de cambio.
  • El aspecto visual y el tono del mensaje son muy importantes. Utiliza un mensaje positivo e intenta crear una conexión emocional con el usuario.
  • Proporciona siempre una forma clara y accesible de cerrarlo. No secuestres al usuario en esta pantalla.
  • Si tienes una pantalla de ajustes, es un buen lugar para poner un acceso a volverlo a ver.
  • Preséntalo una única vez. Utiliza algún almacenamiento, como UserDefaults, para evitar la repetición innecesaria.
  • Si tu app es muy compleja, haz el onboarding interactivo para que el usuario retenga mejor la información.

Apple hace aplicaciones que son utilizadas por millones de personas, por lo que es una buena idea fijarnos en cómo gestionan ellos el onboarding para tomarlo como referencia. Tienen un enfoque elegante y muy simple, usualmente destacando tres características clave de la app acompañadas por unas descripciones breves y un botón de acción claramente visible:

Onboarding app Notas de Apple

Onboarding app Traducir de Apple

Desarrollando la interfaz

Si todavía no lo has hecho, crea un proyecto de iOS nuevo, crea una vista de SwiftUI y llámala OnboardingView.

Si te fijas, Apple muestra su onboarding en una modal. Vamos a hacer lo mismo, por lo que cambia la implementación de ContentView para que, de momento, muestre un botón que desencadene la visualización de OnboardingView:

import SwiftUI

struct ContentView: View {
    @State private var showOnboarding = false

    var body: some View {
        Button("Mostrar onboarding") {
            showOnboarding = true
        }
        .sheet(isPresented: $showOnboarding) {
            OnboardingView()
        }
    }
}

#Preview {
    ContentView()
}

Este podría ser el código de nuestro OnboardingView:

import SwiftUI

struct OnboardingView: View {
    var body: some View {
        VStack(spacing: 0) {
            Text("Silence")
                .font(.largeTitle)
                .fontWeight(.black)
                .padding(.vertical, 48)
            
            VStack(alignment: .leading, spacing: 32) {
                HStack {
                    Image(systemName: "figure.mind.and.body")
                        .font(.title)
                        .foregroundStyle(.indigo)
                        .frame(width: 50)
                    
                    VStack(alignment: .leading) {
                        Text("Momentos de silencio")
                            .font(.headline)
                        
                        Text("Reduce el estrés y mejora tu bienestar emocional dedicando tiempo a respirar y estar en silencio.")
                            .foregroundStyle(.secondary)
                    }
                }
                
                HStack {
                    Image(systemName: "chart.line.uptrend.xyaxis")
                        .font(.title)
                        .foregroundStyle(.indigo)
                        .frame(width: 50)
                    
                    VStack(alignment: .leading) {
                        Text("Estadísticas de progreso")
                            .font(.headline)
                        
                        Text("Monitorea tus avances y observa cómo mejora tu concentración y serenidad con el tiempo.")
                            .foregroundStyle(.secondary)
                    }
                }
                
                HStack {
                    Image(systemName: "lock")
                        .font(.title)
                        .foregroundStyle(.indigo)
                        .frame(width: 50)
                    
                    VStack(alignment: .leading) {
                        Text("Privacidad total")
                            .font(.headline)
                        
                        Text("No almacenamos tus datos. Todo se guarda directamente en Salud de Apple, asegurando tu completa privacidad.")
                            .foregroundStyle(.secondary)
                    }
                }
            }
            .padding(.top)
            
            Spacer()
            
            Button(action: {

            }, label: {
                HStack {
                    Spacer()
                    Text("Continuar")
                        .foregroundStyle(.background)
                    Spacer()
                }
                .padding()
                .background(.indigo)
                .clipShape(.buttonBorder)
            })
        }
        .padding()
    }
}

#Preview {
    OnboardingView()
}

Este es el resultado de nuestra vista:

Onboarding en modo claro

Onboarding en modo oscuro

Repasa el listado de buenas prácticas a la hora de crear un onboarding que hemos visto antes y verás cómo estamos cumpliendo con todo ello: Diseño simple, mensaje directo, conexión emocional con el usuario, mensaje positivo, tres características, botón para cerrarlo…

He elegido el color Indigo porque transmite calma y tranquilidad, y utilizando los colores del sistema tenemos gratis el modo noche bien configurado.

Refactorizando el código

Si bien el código que te he mostrado antes es funcional y nos da el resultado que queríamos, habrás notado que tiene bastante margen de mejora: el código que crea el HStack de cada característica de la app es casi idéntico, tenemos los textos hardcodeados, el botón de “Continuar” no hace nada…

Vamos a empezar refactorizando la creación de cada HStack. Los tres bloques son iguales, constando de una imagen a la izquierda, un título y una descripción, presentados de la misma manera. Podemos extraer esa lógica a una función y parametrizarla para que reciba estos tres datos:

func makeFeatureView(
    imageSystemName: String,
    title: String,
    description: String
) -> some View {
    HStack {
        Image(systemName: imageSystemName)
            .font(.title)
            .foregroundStyle(.indigo)
            .frame(width: 50)
        
        VStack(alignment: .leading) {
            Text(title)
                .font(.headline)
            
            Text(description)
                .foregroundStyle(.secondary)
        }
    }
}

Y, en el body, utilizamos la nueva función para crear cada bloque de característica:

VStack(alignment: .leading, spacing: 32) {
    makeFeatureView(
        imageSystemName: "figure.mind.and.body",
        title: "Momentos de silencio",
        description: "Reduce el estrés y mejora tu bienestar emocional dedicando tiempo a respirar y estar en silencio."
    )
    makeFeatureView(
        imageSystemName: "chart.line.uptrend.xyaxis",
        title: "Estadísticas de progreso",
        description: "Monitorea tus avances y observa cómo mejora tu concentración y serenidad con el tiempo."
    )
    makeFeatureView(
        imageSystemName: "lock",
        title: "Privacidad total",
        description: "No almacenamos tus datos. Todo se guarda directamente en Salud de Apple, asegurando tu completa privacidad."
    )
}
.padding(.top)

Cerrando la vista

Ahora vamos a permitir que el usuario pueda cerrar la pantalla al pulsar el botón de “Continuar”.

Para ello, introducimos el valor de entorno dismiss, que nos permitirá cerrar la modal en la acción del botón:

@Environment(\.dismiss) var dismiss

Y en el botón:

Button(action: {
    dismiss()
}, label: {
    HStack {
        Spacer()
        Text("Continuar")
            .foregroundStyle(.background)
        Spacer()
    }
    .padding()
    .background(.indigo)
    .clipShape(.buttonBorder)
})

Haciendo la app multiidioma localizando los textos

Por último, vamos a ver cómo tener los textos centralizados en un único lugar, y cómo hacer que nuestra app soporte varios idiomas.

Añade un nuevo fichero de tipo String Catalog a la carpeta del proyecto y llámalo “Localizable”:

Verás que, por defecto, aparece el inglés. Pulsa el + situado en la esquina inferior izquierda y añade un nuevo idioma. En este caso voy a añadir el español:

Con esto ya tenemos configurada la app para soportar español e inglés. Ahora tenemos que ir añadiendo los diferentes strings en formato clave-valor para ambos idiomas. A mí me gusta seguir una nomenclatura para las claves del estilo “pantalla.identificador” (por ejemplo, para el texto del botón de continuar del onboarding podría ser “onboarding.continue”), pero aquí ya es a gusto de cada uno.

Ahora aparece que tenemos un 0% de español traducido, así que pulsamos sobre “Spanish” y rellenamos las traducciones:

Ahora lanzamos la app y vemos que el botón de “Mostrar onboarding” está perfectamente traducido, pulsamos en él para ver la pantalla y… desastre: el botón de “Continuar” está bien pero en los textos de los títulos y de las descripciones se ven las claves que hemos puesto en lugar de los valores, tanto en español como en inglés:

¿Por qué nos sucede esto? Si nos fijamos en el constructor del Text que está poniendo el texto en el botón, veremos que estamos utilizando LocalizedStringKey:

Ahí está la clave. Mientras que los botones están esperando recibir una clave del fichero de localizables, la función que hemos creado antes para construir los HStack de las características espera recibir un String, por lo que no entiende que tiene que ir a buscar al fichero de traducciones. Cambiamos el tipo de los parámetros del título y la descripción en la función de String a LocalizedStringKey así:

func makeFeatureView(
    imageSystemName: String,
    title: LocalizedStringKey,
    description: LocalizedStringKey
) -> some View {
    HStack {
        Image(systemName: imageSystemName)
            .font(.title)
            .foregroundStyle(.indigo)
            .frame(width: 50)
        
        VStack(alignment: .leading) {
            Text(title)
                .font(.headline)
            
            Text(description)
                .foregroundStyle(.secondary)
        }
    }
}

Y lanzamos la app. Ahora todo se ve perfecto tanto en español como en inglés:

Conclusión

Hemos visto cómo crear una buena pantalla de bienvenida y como aplicar algunas buenas prácticas para mejorar nuestro código, haciéndolo más mantenible.

En el próximo post veremos cómo hacer que nuestro onboarding se muestre una única vez al arrancar la app, e introduciremos conceptos de arquitectura e inyección de dependencias con los que sentaremos las bases de nuestro proyecto.

¡Gracias por llegar hasta aquí y nos vemos pronto 👋!

Anterior
Anterior

Almacenamiento local con UserDefaults en SwiftUI

Siguiente
Siguiente

De cero al App Store: desarrollando Silence