Ir al contenido
charmotion

Mascota animada de onboarding en SwiftUI

Por el equipo de Charmotion · Actualizado el

Ilustración de portada de la guía de iOS y Swift: mascota animada de onboarding en SwiftUI, con personajes de demostración de Charmotion

Para animar una mascota de marca en el onboarding de SwiftUI, coloca un TabView con .tabViewStyle(.page) debajo de una única vista de la mascota que vive fuera de las páginas. Cuando cambie la página seleccionada, actualiza la propiedad de estado de ánimo de la mascota y dispara un trigger de reacción puntual en los momentos clave. Tener una sola vista mantiene la animación continua en lugar de recargarla en cada deslizamiento.

Puntos clave

  • Mantén una sola vista de la mascota fuera del TabView; una mascota dentro de cada página se recarga y se reinicia en cada deslizamiento.
  • Asigna a cada página un estado de ánimo en loop y usa los triggers solo para momentos concretos (bienvenida, permiso concedido, final).
  • PageTabViewStyle está disponible desde iOS 14, el mismo mínimo que el runtime de Rive.
  • Con Reducir movimiento activado, muestra una pose fija por página y omite las reacciones puntuales.

¿Cómo creo un onboarding paginado con TabView(.page)?

Un TabView vinculado a una selección y con el estilo .page te da páginas deslizables con indicador de página, disponible desde iOS 14. Modela las páginas como un enum para que cada una tenga su propio texto y su estado de la mascota:

enum OnboardingPage: Int, CaseIterable, Identifiable {
    case welcome, feature, permission, paywall
    var id: Int { rawValue }

    var title: String {
        switch self {
        case .welcome: return "Meet Kiko"
        case .feature: return "Snap a receipt, Kiko sorts it"
        case .permission: return "Allow notifications?"
        case .paywall: return "Go Pro"
        }
    }
}

struct OnboardingPages: View {
    @Binding var page: OnboardingPage

    var body: some View {
        TabView(selection: $page) {
            ForEach(OnboardingPage.allCases) { item in
                Text(item.title)
                    .font(.title2.bold())
                    .multilineTextAlignment(.center)
                    .padding(.horizontal, 32)
                    .tag(item)
            }
        }
        .tabViewStyle(.page)
        .indexViewStyle(.page(backgroundDisplayMode: .always))
    }
}

El .tag(item) debe tener el mismo tipo que el binding de la selección; de lo contrario, deslizar no actualizará page.

¿Cómo mantengo viva una sola vista de la mascota entre páginas?

Coloca la mascota encima del TabView dentro de un VStack, no dentro del ForEach. Las páginas se crean y se destruyen al deslizar; una mascota dentro de ellas recargaría su archivo y volvería a empezar desde el estado de entrada cada vez. Fuera de ellas, sigue reproduciéndose y simplemente cambia de estado.

El controlador de abajo usa el nuevo runtime de Rive para Apple y conserva la instancia del view model vinculada, algo necesario para cambiar valores más adelante:

import SwiftUI
import RiveRuntime

@MainActor
final class MascotController: ObservableObject {
    @Published private(set) var rive: Rive?
    private var instance: ViewModelInstance?

    func load(initialMood: Int) async {
        guard rive == nil else { return }
        do {
            let worker = try await Worker()
            let file = try await File(source: .local("mascot", Bundle.main), worker: worker)
            let artboard = try await file.createArtboard()
            let instance = try await file.createViewModelInstance(
                .viewModelDefault(from: .artboardDefault(artboard))
            )
            instance.setValue(of: NumberProperty(path: "mood"), to: Float(initialMood))
            let machine = try await artboard.createStateMachine(
                "CharacterViewModelMachine",
                binding: instance
            )
            self.instance = instance
            rive = try await Rive(file: file, artboard: artboard, stateMachine: machine)
        } catch {
            print("Mascot failed to load: \(error)")
        }
    }

    func setMood(_ mood: Int) {
        instance?.setValue(of: NumberProperty(path: "mood"), to: Float(mood))
    }

    func fire(_ trigger: String) {
        instance?.fire(trigger: TriggerProperty(path: trigger))
    }
}

Asignar el primer estado de ánimo antes de que aparezca la vista evita un fotograma con el estado predeterminado del archivo, un punto que la guía de migración de Rive menciona explícitamente.

¿Cómo asigno a cada página del onboarding un estado de ánimo de la mascota?

Define primero la correspondencia en una tabla y después codifícala en el enum. Los loops describen lo que hace el usuario en la página; los triggers marcan momentos.

PáginaLoop (mood)Trigger
Bienvenidawave (2)enter al aparecer por primera vez
Funciónthinking (3)ninguno
Permisolistening (6)yes o no después del aviso del sistema
Paywallhappy (1)ninguno (mantenlo tranquilo)
Finalidle (0)success
extension OnboardingPage {
    var mood: Int {
        switch self {
        case .welcome: return 2
        case .feature: return 3
        case .permission: return 6
        case .paywall: return 1
        }
    }
}

struct OnboardingView: View {
    @StateObject private var mascot = MascotController()
    @State private var page: OnboardingPage = .welcome

    var body: some View {
        VStack(spacing: 0) {
            RiveUIViewRepresentable(rive: mascot.rive)
                .frame(height: 260)
                .accessibilityHidden(true)
            OnboardingPages(page: $page)
        }
        .task { await mascot.load(initialMood: page.mood) }
        .onChange(of: page) { _, newPage in
            mascot.setMood(newPage.mood)
        }
    }
}

El closure de onChange(of:) con dos parámetros es la forma de iOS 17; en iOS 14 a 16 usa la versión de un solo parámetro. Como la máquina de estados (state machine) gestiona las transiciones, deslizar rápido hacia adelante y hacia atrás es seguro: gana el último valor.

