Pular para o conteúdo
charmotion

Splash screen animada em SwiftUI (padrão de código)

Por equipe Charmotion · Atualizado em

Ilustração de capa do guia de iOS e Swift: splash screen animada em SwiftUI (padrão de código), com personagens de demonstração do Charmotion

Não é possível animar a launch screen (tela de inicialização) do iOS em si, mas você pode fazer sua primeira view SwiftUI começar exatamente com a aparência da launch screen e tocar ali uma intro curta. Sobreponha uma view de intro ao ContentView em um ZStack, remova-a com um fade quando a animação terminar e pule-a quando o Reduzir Movimento estiver ativado.

Principais pontos

  • A launch screen é uma configuração estática, renderizada pelo sistema; ela não executa nenhum código.
  • Faça o primeiro frame da intro igual à launch screen (mesma cor de fundo, mesma posição da imagem) para que não haja salto visível.
  • Carregue o ContentView por baixo da intro para que o app esteja pronto no instante em que ela terminar.
  • Encerre a intro com um sinal vindo da animação (um stream de trigger) e um timeout como fallback, não apenas com um timer estimado.
  • Pule a intro quando o Reduzir Movimento estiver ativado e em warm launches (quando o app já estava em memória).

A launch screen do iOS pode ser animada?

Não. A launch screen é desenhada pelo sistema antes que qualquer código seu seja executado, a partir de um dicionário UILaunchScreen no Info.plist ou de um storyboard de inicialização. Nenhum dos dois consegue executar classes personalizadas ou animações.

As Human Interface Guidelines da Apple também limitam o que ela deve mostrar: "Crie uma launch screen quase idêntica à primeira tela do seu app" e "Não faça propaganda. A launch screen não é uma oportunidade de branding." As HIG acrescentam que, se você precisar de uma splash screen, "considere exibi-la no início do seu fluxo de onboarding". Apps para macOS, visionOS e watchOS nem sequer exigem uma launch screen.

Então, uma "splash screen animada" no iOS são, na verdade, duas coisas em sequência:

Launch screenIntro (sua splash)
Renderizado porO sistemaSua primeira view SwiftUI
ConteúdoCor e imagem estáticasQualquer coisa, incluindo Rive ou Lottie
Configurado emUILaunchScreen no Info.plist ou um storyboardCódigo
DuraçãoAté seu primeiro frame ficar prontoVocê decide
Sequência de abertura: uma launch screen estática do sistema, depois a animação de abertura dentro do app e, em seguida, o loop idle
O sistema mostra uma launch screen estática; sua animação toca assim que a primeira view aparece na tela.

Como fazer a transição da launch screen estática para uma primeira view animada?

Faça da view raiz um ZStack: seu ContentView real por baixo e a intro por cima enquanto uma flag estiver ativa. O ContentView começa a carregar imediatamente, então a intro esconde o tempo de carregamento em vez de somar a ele.

import SwiftUI

@main
struct PipApp: App {
    var body: some Scene {
        WindowGroup {
            RootView()
        }
    }
}

struct RootView: View {
    @Environment(\.accessibilityReduceMotion) private var reduceMotion
    @State private var showIntro = true

    var body: some View {
        ZStack {
            ContentView()
            if showIntro && !reduceMotion {
                IntroView {
                    withAnimation(.easeOut(duration: 0.25)) { showIntro = false }
                }
                .transition(.opacity)
                .zIndex(1)
            }
        }
    }
}

.zIndex(1) mantém a intro em fade acima do ContentView durante a transição de remoção; sem isso, a view que está saindo pode ser desenhada por baixo e o fade desaparece.

Como tocar uma intro do Rive que toca uma vez e depois mostrar o ContentView?

A intro carrega um arquivo de abertura com o novo runtime Apple do Rive, toca a animação e chama onFinish quando ela sinaliza que terminou. Como o novo runtime não tem callback de mudança de estado, o sinal mais limpo é uma propriedade trigger que o arquivo dispara no fim da intro; observe-a com stream(of:). Um timeout garante que o app nunca fique travado.

import SwiftUI
import RiveRuntime

struct IntroView: View {
    let onFinish: () -> Void
    @State private var rive: Rive?
    @State private var instance: ViewModelInstance?

    var body: some View {
        ZStack {
            Color("LaunchBackground").ignoresSafeArea()
            RiveUIViewRepresentable(rive: rive)
                .frame(width: 200, height: 200)
                .accessibilityHidden(true)
        }
        .task { await play() }
    }

    private func play() async {
        let timeout = Task { @MainActor in
            try? await Task.sleep(for: .seconds(3))
            onFinish()
        }
        do {
            let worker = try await Worker()
            let file = try await File(source: .local("launch", Bundle.main), worker: worker)
            let artboard = try await file.createArtboard()
            let instance = try await file.createViewModelInstance(
                .viewModelDefault(from: .artboardDefault(artboard))
            )
            let machine = try await artboard.createStateMachine("LaunchStateMachine", binding: instance)
            self.instance = instance
            rive = try await Rive(file: file, artboard: artboard, stateMachine: machine)

            // A Charmotion LaunchStateMachine plays its 2.6 s intro once and settles on idle;
            // it fires no "done" trigger, so wait for the intro's length. If your own file
            // fires an end trigger, await it instead: instance.stream(of: TriggerProperty(path: …)).
            try await Task.sleep(for: .seconds(2.6))
        } catch {
            // Load failure or cancellation: fall through and show the app.
        }
        timeout.cancel()
        onFinish()
    }
}

Chamar onFinish duas vezes não causa problema aqui, porque isso só define showIntro como false. Se o seu arquivo de abertura não tiver view model, createViewModelInstance lança um erro; nesse caso, use try await Rive(file: file) e conte com o timeout, definido com a duração da animação.

