Anatomia das barras do sistema no SwiftUI: status bar, home indicator e safe area

Como funcionam a status bar e o home indicator no iPhone e como garantir o contraste no SwiftUI com toolbarColorScheme, preferredColorScheme e safe area.

Anatomia das barras do sistema, no Flutter, Jetpack Compose e SwiftUI, com um Android e um iPhone e as barras rotuladas

Já tive publicações recusadas na App Store por causa da status bar. O problema era contraste: a cor dos ícones do sistema não combinava com o fundo da tela, e a informação do topo ficava difícil de ler. No Android, o mesmo descuido costuma aparecer no rodapé, com o ícone de voltar branco sobre uma navigation bar branca, e o usuário não encontra como sair da tela.

Esse tipo de erro não depende do framework. No SwiftUI, o cuidado fica nas telas que fogem do padrão, como um player escuro, porque não existe uma API direta para escolher a cor do texto da status bar. Por isso resolvi escrever sobre a anatomia dessas barras, como elas se comportam e como configurá-las no SwiftUI.

O que é do sistema e o que é do app

Android e iPhone lado a lado, com status bar, navigation bar, home indicator, recorte da câmera, Dynamic Island e área do app rotulados

No iPhone, a tela tem duas faixas que não pertencem ao app. No topo fica a status bar, com hora, rede e bateria, dividindo espaço com o notch ou a Dynamic Island. No rodapé fica o home indicator. Não existe navigation bar com botões como no Android.

O app não desenha o conteúdo dessas faixas. O que ele controla são duas coisas: o que aparece atrás delas e se o texto da status bar vai ser claro ou escuro. A primeira resposta vem do layout do app. A segunda é uma configuração que muita gente nunca toca, e é ela que decide se a hora vai ser legível.

O erro que normalmente acontece

Esse erro chega até a loja porque depende de contexto. Quem desenvolve costuma testar com o aparelho num modo só, claro ou escuro. O usuário usa o outro, ou chega numa tela com fundo próprio, como a splash, o onboarding ou um player de vídeo, e o texto da status bar precisa combinar com esse fundo.

A crença que sustenta o problema é "a barra é do sistema, então o sistema cuida dela". Ela tem uma parte verdadeira: quem desenha a hora e a bateria é o sistema operacional. Mas a cor desses ícones é uma decisão do app, e o sistema não confere se ela combina com o que está atrás.

Como cada framework chama claro e escuro

Tabela com fundo claro e fundo escuro nas linhas e Flutter, Jetpack Compose e SwiftUI nas colunas, mostrando o nome que cada um usa para ícones escuros ou claros

Antes de escrever código, responda uma pergunta: o fundo atrás da barra é claro ou escuro? Fundo claro pede ícones escuros, e fundo escuro pede ícones claros.

A confusão é que cada framework dá nome a isso de um jeito. No Flutter, o nome descreve o ícone, então ícones escuros são dark. No Compose e no SwiftUI, o nome descreve o fundo, então os mesmos ícones escuros são light.

No SwiftUI: color scheme e safe area

Anatomia no SwiftUI: um iPhone com setas ligando toolbarColorScheme, toolbarBackground, preferredColorScheme, ignoresSafeArea, safe area e persistentSystemOverlays à parte da tela que cada um controla

O SwiftUI não tem uma API direta para escolher a cor dos ícones da status bar. No estilo padrão, o iOS escolhe a aparência sozinho para manter o contraste com o conteúdo de baixo. Quando uma tela precisa de um resultado garantido, como um player escuro, o caminho é declarar o color scheme da barra ou da apresentação.

Dentro de um NavigationStack, cada tela pode declarar o color scheme da própria barra:

struct PlayerView: View {
    var body: some View {
        ZStack {
            Color.black.ignoresSafeArea()
            Image(systemName: "play.circle.fill")
                .font(.system(size: 96))
                .foregroundStyle(.white)
        }
        .navigationTitle("Player")
        .toolbarBackground(.black, for: .navigationBar)
        .toolbarBackground(.visible, for: .navigationBar)
        .toolbarColorScheme(.dark, for: .navigationBar)
    }
}

O toolbarColorScheme(.dark, for: .navigationBar) deixa a status bar com texto claro enquanto essa tela está no topo da pilha, e ao voltar vale o que a tela anterior declarou. O nome segue a lógica do Compose: .dark descreve o fundo, e o resultado é texto claro. As duas linhas de toolbarBackground são obrigatórias, porque sem o fundo da barra visível o SwiftUI ignora o toolbarColorScheme. Os três modificadores exigem iOS 16.

Para telas sem barra de navegação, como um player aberto com .fullScreenCover, o caminho é o .preferredColorScheme(.dark). Ele vale para a apresentação inteira, então funciona bem numa tela modal, mas numa tela comum da pilha escurece o app todo. Se nenhuma das duas opções servir, sobra o UIKit: uma subclasse de UIHostingController que sobrescreve preferredStatusBarStyle.

Os insets funcionam ao contrário do Android. O SwiftUI respeita a safe area por padrão, e desenhar atrás das barras é uma escolha explícita. No exemplo, só o Color.black usa .ignoresSafeArea(), então o fundo ocupa a tela toda e o ícone continua na área livre.

Duas telas: a inicial clara com ícones escuros e o player escuro com ícones claros, com o código de Flutter, Jetpack Compose e SwiftUI que declara o estilo de cada uma

Antes de mandar para a loja

A maior parte desses erros só aparece em uma combinação específica de tema, navegação e tela. Antes de cada envio, vale passar por três cenários:

Tema claro e escuro. Troque o tema do sistema com o app aberto e navegue por todas as telas.

Telas sem barra de navegação. Splash, onboarding, player, câmera e qualquer tela com imagem atrás da status bar.

iPhone com notch ou Dynamic Island, no modo claro e no escuro. No iOS, o fundo atrás da status bar é sempre o do app, então vale testar as telas escuras com o aparelho no modo claro e as telas claras no modo escuro.

A regra que vale para qualquer framework

Toda decisão sobre as barras do sistema começa pela cor do fundo atrás delas. Com essa resposta, o nome de cada API vira detalhe de tradução, e o resto é aplicar os insets para o conteúdo não ficar embaixo da hora nem dos gestos.

Nenhuma dessas APIs confere a sua configuração. O estilo padrão do iOS tenta manter o contraste, mas quando o app declara a cor errada, o texto branco sobre fundo branco chega até a revisão da loja do mesmo jeito.

Depois das barras, o próximo passo são os outros casos em que o sistema ocupa parte da tela: o teclado e o notch quando o app gira para paisagem. Em todos eles o app precisa saber qual parte é dele.

Bora construir algo, ou só dar um oi

Se você tem um projeto em mente, quer falar sobre tecnologia ou só dar um oi, eu leio tudo que chega na caixa de entrada.