APIs e Microsserviços

19 jun, 2017

Engenharia de renderização da assinatura no iOS com o Ubersignature

Publicidade

Desde o lançamento em 2009, a Uber tem expandido sua missão de tornar o transporte mais confiável para a entrega de comida, gatos, sorvetes e todo o resto. A API da Uber facilita para os desenvolvedores a criação de aplicativos que coordenem o movimento de todo tipo de coisas, mas cada uma dessas tecnologias tem requisitos variáveis.

Por exemplo, quando lançamos o UberRUSH em 2015, seu rápido crescimento logo exigiu que adicionássemos novas funcionalidades conforme as empresas com diferentes necessidades de entregas se juntaram à plataforma; um recurso de segurança crítico que foi exigido por muitas empresas foi uma prova de entrega com assinatura.

Para atingir esse requisito, nós precisávamos de uma maneira de coletar assinaturas para confirmar a entrega com o UberRUSH. Nós decidimos que a maneira mais eficiente de fazer isso seria adicionando um recurso de renderização de assinaturas ao aplicativo do parceiro Uber para iOS. Incluindo essa funcionalidade ao aplicativo, o parceiro-entregador poderia coletar a assinatura do recebedor rapidamente e facilmente, diretamente em seu dispositivo.

 

Hoje, nós abrimos o código o UberSignature, funcionalidade que permite aos usuários desenhar e armazenar assinaturas no aplicativo UberRUSH. Esse artigo resume como nós projetamos o UberSignature (exibido acima) utilizando um algoritmo existente com algumas modificações para adaptar a solução às necessidades dos nossos entregadores-parceiros. Nós também explicamos como fizemos as assinaturas na tela do aplicativo parecerem mais naturais.

Pesquisa para o desenvolvimento do UberSignature para iOS

Um dos principais desafios de desenvolver o UberSignature era garantir que o código de renderização da assinatura iria funcionar em todos os aparelhos, alguns deles com mais de 5 anos de idade mas que ainda são utilizados por nossos parceiros-entregadores. A funcionalidade também precisava ter uma arquitetura limpa, para que qualquer engenheiro da Uber pudesse mantê-la livre de problemas. Isso é particularmente importante porque a funcionalidade é vista pelo consumidor; um entregador parceiro reiniciando o aplicativo durante uma entrega afeta sua eficiência e causa problemas para o recebedor.

O iOS não vem com nenhuma classe embutida para tratar desenhos diretamente na tela do aparelho, e nós queríamos uma solução que permitisse ao usuário “assinar” naturalmente, com uma aparência suave. Por sorte, isso não é um problema novo no mundo do iOS. Existem muitas fontes disponíveis online que tratam esse problema. Através de nossas pesquisas, nós descobrimos um artigo detalhado, de março de 2013, sobre técnicas avançadas para desenho a mão livre usando o SDK do iOS. Um algoritmo de desenho que produz resultados bem adequados às nossas necessidades.

A solução desenha duas curvas de Bézier paralelas em distâncias variáveis entre si, que são conectadas por linhas retas em cada ponto final, produzindo uma curva de aparência suave com uma espessura que muda continuamente. Outras soluções mudam a propriedade de espessura da linha a cada segmento; isso é problemático porque as mudanças da espessura são bruscas, dando à assinatura uma aparência artificial.

Nós baseamos nosso código no método das curvas paralelas, mas estendemos a implementação e adicionamos algumas funcionalidades novas:

Atualização das assinaturas a cada toque

Ao invés de esperar para desenhar a curva de Bézier a cada quatro pontos de toque, nós queríamos que a solução atualizasse a assinatura a cada toque. Como não podemos desenhar uma curva de Bézier com menos de 4 pontos, nós decidimos desenhar o que pudéssemos com os pontos que tivéssemos: um ponto com um, uma linha com dois, e uma curva quadrática (uma curva com um único ponto de controle) com três. Com essa abordagem, a assinatura mudava a cada novo toque, se corrigindo após cada ponto ser gravado. Isso torna a implementação mais complexa, mas o desenho parece muito mais responsivo e as linhas da assinatura terminam onde os toques terminarem, tornando a aparência da assinatura muito mais natural e fluida.

