Modo offline no iOS e recursos de sua implementação no Realm



Publicado por Ekaterina Semashko, Desenvolvedor Strong Junior para iOS, DataArt

Um pouco sobre o projeto: um aplicativo móvel para a plataforma iOS, escrito em Swift. O objetivo do aplicativo é a capacidade de compartilhar cartões de desconto entre funcionários da empresa e seus amigos.

Um dos objetivos do projeto era aprender e praticar tecnologias e bibliotecas populares. O domínio foi escolhido para armazenar dados locais, o Alamofire foi usado para trabalhar com o servidor, o Google Sign-In foi usado para autenticação, o PINRemoteImage foi usado para o upload de imagens.

As principais funções do aplicativo:

  • adicionando um mapa, editando e excluindo;
  • visualizar os cartões de outras pessoas;
  • procure cartões pelo nome da loja / nome de usuário;
  • Adicione cartões aos seus favoritos para acesso rápido.

A capacidade de usar o aplicativo sem conectar-se à rede foi assumida desde o início, mas apenas no modo de leitura. I.e. pudemos ver informações sobre cartões, mas não pudemos modificá-los sem a Internet. Para isso, o aplicativo sempre teve uma cópia de todos os cartões e marcas do banco de dados do servidor, além de uma lista de favoritos para o usuário atual. A pesquisa também foi implementada localmente.

Mais tarde, decidimos expandir offline adicionando um modo de gravação. As informações sobre as alterações feitas pelo usuário foram armazenadas e sincronizadas quando uma conexão com a Internet apareceu. A implementação desse modo offline de leitura e gravação será discutida.



O que é necessário para um modo offline completo em um aplicativo móvel? Precisamos remover a dependência do usuário da qualidade da conexão com a Internet, em particular:

  1. Remova do servidor a dependência de respostas ao usuário sobre suas ações na interface do usuário. Primeiro, a solicitação irá interagir com o armazenamento local e, em seguida, será enviada ao servidor.
  2. Marque e armazene alterações locais.
  3. Implemente um mecanismo de sincronização - quando uma conexão com a Internet aparecer, você precisará enviar alterações ao servidor.
  4. Mostre ao usuário quais alterações são sincronizadas, quais não são.

Primeira abordagem offline


Antes de tudo, tive que alterar o mecanismo existente para interagir com o servidor e o banco de dados. O objetivo era impedir que o usuário dependesse da presença ou ausência da Internet. Antes de tudo, ele deve interagir com o data warehouse local e as solicitações do servidor devem ficar em segundo plano.

Na versão anterior, havia uma forte conexão entre a camada de armazenamento de dados e a camada de rede. O mecanismo para trabalhar com os dados foi o seguinte: primeiro foi feita uma solicitação ao servidor por meio da classe NetworkManager, aguardamos o resultado, depois os dados foram salvos no banco de dados por meio da classe Repository. Em seguida, o resultado foi fornecido à interface do usuário, conforme mostrado no diagrama.


Para implementar a primeira abordagem offline, separei a camada de armazenamento de dados e a camada de rede, introduzindo uma nova classe Flow que controlava a ordem em que o NetworkManager e o Repository foram chamados. Agora, os dados são salvos no banco de dados por meio da classe Repository, o resultado é enviado à interface do usuário e o usuário continua trabalhando com o aplicativo. Em segundo plano, é feita uma solicitação ao servidor, após a resposta, as informações no banco de dados e na interface do usuário são atualizadas.


Trabalhar com identificadores de objeto


Com a nova arquitetura, várias novas tarefas apareceram, uma das quais está trabalhando com objetos de identificação. Anteriormente, nós os recebíamos do servidor ao criar o objeto. Mas agora que o objeto foi criado localmente, foi necessário gerar um ID e, após a sincronização, atualizá-lo para os atuais. Aqui me deparei com a primeira limitação do Realm: depois de criar um objeto, você não pode alterar sua chave primária.

A primeira opção foi abandonar a chave primária no objeto, transformar o id em um campo regular. Mas, ao mesmo tempo, as vantagens de usar a chave primária foram perdidas: indexação de região, que acelera a busca do objeto, a capacidade de atualizar o objeto com o sinalizador de criação (crie um objeto se ele não existir) e a conformidade com a exclusividade do objeto.

Eu queria salvar a chave primária, mas não poderia ser a identificação do objeto no servidor. Como resultado, a solução de trabalho era ter dois identificadores, um deles servidor, campo opcional e o segundo local, que seria a chave primária.

Como resultado, o ID local é gerado no cliente ao criar o objeto localmente e, no caso em que o objeto veio do servidor, é igual ao ID do servidor. Como no aplicativo de fonte única de verdade existe um banco de dados, ao receber dados do servidor, o objeto é atualizado com o identificador local atual e funciona apenas com ele. Ao enviar dados para o servidor, o identificador do servidor é transmitido.

Armazenamento de alterações não sincronizadas


Alterações em objetos que ainda não foram enviados para o servidor devem ser armazenadas localmente. Isso pode ser implementado das seguintes maneiras:

  1. Adicionando campos a objetos existentes
  2. armazenar objetos não sincronizados em tabelas separadas;
  3. armazenando alterações de campos individuais em algum formato.