Os conjuntos do Charmotion incluem uma LaunchStateMachine separada para esse tipo de intro que toca uma vez: ela reproduz a animação launch de 2,6 segundos uma única vez e termina na pose idle. Por isso o exemplo simplesmente espera esse tempo.

Como evitar um flash entre a launch screen e a primeira view?

Uma "piscada" visível acontece quando a launch screen e o primeiro frame da intro são diferentes. Corrija na origem:

  1. Use uma cor nomeada para os dois. No Info.plist, defina UILaunchScreen → UIColorName com uma cor do asset catalog (aqui, LaunchBackground) e use a mesma Color("LaunchBackground") atrás da intro. Assim, os dois seguem o modo claro e o escuro da mesma forma.
  2. Alinhe a posição da imagem. Se a launch screen mostra uma imagem (UIImageName), desenhe o primeiro frame da intro com o personagem no mesmo tamanho e centralizado. O mais fácil: deixe a launch screen só com a cor e faça o personagem aparecer na intro.
  3. Não bloqueie o primeiro frame. Trabalho síncrono pesado em App.init ou no init de uma view atrasa o primeiro frame e prolonga a launch screen estática. Mova esse trabalho para .task.
  4. Mantenha a view do Rive transparente até carregar. RiveUIViewRepresentable(rive: nil) não desenha nada, então só a cor de fundo aparece até o arquivo ficar pronto.

O guia da Apple sobre como especificar a launch screen detalha as chaves do Info.plist e a opção com storyboard.

Quanto tempo deve durar uma animação de splash?

A Apple não define uma duração para intros dentro do app, mas as orientações dela apontam numa direção: a abertura deve parecer instantânea, e a launch screen existe para o app parecer pronto, não para reforçar a marca. Trate a intro do mesmo jeito.

  • Sobreponha a intro a um trabalho real. Se ela cobre a autenticação ou uma primeira requisição de rede que levaria esse tempo de qualquer forma, não custa nada ao usuário.
  • Toque uma vez por inicialização a frio. Ao voltar do segundo plano, o app deve aparecer direto.
  • Permita pular com um toque. Adicione .onTapGesture { onFinish() } à intro.
  • Considere mostrar só na primeira abertura. Guarde uma flag com @AppStorage e mostre a intro só na primeira vez ou depois de uma atualização.

Para orientações de tempo sobre animações de carregamento e feedback em geral, veja durações de animação e UX de carregamento e, para o panorama multiplataforma, animação de abertura e splash screen em apps.

Como pular a intro com Reduzir Movimento ativado?

A RootView acima já faz isso: if showIntro && !reduceMotion nunca insere a intro quando accessibilityReduceMotion é verdadeiro. O usuário vai direto da launch screen estática para a ContentView, que já combina com ela.

Se a intro também esconde um carregamento, substitua por uma versão estática em vez de nada: o mesmo fundo e uma pose parada do personagem, removidos com um fade curto quando o carregamento terminar. O objetivo do Reduzir Movimento é tirar o movimento, não mostrar uma tela vazia.

if showIntro {
    if reduceMotion {
        StaticIntro()            // background + still pose
            .transition(.opacity)
            .zIndex(1)
    } else {
        IntroView { withAnimation { showIntro = false } }
            .transition(.opacity)
            .zIndex(1)
    }
}

Oculte o personagem da intro do VoiceOver com .accessibilityHidden(true); ele não acrescenta nada à experiência de quem usa leitor de tela.

Adicione uma intro animada depois da launch screen do iOS

  1. Configure a launch screen. No Info.plist, defina UILaunchScreen com um UIColorName apontando para uma cor do asset catalog.
  2. Monte um ZStack raiz. Coloque ContentView() embaixo e uma IntroView por cima enquanto showIntro for verdadeiro.
  3. Alinhe o primeiro frame. Use a mesma cor nomeada atrás da intro e mantenha a view do Rive vazia até carregar.
  4. Toque a intro. Carregue o .riv de abertura com o novo runtime. Se o seu arquivo dispara um trigger de fim, espere por ele com stream(of:); uma LaunchStateMachine do Charmotion não dispara nenhum, então espere a duração dela, 2,6 s. Em qualquer caso, mantenha um timeout de segurança.
  5. Faça o fade out. Defina showIntro = false dentro de withAnimation, com .transition(.opacity) e .zIndex(1) na intro.
  6. Respeite o Reduzir Movimento. Pule a intro ou mostre uma versão estática quando accessibilityReduceMotion for verdadeiro.

Perguntas frequentes

Posso usar um GIF ou vídeo como launch screen do iOS?

Não. A launch screen aceita uma cor e uma imagem estáticas pelo Info.plist, ou um storyboard sem código personalizado, e o sistema a renderiza antes de o seu app rodar. Toque o GIF, vídeo ou animação Lottie ou Rive na primeira view, começando pela aparência da launch screen.

Por que minha animação de splash pisca em branco primeiro?

O fundo da launch screen e o primeiro frame da intro são diferentes, muitas vezes porque um usa uma cor fixa e o outro segue o modo escuro. Use a mesma cor do asset catalog no UIColorName da UILaunchScreen e no fundo da intro, e tire o trabalho pesado do App.init.

A intro de splash deve tocar toda vez que o app abre?

Normalmente só na inicialização a frio, e muitos apps limitam à primeira abertura ou depois de uma atualização. Ao voltar do segundo plano, o app deve aparecer na hora. Deixe o usuário pular com um toque e pule automaticamente quando Reduzir Movimento estiver ativado.

Fontes

Guias relacionados