Aplicativos móveis nem sempre são simples e concisos, como nós os desenvolvedores adoramos. Outros aplicativos são criados para resolver problemas complexos do usuário e contêm muitas telas e scripts. Por exemplo, aplicativos para a realização de testes, questionários e pesquisas - sempre que você precisar preencher muitos formulários no processo. Esta aplicação será discutida neste artigo.

Começamos a desenvolver um aplicativo móvel para agentes envolvidos no registro de apólices de seguros no local. Eles preenchem grandes formulários no aplicativo com dados do cliente: informações sobre o carro, proprietários, motoristas, etc. Embora cada formulário tenha suas próprias seções, células e estrutura, e cada item do questionário exija um tipo de dados exclusivo (sequência, data, documento em anexo), os formulários de tela eram bastante semelhantes. Mas o principal é o número deles ... Ninguém quer se envolver na repetição da visualização e no processamento dos mesmos elementos muitas vezes.
Para evitar as muitas horas de trabalho manual na criação de formulários, você precisa aplicar um pouco de criatividade e muita construção dinâmica da interface do usuário. Neste artigo, queremos compartilhar como resolvemos esse problema.
Para uma solução elegante para o problema, usamos o mecanismo para gerar objetos - ViewModels, que são usados para criar formulários personalizados usando tabelas.

No trabalho normal, para cada tabela individual que o desenvolvedor deseja ver na tela, uma classe ViewModel separada deve ser criada. Ele define o componente visual da tabela. Decidimos subir um nível acima e gerar ViewModels e Models dinamicamente, usando uma descrição simples da estrutura através dos campos Enum.
Como isso funciona
Tudo começou com enum. Para cada perfil, criamos uma enumeração única - essas são nossas seções do perfil. Um de seus métodos é retornar a matriz de células nesta seção.
As células na tabela também serão enumeradas com funções adicionais que descreverão as propriedades das células. Em tais funções, definimos o nome da célula, o valor inicial. Posteriormente, adicionou parâmetros como
- display check: algumas células devem estar ocultas,
- lista de células “principais”: células das quais depende o valor, validação ou exibição dessa célula,
- tipo de célula: células simples com valores, células no switch, células com a função de adicionar elementos, etc.
Assinamos todas as seções do protocolo geral QuestionnaireSectionCellType para excluir a ligação a uma seção específica; faremos o mesmo com todas as células da tabela (QuestionnaireCellType).
protocol QuestionnaireSectionCellType { var title: String { get } var sectionCellTypes: [QuestionnaireCellType] { get } } protocol QuestionnaireCellType { var title: String { get } var initialValue: Any? { get } var isHidden: Bool { get } var parentFields: [QuestionnaireCellType] { get } … }
Esse modelo será muito fácil de preencher. Simplesmente percorremos todas as seções, em cada seção percorremos uma matriz de células e as adicionamos à matriz do modelo.
No exemplo da tela do tomador de seguro (enumeração com seções - InsurantSectionType):
final class InsurantModel: BaseModel<QuestionnaireCellType> { override init() { super.init() initParameters() } private func initParameters() { InsurantSectionType.allCases.forEach { type in type.sectionCellTypes.forEach { if let valueModel = ValueModel(type: $0, parentFields: $0.parentFields, value: $0.initialValue) { valueModels.append(valueModel) } } } } }
Feito! Agora temos uma tabela com valores iniciais. Adicione métodos para ler o valor com a chave QuestionnaireCellType e salve-o no elemento de matriz desejado.
Alguns modelos podem ter campos opcionais, portanto, adicionamos uma matriz com chaves opcionais. Durante a validação do modelo, essas chaves podem não conter valores, mas o modelo será considerado preenchido.
Além disso, por conveniência, todos os valores no ValueModel que subscrevemos no protocolo comum StringRepresentable protocol para limitar a lista de valores possíveis e adicionar um método para exibir o valor na célula.
protocol StringRepresentable { var stringValue: String? { get } }
A funcionalidade aumentou e muitas outras propriedades e métodos apareceram nos modelos: limpeza do modelo (os valores iniciais devem ser definidos em alguns modelos), suporte para uma matriz dinâmica de valores (valor: Matriz), etc.
Essa abordagem acabou sendo muito conveniente para armazenar no banco de dados usando o Realm. Para preencher o questionário, é possível selecionar um modelo completo salvo anteriormente. Para estender a política CTP, o agente não precisará mais preencher os documentos do usuário, os drivers anexados por ele e os dados TCP do novo. Em vez disso, você pode simplesmente reutilizá-lo para preencher o existente.
Para alterar ou suplementar tabelas, você só precisa encontrar o ViewModel relacionado a uma tela específica, encontrar a enumeração necessária para exibir o bloco desejado e adicionar ou corrigir vários casos. Tudo, a mesa terá a forma necessária!
O preenchimento do formulário com os valores dos testes também foi muito conveniente e rápido. Dessa forma, você pode gerar rapidamente quaisquer dados de teste. E se você adicionar um arquivo separado com os dados iniciais, de onde o programa levará o valor para cada campo específico do questionário, mesmo um iniciante poderá gerar questionários prontos, sem precisar desmontar o restante do código, exceto um arquivo específico.
Dependências
Uma tarefa separada que resolvemos durante o processo de desenvolvimento é o tratamento de dependências. Alguns elementos do questionário foram interconectados. Portanto, o número do documento não pode ser preenchido sem a escolha do tipo do documento, o número da casa não pode ser indicado sem a indicação da cidade e da rua etc.

