
Caça a um bug de animação invisível no iOS do DuckDuckGo
O problema
Ao polir a interface flutuante do DuckDuckGo para iOS - uma barra de ferramentas em cápsula de vidro que se transforma e colapsa de forma semelhante à do Safari - Karl Koch encontrou um bug na transição entre o seletor de abas e uma página web. Em câmera lenta, aparecia uma dupla imagem fantasma da barra: duas cópias sobrepostas das setas de voltar e avançar, do contador de abas e do botão de menu. O restante da transição parecia correto, o que levou a três tentativas de correção tratando o problema como sequenciamento (retimulação do fade, ajuste de crossfade, easing). Nada resolveu, porque o fantasma apenas mudava de forma.
Medir em vez de adivinhar
A solução veio ao gravar a transição a 60fps com o modo de animações lentas do simulador (10× de desaceleração) e examinar quadro a quadro com ffmpeg. Foi assim que se descobriu que o badge de contagem de abas da barra real estava totalmente visível 24 quadros antes de a animação de snapshot começar. Nenhuma retimulação de snapshot corrigiria um bug que ocorria antes de o código da animação rodar.
A causa real não era duração nem curva, mas posse da view (view ownership): dispensar o seletor de abas disparava um completion handler assíncrono que forçava a opacidade total da barra no próximo turno do run-loop, sem saber que uma transição customizada em curso já controlava aquela view. Outros três caminhos de código podiam fazer o mesmo. A correção foi uma flag de posse: enquanto a transição controla a barra, nenhum outro caminho escreve a propriedade alpha.
Bounds não são o que é desenhado
Resolvido o fantasma, surgiu um segundo bug: os cantos arredondados inferiores da barra permaneciam retos durante toda a transição e se arredondavam de repente quando a barra real assumia. A cápsula é deliberadamente desenhada fora dos próprios bounds da view, para flutuar perto do indicador de home sem afetar o layout. Como snapshotView(afterScreenUpdates:) captura apenas os bounds, cada snapshot tinha a base da cápsula cortada, incluindo cantos e sombra.
A lição generaliza: quando uma transformação empurra conteúdo para fora da view, bounds e conteúdo desenhado viram retângulos distintos. A correção foi calcular o retângulo visível unindo os bounds ao frame da view de fundo com a margem da sombra, e snapshotar o que a view desenha, não o que seus bounds indicam.
O achatamento
Um terceiro bug, apontado em review, achatava o logo do Dax em uma oval ao abrir o seletor de abas de uma página nova vazia - mas apenas na visualização em lista. Um snapshot da página inteira era redimensionado para caber no frame da célula de destino, e resizableSnapshotView estica de forma não uniforme em vez de escalar. Numa grade, as proporções eram próximas às da tela e ninguém notava; numa linha de lista, cerca de seis vezes mais larga que alta, a página virava uma faixa achatada. A primeira correção, ajustar à largura, eliminou o estiramento mas causava corte agressivo. A solução foi escalar pelo menor dos dois fatores (min entre largura e altura), obtendo uma redução proporcional e centralizada em vez de um preenchimento que recorta.
Lições reaproveitáveis
Koch conclui que qualquer transição que anime um snapshot em vez da view real merece checagem de dois modos de falha: capturar bounds em vez da extensão desenhada, e redimensionar em vez de escalar. Ambos passam em code review porque compilam e parecem corretos na proporção testada - só aparecem com outra forma, uma linha mais larga ou uma view com overflow, quando então o sintoma parece um bug de timing. Raramente é. A recomendação é gravar a transição, percorrê-la quadro a quadro e verificar qual retângulo se está pedindo antes de mexer em durações ou curvas.