Fazendo MotionEffects com SwiftUI
Pequenos detalhes fazem toda a diferença, e pequenos detalhes me deixam desproporcionalmente feliz, porque trazem deleite, fazem com que pequenas interações que temos com os nossos aparelhos um sejam pouco mais legais, mais divertidas. E o que mais pode nos fazer felizes, como desenvolvedores? O SwiftUI, é claro!
Uma das interações mais legais e super tranquilas da gente implementar nos nossos apps UIKit é o UIInterpolationMotionEffect, que dá aquele efeito de parallax quando mexemos o celular, mais conhecido pelo efeito Perspectiva que podemos aplicar nas imagens de fundo de nossos iPhones e iPads. Mas quando tentei adicionar um em minha View de SwiftUI, fui pego por isso daqui:
Eis que descubro, pesquisando pela documentação que não existe uma forma de colocar um efeito desses nativamente com o SwiftUI! Meu primeiro pensamento foi criar uma UIViewRepresentable que tivesse a minha View dentro de um container do SwiftUI, fazendo um bridge de SwiftUI -> UIKit -> SwiftUI, que tentei fazer, mas sem muito sucesso.
Decidi então ver se era possível ler os sensores do aparelho e recriar esse comportamento manualmente, criando uma view que me parece mais “nativa” ao SwiftUI. E com o CoreMotion isso tudo não só é possível como é bem fácil, e vai nos dar mais facilidade para criar efeitos compostos, como elementos que se mexem em velocidades diferentes de acordo com o movimento do meu device.
Para acessar os dados do acelerômetro/giroscópio do aparelho, precisamos de uma instância do CMMotionManager, que segundo as recomendações da Apple, precisa ser compartilhada:
Com o CMMotionManager
, podemos acessar o acelerômetro, giroscópio, magnetômetro e o device-motion, que é uma multitude de dados inferidos pelos algoritmos do Core Motion. Como quero fazer meu efeito com a rotação do device, vou utilizar o giroscópio, que mede cada eixo de rotação do aparelho. Primeiro, crio uma View
que vai cuidar dessa leitura e, assim como o GeometryReader faz, expor esses dados para views filhas, e nela fazer as configurações necessárias do MotionManager, como o intervalo de update dos dados (com uma frequência de atualização de 30x por segundo), o início/fim da leitura deles e o publisher onde vamos ler e processar esses dados:
E assim podemos começar a ler os dados do nosso manager, alterando o onReceive
. Também criei um struct
que encapsula os dados do giroscópio (eixos x,y e z) para passá-los para nossas child views:
Fazendo isso tudo, devemos rodar o app no device (o simulador não tem suporte ao CoreMotion, infelizmente), e aí, podemos atestar que quase nada mudou, e o pouco que mudou não é perceptível quando utilizamos o aparelho em si, sendo visível apenas pelo vídeo numa tela parada. Além disso, o movimento está muito tremido.
Os valores que o giroscópio emite são muito baixos, então precisamos manipular eles de forma que o offset
do card seja alterado com mais intensidade, e podemos aproveitar para configurar alguns limites, iguais às propriedades minimumRelativeValue
e maximumRelativeValue
do UIInterpolationMotionEffect
, além de adicionar uma animação básica na view, para que a tremedeira pare:
Agora além de ter uma animação muito mais suave, podemos configurar todos os valores do movimento para que o efeito fique exatamente da forma como desejamos! Mas ao virar o device, podemos perceber que o movimento não está indo pros mesmos lados quando mexemos nosso device. Por que? As coordenadas de x e y da minha view e as coordenadas que o giroscópio está emitindo não são consistentes em cada orientação, então precisamos calcular e fazer essa troca manualmente.
Para isso, precisamos criar uma forma de detectar quando o aparelho muda de orientação, e mudar o cálculo do offset de acordo, e isso é bem simples:
Para finalizar, o nosso Reader deve respeitar qualquer configuração que o usuário possa fazer no aparelho, como o modo de baixa energia e a configuração de acessibilidade para reduzir movimentos. Para isso, precisamos criar duas propriedades, uma para acessar o Environment que representa o Reduce Motion e uma que encapsula todas as barreiras de acesso ao acelerômetro:
Como uma cerejinha em cima do bolo para facilitar o uso do nosso Reader, criaremos uma extension de View
que faz todo o processo de parallaxing automagicamente para a gente!
E com isso, o nosso código lá do começo funciona perfeitamente, da forma que eu queria que ele funcionasse :)
O projeto completo com o exemplo do card e o reader do giroscópio está no meu GitHub, e deixo meus agradecimentos aqui ao @Alan Pégoli que fez o beta test e apontou uma falha gritante no artigo :)