Fizemos a atualização dos valores do questionário com a limpeza de todos os campos dependentes (por exemplo, ao excluir ou alterar o tipo de um documento, limpamos o campo "número do documento"):
func updateValueModel(value: StringRepresentable?, for type: QuestionnaireCellType) { guard let model = valueModels.first(where: { $0.type.equal(to: type) }) else { return } model.value = value clearRelativeValues(type: type) } func clearRelativeValues(type: QuestionnaireCellType) { _ = valueModels.filter { $0.parentFields.contains(where: { $0.equal(to: type) }) } .compactMap { $0.type } .compactMap { updateValueModel(value: nil, for: $0) } }
Armadilhas que tivemos que resolver durante o desenvolvimento e como conseguimos
É claro que esse método é conveniente para telas com a mesma funcionalidade (preenchimento dos campos), mas não é tão conveniente se elementos ou funções exclusivas aparecerem em uma tela separada e não em outras telas. Em nossa aplicação, foram eles:
- Uma tela com a potência do motor, que teve que ser gerada separadamente, e é por isso que diferia na funcionalidade. Nesta tela, a solicitação deve desaparecer e o valor do servidor é substituído automaticamente. Eu tive que criar separadamente uma classe para ela, que seria responsável por exibir, carregar, validar, carregar do servidor e substituir um valor em um campo vazio, sem incomodar o usuário se este decidir inserir seu próprio valor.
- A tela do número de registro, na qual o único é o comutador, que afeta a exibição ou ocultação do campo de texto. Para este caso, uma condição adicional teve que ser feita, que determinaria programaticamente os casos com a posição do interruptor ativada como um valor vazio.
- Listas dinâmicas, como uma lista de drivers que precisavam ser armazenados e vinculados a um formulário, também ficaram fora de conceito.
- Tipos exclusivos de validação de dados. Pode haver muitas máscaras misturadas com regex'ami. E validação de data para vários campos, onde a validação diferiu dramaticamente (restrições nos valores mínimo / máximo), etc.
- As telas de entrada de dados são feitas como células collectionView. (Isso foi exigido pelo design!) Por causa disso, a exibição de janelas modais exigia controle preciso sobre o índice selecionado. Eu tive que verificar os campos disponíveis para preenchimento e excluir da lista aqueles que o usuário não deveria ver.
- Para exibir corretamente os dados na tabela, foi necessário fazer alterações nos métodos de modelo de algumas telas. Células como nome e endereço são exibidas na tabela como um único elemento, mas requerem que várias telas pop-up sejam totalmente preenchidas.
Conclusão
Essa experiência nos permitiu na True Engineering implementar rapidamente um aplicativo móvel fácil de manter. A versatilidade permite gerar rapidamente tabelas com diferentes tipos de dados de entrada: criamos 20 janelas em apenas uma semana. Essa abordagem também acelera o processo de teste de aplicativos. Num futuro próximo, reutilizaremos a fábrica pronta para gerar rapidamente novas tabelas e novas funcionalidades.