Novo cálculo para a espessura da assinatura

Nós mudamos a maneira como a espessura da assinatura é calculada. Ao invés de ficar mais fina conforme o dedo do usuário de move mais rápido, nós descobrimos que fazer a assinatura aparecer mais fina imitaria melhor a aparência da caneta no papel.

Habilidade de desenhar pontos

Nós adicionamos a possibilidade de criar pontos quando toquem a tela, o que era importante para permitir que alguns usuários desenhassem com mais precisão suas assinaturas.

Dividir a implementação em múltiplas classes

Nós construímos um controle de visualização que poderia lidar com o redimensionamento e ainda oferecer as funcionalidades de redefinir, recuperar a assinatura atual e inicializar com a imagem de uma assinatura anterior. Por baixo, nós dividimos a implementação em 5 classes com menos responsabilidades. Isso tornou o código mais fácil de ser entendido, corrigido e mantido.

Projetando a arquitetura

Para possibilitar essas funcionalidades e facilitar para uma aparência mais natural para assinaturas no aplicativo, nós criamos um algoritmo de assinaturas com cinco classes distintas, descritas abaixo:

 

Existem 4 tipos de linhas/curvas desenhadas de acordo com o número de pontos coletados

Nas sessões a seguir, nós resumimos como cada uma dessas classes se enquadra na arquitetura do UberSignature.

Curvas de Bézier + UBWeightedPoint

As assinaturas apresentadas exibem diferentes pesos associados a cada ponto. Então nós criamos uma estrutura para representar isso, chamada UBWeightedPoint:

{
   CGPoint point;
   CGFloat weight;
}  UBWeightedPoint;

Então nós criamos uma categoria no UIBezierPath que cria nossos caminhos a partir dos pontos obtidos. Ela foi construída com métodos que criam um UIBezierPath para até quatro pontos obtidos, conforme demonstrado abaixo. Isso é necessário para nossa técnica de correção estendida, onde nós atualizamos a assinatura a cada toque.

 

A classe UBSignatureBezierProvider cria e finaliza béziers temporários e os expõem via delegate callbacks

Essa categoria envolve quase toda a lógica necessária para desenhar as curvas de bézier das assinaturas. O desenho e técnicas de cálculo aqui são similares à nossa inspiração original, abstraída do estado e código de exibição.

UBSignatureBezierProvider

Esse objeto responde à chamada de método addPointToSignatureBezier: e gera a assinatura, providenciando a combinação do caminho temporário (mudando constantemente) e os caminhos finais. O Bézier fica mais longo até que os quatro pontos sejam capturados, o suficiente para uma curva Bézier. Conforme exibido na figura abaixo, o caminho finalizado sempre retorna as linhas finalizadas, com o retorno temporário exibindo a parte da linha que ainda está sendo desenhada.

O método para adicionar novos pontos de toque é calculado utilizando a equação linear padrão y = mx + c, que encontra o peso associado aos pontos de acordo com a distância com o ponto de toque anterior. Nessa equação, “x” é a distância entre os pontos subtraída da distância máxima, “c” (a constante) decide o peso mínimo da linha, “m” (o gradiente) controle a taxa com que o peso diminui conforme o cumprimento muda, e “y” é o peso que é retornado. A relação entre essas variáveis resulta em um peso menor se a distância aumentar, até a distância máxima.

Conforme os pontos são adicionados, o provedor chama seu representante com uma atualização temporária do Bézier, formado utilizando os métodos da categoria UIBezierPath com os pontos recém calculados, conforme descrito abaixo:

 

