Splash screen animada em SwiftUI (padrão de código)
Por equipe Charmotion · Atualizado em

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
ContentViewpor 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 screen | Intro (sua splash) | |
|---|---|---|
| Renderizado por | O sistema | Sua primeira view SwiftUI |
| Conteúdo | Cor e imagem estáticas | Qualquer coisa, incluindo Rive ou Lottie |
| Configurado em | UILaunchScreen no Info.plist ou um storyboard | Código |
| Duração | Até seu primeiro frame ficar pronto | Você decide |

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:
- Use uma cor nomeada para os dois. No Info.plist, defina
UILaunchScreen→UIColorNamecom uma cor do asset catalog (aqui,LaunchBackground) e use a mesmaColor("LaunchBackground")atrás da intro. Assim, os dois seguem o modo claro e o escuro da mesma forma. - 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. - Não bloqueie o primeiro frame. Trabalho síncrono pesado em
App.initou noinitde uma view atrasa o primeiro frame e prolonga a launch screen estática. Mova esse trabalho para.task. - 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
@AppStoragee 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
- Configure a launch screen. No Info.plist, defina
UILaunchScreencom umUIColorNameapontando para uma cor do asset catalog. - Monte um ZStack raiz. Coloque
ContentView()embaixo e umaIntroViewpor cima enquantoshowIntrofor verdadeiro. - Alinhe o primeiro frame. Use a mesma cor nomeada atrás da intro e mantenha a view do Rive vazia até carregar.
- 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:); umaLaunchStateMachinedo Charmotion não dispara nenhum, então espere a duração dela, 2,6 s. Em qualquer caso, mantenha um timeout de segurança. - Faça o fade out. Defina
showIntro = falsedentro dewithAnimation, com.transition(.opacity)e.zIndex(1)na intro. - Respeite o Reduzir Movimento. Pule a intro ou mostre uma versão estática quando
accessibilityReduceMotionfor 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.