Eu não uso objetos Realm diretamente em minhas classes, mas faço o mapeamento deles por conta própria para evitar problemas com o multithreading. As atualizações automáticas da interface são feitas usando amostras de resultados de atualização automática, nas quais assino solicitações de atualização. Somente a primeira abordagem funcionou com minha arquitetura atual; portanto, a opção recaiu na adição de campos aos objetos existentes.

O objeto de mapa passou por mais alterações:

  • sincronizado - existem dados no servidor;
  • delete - true, se o cartão for excluído apenas localmente, a sincronização será necessária.

Identificadores discutidos na parte anterior:

  • localId - a chave primária da entidade no aplicativo, igual ao ID do servidor ou gerada localmente;
  • serverId - identificação do servidor.

Vale a pena mencionar separadamente o armazenamento de imagens. Em essência, o campo DiskURL do anexo foi adicionado ao campo serverURL da imagem no servidor, que armazena o endereço da imagem não sincronizada local. Ao sincronizar a imagem, a local foi excluída para não obstruir a memória do dispositivo.

Sincronização do servidor


Para sincronizar com o servidor, foi adicionado o trabalho com alcançabilidade, para que, quando a Internet aparecer, o mecanismo de sincronização seja iniciado.

Primeiro, ele verifica se há alguma alteração no banco de dados que precise ser enviada. Em seguida, uma solicitação é enviada ao servidor para uma conversão de dados real; como resultado, as alterações que não precisam ser enviadas ao cliente são filtradas (por exemplo, uma alteração em um objeto que já foi excluído no servidor). As alterações restantes enfileiram solicitações ao servidor.

Para enviar alterações, foi possível implementar atualizações em massa, enviando as alterações em uma matriz ou fazer uma grande solicitação para sincronizar todos os dados. Mas, nessa época, o desenvolvedor de back-end já estava ocupado em outro projeto e apenas nos ajudou em nosso tempo livre; portanto, para cada tipo de alteração, criamos nossa própria solicitação.

Eu implementei a fila através do OperationQueue e agrupei cada solicitação em uma operação assíncrona. Algumas operações dependem uma da outra, por exemplo, não podemos carregar a imagem do mapa antes de criar o mapa, por isso adicionei a dependência da operação da imagem à operação do mapa. Além disso, a operação de upload de imagens para o servidor recebeu uma prioridade mais baixa do que todos os outros, e eu as adicionei à fila também por último devido ao seu peso pesado.

Ao planejar o modo offline, a grande questão era resolver conflitos com o servidor durante a sincronização. Mas quando chegamos a esse ponto durante a implementação, percebemos que o caso em que um usuário altera os mesmos dados em dispositivos diferentes é muito raro. Portanto, basta implementarmos o último mecanismo de vitórias do escritor. Durante a sincronização, sempre é dada prioridade às alterações não enviadas no cliente, elas não são esfregadas.

O tratamento de erros ainda está no início, se a sincronização falhar, o objeto será adicionado à fila de alterações na próxima vez que a Internet aparecer. E, se ainda ficar fora de sincronia após a mesclagem, o usuário decidirá se deve deixá-lo ou excluí-lo.

Solução adicional ao trabalhar com o Realm


Ao trabalhar com o Reino, enfrentamos vários outros problemas. Talvez essa experiência também seja útil para alguém.

Ao classificar por sequência, a ordem vai de acordo com a ordem de caracteres em UTF-8, não há suporte à pesquisa com distinção entre maiúsculas e minúsculas. Estamos diante de uma situação em que os nomes em minúsculas vêm depois dos nomes em maiúsculas, por exemplo: Ímã, Pyaterochka, Fita. Se a lista for muito grande, todos os nomes em letras minúsculas estarão na parte inferior, o que é muito desagradável.

Para preservar a ordem de classificação, independentemente do caso, tivemos que introduzir um novo campo lowercasedName, atualizá-lo ao atualizar o nome e classificá-lo.

Além disso, um novo campo foi adicionado para classificação pela presença de um cartão nos favoritos, pois, em essência, isso requer uma subconsulta para as relações do objeto.

Ao pesquisar no Domínio, existe o método CONTAINS [c]% @ para pesquisas que não diferenciam maiúsculas de minúsculas. Mas, infelizmente, ele funciona apenas com o alfabeto latino. Para marcas russas, também tivemos que criar campos separados e pesquisá-los. Mais tarde, porém, ficou em nossas mãos excluir caracteres especiais ao pesquisar.



Como você pode ver, para aplicativos móveis, é bem possível implementar um modo offline com salvar alterações e sincronizar com pouco sangue, e às vezes até com alterações mínimas no back-end.

Apesar de algumas dificuldades, você pode usar o Realm para implementá-lo, enquanto recebe todas as vantagens na forma de atualizações ao vivo, arquitetura de cópia zero e uma API conveniente.

Portanto, não há motivo para negar aos usuários o acesso aos dados a qualquer momento, independentemente da qualidade da conexão.

Source: https://habr.com/ru/post/pt432422/


All Articles