Assim que o ponto de toque para o próximo Bézier tenha sido adicionado, o último ponto do Bézier atual se torna a média para o ponto seguinte e o que vem depois, uma tática que facilita a junção deles. Nesse momento, o Bézier atual não vai mudar, então o representante será notificado que esse caminho foi finalizado. O Bézier temporário pega outro ponto, utilizando o último ponto do caminho finalizado como o primeiro ponto e o novo ponto como segundo. Esse processo se repete indefinidamente até o provedor ser redefinido, iniciando uma nova linha.

UBSignatureDrawingModel

UBSignatureDrawingModel é uma classe que utiliza uma instância do UBSignatureBezierProvider para gerar a assinatura, armazenando-a e exibindo como dois objetos: um UIImage e um (temporário) UIBezierPath.

Utilizar um único Bézier para representar a assinatura inteira não escalaria bem; teria que ser desenhado a cada atualização, com cada vez mais pontos, dessa maneira, aumentaria o tempo necessário para exibir a assinatura na tela sempre que ela mudar. Nós utilizamos uma imagem para representar a assinatura, com os Béziers do provedor desenhados a cada vez que eles sejam finalizados. Apesar de a imagem demorar ligeiramente mais para ser desenhada do que um Bézier, é uma operação constante e escala bem (e rapidamente) quando comparada com outros métodos.

Nosso modelo também armazena e exibe o caminho temporário do provedor. Nós não podemos desenhar isso na imagem, porque durante a próxima atualização ele vai mudar de forma e não podemos remover o caminho anterior da imagem. Ao invés disso, nós armazenamos e exibimos o caminho separadamente. Eles poderiam ser exibidos como uma imagem diferente, mas a performance é pior para exibir uma outra imagem em tamanho real do que para exibir um pequeno caminho de Bézier que nunca vai exceder quatro pontos.

Como a assinatura é representada por uma imagem, ela corresponde às dimensões da visualização. Se o tamanho da visualização suportada pelo modelo muda, pode ser solicitado o redimensionamento da imagem. Antes do redimensionamento, ele vai desenhar o bézier na imagem, pois escalar nossa curva bézier e calcular o peso dos pontos pode ser complicado e não se alinhar bem à nossa imagem redimensionada.

UBSignatureDrawingModelAsync

O UBsignatureDrawingModelAsync é um wrapper ao redor do modelo, permitindo que ele seja utilizado de maneira assíncrona para que ele não bloqueie o fluxo principal do aplicativo. Isso é importante pois o modelo é computacionalmente caro; executar ele no fluxo principal reduziria a frequência com que poderíamos capturar os toques, afetando significantemente a precisão da assinatura.

Para evitar complicações na implementação, o modelo subjacente é desenvolvido sincronamente e não utiliza os fluxos em backgroud. O wrapper assíncrono então utiliza o modelo em um NSOperationQueue e o fluxo principal para os métodos síncronos. A propriedade do modelo é atômica para evitar que múltiplos fluxos acessem o modelo simultaneamente, assim abstraindo a complexidade do código assíncrono em uma única classe. Isso também significa que ainda podemos utilizar o modelo síncrono diretamente. No caso de correção de erros, isso torna nosso código mais fácil de ser seguido.

UBSignatureDrawingViewController

O controlador de visualização, chamado UBSignatureDrawingViewController, encapsula nossa funcionalidade de assinatura. Ele utiliza o modelo assíncrono internamente, sobrescrevendo os eventos de toque UIResponder para adicionar pontos, então exibe a imagem da assinatura através a UIImageView e a curva Bézier temporária utilizando o CALayer. Ele também redimensiona os eventos e exibe um método de imagens para obter a assinatura quando estiver completa.

Próximos passos

Com os smartphones se tornam mais importantes para o comércio, o UberSignature fornece uma maneira aperfeiçoada e tipograficamente mais precisa para assinaturas no iOS utilizando somente o toque. Nós esperamos que os desenvolvedores possam se beneficiar da utilização do UberSignature em seus próprios aplicativos.

 

***

Este artigo é do Uber Engineering. Ele foi escrito por Dom Chapman. A tradução foi feita pela Redação iMasters com autorização. Você pode conferir o original em: https://eng.uber.com/ubersignature/