Tres pantallas de onboarding en las que una mascota saluda, escucha mientras el usuario escribe y luego celebra
Onboarding con una sola mascota: cada paso define un estado de ánimo o dispara un trigger.

¿Cómo disparo una reacción puntual cuando aparece una página?

Dispara los triggers desde eventos, no desde el layout. Buenos puntos de enganche son el final de la carga (enter), el resultado de un aviso de permiso y el botón final:

Button("Allow notifications") {
    Task {
        let center = UNUserNotificationCenter.current()
        let granted = (try? await center.requestAuthorization(options: [.alert, .badge])) ?? false
        mascot.fire(granted ? "yes" : "no")
        withAnimation { page = .paywall }
    }
}

(Esto requiere import UserNotifications). Evita disparar un trigger en el onAppear de una página: TabView puede crear las páginas vecinas antes de tiempo, así que onAppear puede ejecutarse antes de que el usuario vea realmente la página. Reaccionar a la selección de page, como en el onChange de arriba, es fiable.

Que las reacciones puntuales sean breves y poco frecuentes. Una mascota que celebra cada deslizamiento deja de significar algo en la tercera página; reserva success para el final de verdad.

Seis reacciones puntuales: éxito, error, sí, no, entrada y salida
Las reacciones puntuales se disparan con triggers y vuelven al estado de ánimo actual al terminar.

¿Cómo respeto Reducir movimiento en el onboarding?

Lee @Environment(\.accessibilityReduceMotion). Cuando sea true, reemplaza la vista en vivo por una imagen fija por página y omite los triggers. Un fundido corto entre imágenes fijas es un sustituto habitual del movimiento cuando se usa movimiento reducido.

struct OnboardingMascot: View {
    @Environment(\.accessibilityReduceMotion) private var reduceMotion
    @ObservedObject var mascot: MascotController
    let page: OnboardingPage

    var body: some View {
        if reduceMotion {
            Image("mascot-mood-\(page.mood)")
                .resizable()
                .scaledToFit()
                .id(page)
                .transition(.opacity)
        } else {
            RiveUIViewRepresentable(rive: mascot.rive)
        }
    }
}

Las imágenes fijas pueden ser fotogramas exportados de los videos del mismo personaje, para que la versión reducida coincida con la animada. Cualquier información que transmita la mascota, como "permiso concedido", también debe aparecer en texto. Encontrarás más patrones en Reducir movimiento en SwiftUI y UIKit.

¿El onboarding debe empezar con un splash screen o ir directo al contenido?

Las Human Interface Guidelines de Apple son directas: la pantalla de inicio debe ser "casi idéntica a la primera pantalla de tu app" y "no es una oportunidad de branding". Si quieres un momento de marca, la HIG sugiere mostrarlo "al principio de tu flujo de onboarding".

Eso convierte la página de bienvenida en el lugar natural para la entrada de la mascota: la pantalla de inicio del sistema muestra un fondo liso, la primera página del onboarding aparece con el mismo fondo, y la mascota reproduce enter y pasa a su loop de saludo mientras el usuario lee. No hace falta un splash screen aparte. El patrón de código para una intro independiente está en splash screen animado en SwiftUI.

Si todavía no tienes un personaje animado, Charmotion genera uno a partir de una mascota o un logo subido: 17 animaciones, normalmente en pocos minutos. El .riv con el contrato de mood y triggers usado arriba, además de archivos SwiftUI para ambos runtimes de Rive, vienen con todos los planes de pago (desde $20/mes); el plan gratis no tiene sets de personaje. Consulta los precios.

Agrega una mascota animada a un onboarding de SwiftUI

  1. Modela las páginas. Crea un enum de páginas con un título y un número de estado de ánimo para cada una.
  2. Crea el paginador. Usa TabView(selection:) con .tabViewStyle(.page) y etiqueta cada página con su caso del enum.
  3. Agrega una mascota. Coloca RiveUIViewRepresentable(rive:) encima del TabView en un VStack, respaldado por un controlador que conserve la instancia del view model.
  4. Sincroniza el estado de ánimo. En .onChange(of: page), asigna a NumberProperty(path: "mood") el estado de ánimo de la página.
  5. Dispara los momentos. Dispara los triggers enter, yes/no y success desde eventos reales, como el resultado de un permiso.
  6. Gestiona Reducir movimiento (Reduce Motion). Cambia la vista en vivo por una imagen fija por página cuando accessibilityReduceMotion sea true.

Preguntas frecuentes

¿Por qué mi mascota se reinicia en cada página del onboarding?

Está dentro de las páginas del TabView, así que SwiftUI crea una vista nueva, y un archivo de Rive nuevo, para cada página. Mueve la mascota fuera del TabView y guarda su controlador en un @StateObject de la vista padre. Así, al cambiar de página solo cambia el valor del estado de ánimo.

¿Puedo usar Lottie en lugar de Rive para una mascota de onboarding?

Sí, si cada página solo reproduce su propio clip. Lottie no tiene una máquina de estados con Data Binding en LottieView, así que tú cambias entre archivos o segmentos de marcadores, y las transiciones entre ellos son cortes a menos que las diseñes. Rive gestiona la mezcla entre estados de ánimo dentro de un solo archivo.

¿La mascota debe ser visible para VoiceOver?

Normalmente no. En el onboarding el texto transmite el mensaje, así que marca la mascota con accessibilityHidden(true). Si el personaje comunica algo que no está en el texto, dale una etiqueta de accesibilidad que describa el estado, como "Configuración completa", en lugar de la animación en sí.

Respuestas rápidas de esta guía

Fuentes

Guías relacionadas