Melhores abordagens arquiteturais para a criação de aplicativos de rede iOS (clientes REST)

323

Sou desenvolvedor iOS com alguma experiência e essa pergunta é realmente interessante para mim. Vi muitos recursos e materiais diferentes sobre esse tópico, mas ainda assim estou confuso. Qual é a melhor arquitetura para um aplicativo em rede iOS? Quero dizer, estrutura abstrata básica, padrões, que se encaixam em todos os aplicativos de rede, seja um aplicativo pequeno que tenha apenas algumas solicitações de servidor ou um cliente REST complexo. A Apple recomenda usar MVCcomo uma abordagem arquitetural básica para todos os aplicativos iOS, mas MVCnem os MVVMpadrões mais modernos explicam onde colocar o código lógico da rede e como organizá-lo em geral.

Preciso desenvolver algo como MVCS( Sfor Service) e, nessa Servicecamada, colocar todos os ajustes, se tivermos mapeamentos de objetos complexos e persistência, ou mesmo uma implementação de comunicação de rede própria com a API padrão). Mas essa abordagem parece uma sobrecarga para mim. Outra abordagem é ter um singletonAPI solicitações e outra lógica de rede, que em perspectiva pode ser realmente complexa? Depois de fazer algumas pesquisas, encontrei duas abordagens básicas para isso. Aqui , foi recomendado criar uma classe separada para cada solicitação de rede para serviço da Web API(como LoginRequestclasse ou PostCommentRequestclasse e assim por diante), que herda toda a classe abstrata de solicitação básica AbstractBaseRequeste, além disso, criar algum gerenciador de rede global que encapsule o código de rede comum e outras preferências (pode ser AFNetworkingpersonalização ouRestKitAPI despachante ou classe gerente como na primeira abordagem, mas não para criar classes para cada pedido e, em vez de encapsular cada pedido como um método de instância público desta classe de gestão, como: fetchContacts, loginUsermétodos, etc. Então, o que é a melhor e a maneira correta? Existem outras abordagens interessantes que ainda não conheço?

E devo criar outra camada para todo esse material de rede como Service, ouNetworkProvider camada ou qualquer outra coisa em cima da minha MVCarquitetura, ou essa camada deve ser integrada (injetada) nas MVCcamadas existentes, por exemplo Model?

Sei que existem abordagens bonitas ou como esses monstros móveis, como o cliente do Facebook ou o LinkedIn, lidam com a crescente complexidade da lógica da rede?

Eu sei que não há resposta exata e formal para o problema. O objetivo desta pergunta é coletar as abordagens mais interessantes de desenvolvedores iOS experientes . A melhor abordagem sugerida será marcada como aceita e premiada com uma recompensa de reputação; outras serão votadas. É principalmente uma questão teórica e de pesquisa. Quero entender a abordagem arquitetônica básica, abstrata e correta para aplicativos de rede no iOS. Espero uma explicação detalhada de desenvolvedores experientes.

MainstreamDeveloper00
fonte
14
Não é uma pergunta da "lista de compras"? Acabei de ter uma pergunta votada no inferno e encerrada porque foi declarado "qual é o melhor" perguntas do tipo desencadearam um debate pouco construtivo. O que torna essa lista de compras uma boa pergunta digna de votos e recompensas enquanto outras são fechadas?
Alvin Thompson
1
Normalmente, a lógica de rede entra no controlador, o que altera um objeto de modelo e notifica qualquer delegado ou observador.
quellish
1
Perguntas e respostas muito interessantes. Após 4 anos de codificação iOS, e tentando encontrar a maneira mais bonita de adicionar uma camada de rede ao aplicativo. Qual classe deve ter a responsabilidade de gerenciar uma solicitação de rede? As respostas abaixo são realmente pertinentes. Obrigado
darksider
@ JoeBlow isso não é verdade. O setor de aplicativos móveis ainda depende muito das comunicações servidor-cliente.
Scord 06/08

Respostas:

327

I want to understand basic, abstract and correct architectural approach for networking applications in iOS: não existe uma abordagem "melhor" ou "mais correta" para criar uma arquitetura de aplicativo. É muito trabalho criativo. Você deve sempre escolher a arquitetura mais direta e extensível, que ficará clara para qualquer desenvolvedor que começar a trabalhar no seu projeto ou para outros desenvolvedores da sua equipe, mas eu concordo que pode haver um "bom" e um "ruim" "arquitetura.

Você disse: collect the most interesting approaches from experienced iOS developersnão acho que minha abordagem seja a mais interessante ou correta, mas a usei em vários projetos e estou satisfeita com ela. É uma abordagem híbrida das que você mencionou acima e também com melhorias de meus próprios esforços de pesquisa. Sou interessante nos problemas de construção de abordagens, que combinam vários padrões e expressões conhecidas. Penso que muitos padrões empresariais da Fowler podem ser aplicados com sucesso aos aplicativos móveis. Aqui está uma lista dos mais interessantes, que podemos aplicar para criar uma arquitetura de aplicativo iOS ( na minha opinião ): Camada de serviço , Unidade de trabalho , Fachada remota , Objeto de transferência de dados ,Gateway , Supertipo de camada , ou criar sua própria camada leve de mapeamento / persistência de objetos leve, com base em SQLite bruto ou LevelDB, Caso Especial , Modelo de Domínio . Você sempre deve projetar corretamente uma camada de modelo e sempre não esquecer a persistência (isso pode aumentar significativamente o desempenho do seu aplicativo). Você pode usar Core Datapara isso. Mas você não deve esquecer que isso Core Datanão é um ORM ou um banco de dados, mas um gerenciador de gráficos de objetos com persistência como uma boa opção. Portanto, muitas vezes Core Datapode ser muito pesado para suas necessidades e você pode procurar novas soluções, como Realm e Couchbase Lite. Também aconselho você a se familiarizar com o Domain Driven Design e o CQRS .

A princípio, acho que devemos criar outra camada para a rede, porque não queremos controladores de gordura ou modelos pesados ​​e sobrecarregados. Eu não acredito nessas fat model, skinny controllercoisas. Mas acredito na skinny everythingabordagem, porque nenhuma classe deve ser gorda, nunca. Todas as redes geralmente podem ser abstraídas como lógica de negócios; consequentemente, devemos ter outra camada, onde podemos colocá-la. Camada de serviço é o que precisamos:

It encapsulates the application's business logic,  controlling transactions 
and coordinating responses in the implementation of its operations.

Em nosso MVCreino, Service Layerhá algo como um mediador entre o modelo de domínio e os controladores. Há uma variação bastante semelhante dessa abordagem chamada MVCS, onde a Storeé realmente a nossa Servicecamada. Storevends modela instâncias e lida com a rede, o cache etc. Quero mencionar que você não deve escrever toda a lógica de rede e de negócios em sua camada de serviço. Isso também pode ser considerado como um design ruim. Para mais informações, consulte os modelos de domínio Anêmico e Rico . Alguns métodos de serviço e lógica de negócios podem ser manipulados no modelo, por isso será um modelo "rico" (com comportamento).

Eu sempre uso extensivamente duas bibliotecas: AFNetworking 2.0 e ReactiveCocoa . Eu acho que é um item obrigatório para qualquer aplicativo moderno que interaja com a rede e os serviços da web ou contenha lógica complexa da interface do usuário.

ARQUITETURA

Inicialmente, crio uma APIClientclasse geral , que é uma subclasse de AFHTTPSessionManager . Esta é uma força de trabalho de todas as redes no aplicativo: todas as classes de serviço delegam solicitações REST reais a ele. Ele contém todas as personalizações do cliente HTTP, que eu preciso no aplicativo em particular: fixação de SSL, processamento de erros e criação de NSErrorobjetos diretos com motivos de falha detalhados e descrições de todos APIe erros de conexão (nesse caso, o controlador poderá mostrar mensagens corretas para usuário), configurando serializadores de solicitação e resposta, cabeçalhos http e outros itens relacionados à rede. Então eu logicamente dividir todas as solicitações de API em subserviços ou, mais corretamente, microservices : UserSerivces, CommonServices,SecurityServices ,FriendsServicese assim por diante, de acordo com a lógica de negócios que eles implementam. Cada um desses microsserviços é uma classe separada. Eles juntos formam um Service Layer. Essas classes contêm métodos para cada solicitação de API, modelos de domínio de processo e sempre retornam um RACSignalcom o modelo de resposta analisado ou NSErrorpara o responsável pela chamada.

Quero mencionar que se você tiver uma lógica de serialização de modelo complexa - crie outra camada para ela: algo como o Data Mapper, mas mais geral, por exemplo, JSON / XML -> Model mapper. Se você tiver cache: crie-o também como uma camada / serviço separado (você não deve misturar lógica de negócios com cache). Por quê? Porque a camada de armazenamento em cache correta pode ser bastante complexa com suas próprias dicas. As pessoas implementam lógica complexa para obter cache válido e previsível, como, por exemplo, cache monoidal com projeções baseadas em profunctors. Você pode ler sobre esta bela biblioteca chamada Carlos para entender mais. E não esqueça que o Core Data pode realmente ajudá-lo com todos os problemas de cache e permitirá que você escreva menos lógica. Além disso, se você tiver alguma lógica entre os NSManagedObjectContextmodelos de solicitação e servidor, poderá usar RepositórioPadrão de , que separa a lógica que recupera os dados e os mapeia para o modelo de entidade da lógica de negócios que atua no modelo. Portanto, aconselho usar o padrão de repositório mesmo quando você tiver uma arquitetura baseada em dados principais. Repositório pode coisas abstratas, como NSFetchRequest, NSEntityDescription, NSPredicatee assim por diante para os métodos simples, como getou put.

Depois de todas essas ações na camada Serviço, o responsável pela chamada (controlador de exibição) pode executar algumas tarefas assíncronas complexas com a resposta: manipulações de sinal, encadeamento, mapeamento etc. com a ajuda de ReactiveCocoaprimitivos, ou apenas se inscreva e mostre os resultados na exibição . Eu injetar com a injeção de dependência em todas estas classes de serviço meus APIClient, que se traduzirá uma chamada de serviço especial em correspondentes GET, POST, PUT, DELETE, etc. pedido para o terminal REST. Nesse caso, APIClienté passado implicitamente para todos os controladores, você pode explicitar isso explicando APIClientas classes de serviço. Isso pode fazer sentido se você quiser usar personalizações diferentes doAPIClientpara classes de serviço específicas, mas se você, por algum motivo, não quiser cópias extras ou tiver certeza de que sempre usará uma instância específica (sem personalizações) do APIClient- faça dele um singleton, mas NÃO, por favor, NÃO Faça aulas de serviço como singletons.

Em seguida, cada controlador de exibição novamente com o DI injeta a classe de serviço necessária, chama métodos de serviço apropriados e compõe seus resultados com a lógica da interface do usuário. Para injeção de dependência, eu gosto de usar o BloodMagic ou um framework Typhoon mais poderoso . Eu nunca uso singletons, APIManagerWhateveraulas de Deus ou outras coisas erradas. Porque se você ligar para a sua turma WhateverManager, isso indica que você não conhece seu objetivo e é uma má escolha de design . Singletons também é um anti-padrão e, na maioria dos casos (exceto os raros), é uma solução errada . Singleton deve ser considerado apenas se todos os três dos seguintes critérios forem atendidos:

  1. A propriedade da instância única não pode ser razoavelmente atribuída;
  2. Inicialização lenta é desejável;
  3. O acesso global não é previsto de outra forma.

No nosso caso, a propriedade da instância única não é um problema e também não precisamos de acesso global depois de dividirmos o nosso god manager em serviços, porque agora apenas um ou vários controladores dedicados precisam de um serviço específico (por exemplo, UserProfilenecessidades do controlador UserServicese assim por diante) .

Devemos sempre respeitar o Sprincípio no SOLID e usar a separação de preocupações . Portanto, não coloque todos os métodos de serviço e chamadas de rede em uma classe, porque é uma loucura, especialmente se você desenvolver um aplicativo corporativo de grande porte. É por isso que devemos considerar a injeção de dependência e a abordagem de serviços. Considero essa abordagem moderna e pós-OO . Nesse caso, dividimos nosso aplicativo em duas partes: lógica de controle (controladores e eventos) e parâmetros.

Um tipo de parâmetros seria parâmetros comuns de "dados". É o que repassamos funções, manipulamos, modificamos, persistimos, etc. Essas são entidades, agregados, coleções, classes de casos. O outro tipo seria parâmetros de "serviço". São classes que encapsulam a lógica de negócios, permitem a comunicação com sistemas externos, fornecem acesso a dados.

Aqui está um fluxo de trabalho geral da minha arquitetura, por exemplo. Vamos supor que temos um FriendsViewController, que exibe a lista de amigos do usuário e temos a opção de remover dos amigos. Eu crio um método na minha FriendsServicesclasse chamado:

- (RACSignal *)removeFriend:(Friend * const)friend

onde Friendé um objeto de modelo / domínio (ou pode ser apenas um Userobjeto se eles tiverem atributos semelhantes). Underhood este parses método Friendpara NSDictionaryde parâmetros JSON friend_id, name, surname, friend_request_ide assim por diante. Eu sempre uso a biblioteca Mantle para esse tipo de clichê e para a minha camada de modelo (analisando para frente e para trás, gerenciando hierarquias de objetos aninhadas em JSON e assim por diante). Depois de analisá-lo chama APIClient DELETEmétodo para fazer um pedido de descanso efectivo e volta Responseem RACSignalpara o chamador ( FriendsViewControllerno nosso caso) para exibir mensagem apropriada para o usuário ou qualquer outra coisa.

Se nossa aplicação é muito grande, temos que separar nossa lógica ainda mais claramente. Por exemplo, nem sempre é bom misturar Repositoryou modelar a lógica com Serviceuma. Quando descrevi minha abordagem, eu disse que esse removeFriendmétodo deveria estar na Servicecamada, mas se formos mais pedantes, podemos notar que ele pertence melhor Repository. Vamos lembrar o que é Repositório. Eric Evans deu uma descrição precisa em seu livro [DDD]:

Um Repositório representa todos os objetos de um determinado tipo como um conjunto conceitual. Ele age como uma coleção, exceto com capacidade de consulta mais elaborada.

Portanto, a Repositoryé essencialmente uma fachada que usa a semântica no estilo de coleção (Adicionar, Atualizar, Remover) para fornecer acesso aos dados / objetos. É por isso que quando você tem algo como: getFriendsList, getUserGroups, removeFriendvocê pode colocá-lo no Repository, porque coleção-like semântica é bastante clara aqui. E código como:

- (RACSignal *)approveFriendRequest:(FriendRequest * const)request;

é definitivamente uma lógica de negócios, porque está além das CRUDoperações básicas e conecta dois objetos de domínio ( Friende Request), é por isso que deve ser colocada na Servicecamada. Também quero observar: não crie abstrações desnecessárias . Use todas essas abordagens com sabedoria. Porque se você sobrecarregar seu aplicativo com abstrações, isso aumentará sua complexidade acidental, e a complexidade causará mais problemas em sistemas de software do que qualquer outra coisa

Descrevo um exemplo "antigo" do Objective-C, mas essa abordagem pode ser muito fácil de adaptar à linguagem Swift com muito mais melhorias, porque possui recursos mais úteis e açúcar funcional. Eu recomendo usar esta biblioteca: Moya . Permite criar uma APIClientcamada mais elegante (nosso cavalo de batalha, como você se lembra). Agora, nosso APIClientprovedor será um tipo de valor (enum) com extensões em conformidade com protocolos e alavancando a correspondência de padrões de desestruturação. Enumerações rápidas + correspondência de padrões nos permitem criar tipos de dados algébricos como na programação funcional clássica. Nossos microsserviços usarão esse APIClientprovedor aprimorado como na abordagem comum de Objective-C. Para a camada do modelo, em vez de Mantlevocê pode usar biblioteca ObjectMapperou gosto de usar a biblioteca Argo mais elegante e funcional .

Então, descrevi minha abordagem arquitetônica geral, que pode ser adaptada para qualquer aplicativo, eu acho. Pode haver muito mais melhorias, é claro. Eu aconselho você a aprender programação funcional, porque você pode se beneficiar muito disso, mas também não vá muito longe. Eliminar um estado mutável global excessivo, compartilhado, criar um modelo de domínio imutável ou criar funções puras sem efeitos colaterais externos é, geralmente, uma boa prática, e uma nova Swiftlinguagem incentiva isso. Mas lembre-se sempre de que sobrecarregar seu código com padrões funcionais puros pesados, abordagens teóricas de categoria é uma péssima idéia, porque outros desenvolvedores lerão e darão suporte ao seu código e poderão ser frustrados ou assustadores.prismatic profunctorse esse tipo de coisa no seu modelo imutável. A mesma coisa com ReactiveCocoa: não RACifyexagere muito no seu código , pois ele pode se tornar ilegível muito rápido, especialmente para iniciantes. Use-o quando realmente puder simplificar seus objetivos e lógica.

Então read a lot, mix, experiment, and try to pick up the best from different architectural approaches,. É o melhor conselho que posso lhe dar.

Oleksandr Karaberov
fonte
Também é uma abordagem interessante e sólida.
MainstreamDeveloper00
1
@darksider Como já escrevi na minha resposta: "` Eu nunca usar singletons, classe Deus APIManagerWhatever ou outras coisas errado, porque Singleton é um anti-padrão, e na maioria dos casos (exceto os raros) é uma solução errada. ". I don't like singletons. I have an opinion that if you decided to use singletons in your project you should have at least three criteria why you do this (I edited my answer). So I inject them (lazy of course and not each time, but uma vez `) em todos os controladores.
Oleksandr Karaberov
14
Oi @alexander. Você tem algum exemplo de projeto no GitHub? Você descreve uma abordagem muito interessante. Obrigado. Mas sou iniciante no desenvolvimento de Objective-C. E para mim é difícil entender alguns aspectos. Talvez você possa enviar algum projeto de teste no GitHub e fornecer um link?
Denis
1
Olá @AlexanderKaraberov, estou um pouco confuso em relação à explicação da loja que você deu. Suponha que eu possua 5 modelos, para cada um possuo 2 classes, uma que mantém a rede e outro cache de objetos. Agora, devo ter uma classe Store separada para cada modelo que chame a função de rede e classe de cache ou uma única classe Store que tenha todas as funções para cada modelo, para que o controlador sempre acesse arquivos únicos para dados.
meteoros
1
@icodebuster este projecto de demonstração foi me ajudar a entender muitos dos conceitos descritos aqui: github.com/darthpelo/NetworkLayerExample
31

De acordo com o objetivo desta pergunta, gostaria de descrever nossa abordagem de arquitetura.

Abordagem de arquitetura

A arquitetura de nosso aplicativo iOS geral se baseia nos seguintes padrões: Camadas de serviço , MVVM , Vinculação de dados da interface do usuário , Injeção de dependência ; e paradigma de Programação Reativa Funcional .

Podemos dividir um aplicativo voltado para o consumidor típico nas seguintes camadas lógicas:

  • Montagem
  • Modelo
  • Serviços
  • Armazenamento
  • Gerentes
  • Coordenadores
  • UI
  • A infraestrutura

A camada de montagem é um ponto de inicialização do nosso aplicativo. Ele contém um contêiner de injeção de dependência e declarações dos objetos do aplicativo e suas dependências. Essa camada também pode conter a configuração do aplicativo (URLs, chaves de serviços de terceiros e assim por diante). Para esse fim, usamos a biblioteca Typhoon .

A camada de modelo contém classes, validações, mapeamentos de modelos de domínio. Usamos a biblioteca Mantle para mapear nossos modelos: suporta serialização / desserialização em JSONformato e NSManagedObjectmodelos. Para validação e representação forma de nossos modelos usamos FXForms e FXModelValidation bibliotecas.

A camada de serviços declara os serviços que usamos para interagir com sistemas externos, a fim de enviar ou receber dados representados em nosso modelo de domínio. Geralmente, temos serviços de comunicação com APIs do servidor (por entidade), serviços de mensagens (como o PubNub ), serviços de armazenamento (como o Amazon S3), etc. Basicamente, os serviços envolvem objetos fornecidos pelos SDKs (por exemplo, o PubNub SDK) ou implementam sua própria comunicação lógica. Para redes em geral, usamos a biblioteca AFNetworking .

O objetivo da camada de armazenamento é organizar o armazenamento de dados local no dispositivo. Usamos Core Data ou Realm para isso (ambos têm prós e contras, a decisão sobre o que usar é baseada em especificações concretas). Para a configuração do Core Data, usamos a biblioteca MDMCoreData e várias classes - armazenamentos - (semelhantes aos serviços) que fornecem acesso ao armazenamento local para cada entidade. Para o Realm, usamos apenas armazenamentos semelhantes para ter acesso ao armazenamento local.

A camada de gerentes é um local onde nossas abstrações / invólucros vivem.

Em uma função de gerente, pode ser:

  • Credentials Manager com suas diferentes implementações (keychain, NSDefaults, ...)
  • Current Session Manager, que sabe como manter e fornecer a sessão atual do usuário
  • Capture Pipeline que fornece acesso a dispositivos de mídia (gravação de vídeo, áudio, fotografia)
  • Gerenciador BLE que fornece acesso a serviços e periféricos bluetooth
  • Gerente de localização geográfica
  • ...

Portanto, no papel de gerente, pode haver qualquer objeto que implemente a lógica de um aspecto ou preocupação específica necessária para o funcionamento do aplicativo.

Tentamos evitar Singletons, mas essa camada é um lugar onde eles moram, se necessário.

A camada Coordenadores fornece objetos que dependem de objetos de outras camadas (Serviço, Armazenamento, Modelo) para combinar sua lógica em uma sequência de trabalho necessária para determinado módulo (recurso, tela, história do usuário ou experiência do usuário). Geralmente, encadeia operações assíncronas e sabe como reagir aos casos de sucesso e falha. Como exemplo, você pode imaginar um recurso de mensagens e o MessagingCoordinatorobjeto correspondente . A manipulação da operação de envio de mensagens pode ser assim:

  1. Validar mensagem (camada de modelo)
  2. Salvar mensagem localmente (armazenamento de mensagens)
  3. Carregar anexo de mensagem (serviço amazon s3)
  4. Atualizar status das mensagens e URLs de anexos e salvar mensagens localmente (armazenamento de mensagens)
  5. Serializar mensagem para o formato JSON (camada de modelo)
  6. Publicar mensagem no PubNub (serviço PubNub)
  7. Atualize o status e os atributos das mensagens e salve-os localmente (armazenamento de mensagens)

Em cada uma das etapas acima, um erro é tratado de forma correspondente.

A camada da interface do usuário consiste nas seguintes subcamadas:

  1. ViewModels
  2. ViewControllers
  3. Visualizações

Para evitar os Massive View Controllers, usamos o padrão MVVM e implementamos a lógica necessária para a apresentação da interface do usuário no ViewModels. Um ViewModel geralmente possui coordenadores e gerentes como dependências. ViewModels usados ​​pelo ViewControllers e alguns tipos de Views (por exemplo, células de exibição de tabela). A cola entre os ViewControllers e os ViewModels é o padrão Data Binding and Command. Para possibilitar essa cola, usamos a biblioteca ReactiveCocoa .

Também usamos ReactiveCocoa e seu RACSignalconceito como uma interface e retornando o tipo de valor de todos os métodos de coordenadores, serviços e armazenamentos. Isso nos permite encadear operações, executá-las paralelamente ou em série, e muitas outras coisas úteis fornecidas pelo ReactiveCocoa.

Tentamos implementar nosso comportamento de interface do usuário de maneira declarativa. Ligação de dados e layout automático ajudam muito a atingir esse objetivo.

A camada de infraestrutura contém todos os auxiliares, extensões, utilitários necessários para o trabalho do aplicativo.


Essa abordagem funciona bem para nós e para os tipos de aplicativos que costumamos criar. Mas você deve entender que esta é apenas uma abordagem subjetiva que deve ser adaptada / alterada para o objetivo da equipe concreta.

Espero que isso ajude você!

Além disso, você pode encontrar mais informações sobre o processo de desenvolvimento do iOS nesta postagem do blog Desenvolvimento como serviço do iOS

Alex Petropavlovsky
fonte
Começou a gostar dessa arquitetura há alguns meses, graças a Alex por compartilhá-la! Gostaria de experimentá-lo com o RxSwift em um futuro próximo!
ingaham
18

Como todos os aplicativos iOS são diferentes, acho que existem abordagens diferentes a serem consideradas aqui, mas geralmente eu faço o seguinte:
Crie uma classe de gerente central (singleton) para lidar com todas as solicitações de API (geralmente denominadas APICommunicator) e todo método de instância é uma chamada de API . E existe um método central (não público):

-(RACSignal *)sendGetToServerToSubPath:(NSString *)path withParameters:(NSDictionary *)params;

Para o registro, eu uso duas bibliotecas / estruturas principais, ReactiveCocoa e AFNetworking. O ReactiveCocoa lida perfeitamente com as respostas assíncronas da rede, você pode fazer (sendNext :, sendError :, etc.).
Esse método chama a API, obtém os resultados e os envia pelo RAC no formato 'bruto' (como NSArray, o que a AFNetworking retorna).
Em seguida, um método como o getStuffList:chamado método acima assina seu sinal, analisa os dados brutos em objetos (com algo como Motis) e envia os objetos um a um para o chamador ( getStuffList:e métodos semelhantes também retornam um sinal que o controlador pode assinar) )
O controlador inscrito recebe os objetos pelo subscribeNext:bloco e os manipula.

Tentei de várias maneiras em aplicativos diferentes, mas este funcionou da melhor maneira possível, então eu tenho usado isso em alguns aplicativos recentemente, ele se encaixa em projetos pequenos e grandes e é fácil estender e manter, se algo precisar ser modificado.
Espero que isso ajude, gostaria de ouvir as opiniões dos outros sobre minha abordagem e talvez como os outros pensam que isso poderia ser melhorado.

Rickye
fonte
2
Obrigado pela resposta +1. Boa abordagem. Eu deixo a pergunta. Pode ser que tenhamos outras abordagens de outros desenvolvedores.
MainstreamDeveloper00
1
Gosto de uma variação dessa abordagem - uso um gerenciador de API central que cuida da mecânica de comunicação com a API. No entanto, tento fazer toda a funcionalidade exposta nos meus objetos de modelo. Os modelos fornecerão métodos como + (void)getAllUsersWithSuccess:(void(^)(NSArray*))success failure:(void(^)(NSError*))failure;e - (void)postWithSuccess:(void(^)(instancetype))success failure:(void(^)(NSError*))failure;que fazem os preparativos necessários e depois chamarão o gerente de API.
Jsadler
1
Essa abordagem é simples, mas à medida que o número de API está aumentando, fica mais difícil manter o gerenciador de API singleton. E toda nova API adicionada se relacionará com o gerente, independentemente de qual módulo essa API pertence. Tente usar github.com/kevin0571/STNetTaskQueue para gerenciar as solicitações de API.
Kevin
Além do ponto de por que você está anunciando sua biblioteca, o mais longe possível da minha solução e muito mais complicada, tentei essa abordagem em inúmeros projetos, tanto pequenos quanto grandes, como mencionado, e tenho usado exatamente o mesmo desde que escrevi esta resposta. Com convenções de nomenclatura inteligentes, não é difícil de manter.
Rickye
8

Na minha situação, geralmente estou usando a biblioteca ResKit para configurar a camada de rede. Ele fornece análise fácil de usar. Isso reduz meu esforço em configurar o mapeamento para diferentes respostas e outras coisas.

Eu adiciono apenas algum código para configurar o mapeamento automaticamente. Eu defino a classe base para meus modelos (não protocolo por causa de muito código para verificar se algum método está implementado ou não e menos código nos modelos):

MappableEntry.h

@interface MappableEntity : NSObject

+ (NSArray*)pathPatterns;
+ (NSArray*)keyPathes;
+ (NSArray*)fieldsArrayForMapping;
+ (NSDictionary*)fieldsDictionaryForMapping;
+ (NSArray*)relationships;

@end

MappableEntry.m

@implementation MappableEntity

+(NSArray*)pathPatterns {
    return @[];
}

+(NSArray*)keyPathes {
    return nil;
}

+(NSArray*)fieldsArrayForMapping {
    return @[];
}

+(NSDictionary*)fieldsDictionaryForMapping {
    return @{};
}

+(NSArray*)relationships {
    return @[];
}

@end

Relacionamentos são objetos que representam objetos aninhados em resposta:

RelationshipObject.h

@interface RelationshipObject : NSObject

@property (nonatomic,copy) NSString* source;
@property (nonatomic,copy) NSString* destination;
@property (nonatomic) Class mappingClass;

+(RelationshipObject*)relationshipWithKey:(NSString*)key andMappingClass:(Class)mappingClass;
+(RelationshipObject*)relationshipWithSource:(NSString*)source destination:(NSString*)destination andMappingClass:(Class)mappingClass;

@end

RelationshipObject.m

@implementation RelationshipObject

+(RelationshipObject*)relationshipWithKey:(NSString*)key andMappingClass:(Class)mappingClass {
    RelationshipObject* object = [[RelationshipObject alloc] init];
    object.source = key;
    object.destination = key;
    object.mappingClass = mappingClass;
    return object;
}

+(RelationshipObject*)relationshipWithSource:(NSString*)source destination:(NSString*)destination andMappingClass:(Class)mappingClass {
    RelationshipObject* object = [[RelationshipObject alloc] init];
    object.source = source;
    object.destination = destination;
    object.mappingClass = mappingClass;
    return object;
}

@end

Então, estou configurando o mapeamento para o RestKit assim:

ObjectMappingInitializer.h

@interface ObjectMappingInitializer : NSObject

+(void)initializeRKObjectManagerMapping:(RKObjectManager*)objectManager;

@end

ObjectMappingInitializer.m

@interface ObjectMappingInitializer (Private)

+ (NSArray*)mappableClasses;

@end

@implementation ObjectMappingInitializer

+(void)initializeRKObjectManagerMapping:(RKObjectManager*)objectManager {

    NSMutableDictionary *mappingObjects = [NSMutableDictionary dictionary];

    // Creating mappings for classes
    for (Class mappableClass in [self mappableClasses]) {
        RKObjectMapping *newMapping = [RKObjectMapping mappingForClass:mappableClass];
        [newMapping addAttributeMappingsFromArray:[mappableClass fieldsArrayForMapping]];
        [newMapping addAttributeMappingsFromDictionary:[mappableClass fieldsDictionaryForMapping]];
        [mappingObjects setObject:newMapping forKey:[mappableClass description]];
    }

    // Creating relations for mappings
    for (Class mappableClass in [self mappableClasses]) {
        RKObjectMapping *mapping = [mappingObjects objectForKey:[mappableClass description]];
        for (RelationshipObject *relation in [mappableClass relationships]) {
            [mapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:relation.source toKeyPath:relation.destination withMapping:[mappingObjects objectForKey:[relation.mappingClass description]]]];
        }
    }

    // Creating response descriptors with mappings
    for (Class mappableClass in [self mappableClasses]) {
        for (NSString* pathPattern in [mappableClass pathPatterns]) {
            if ([mappableClass keyPathes]) {
                for (NSString* keyPath in [mappableClass keyPathes]) {
                    [objectManager addResponseDescriptor:[RKResponseDescriptor responseDescriptorWithMapping:[mappingObjects objectForKey:[mappableClass description]] method:RKRequestMethodAny pathPattern:pathPattern keyPath:keyPath statusCodes:RKStatusCodeIndexSetForClass(RKStatusCodeClassSuccessful)]];
                }
            } else {
                [objectManager addResponseDescriptor:[RKResponseDescriptor responseDescriptorWithMapping:[mappingObjects objectForKey:[mappableClass description]] method:RKRequestMethodAny pathPattern:pathPattern keyPath:nil statusCodes:RKStatusCodeIndexSetForClass(RKStatusCodeClassSuccessful)]];
            }
        }
    }

    // Error Mapping
    RKObjectMapping *errorMapping = [RKObjectMapping mappingForClass:[Error class]];
    [errorMapping addAttributeMappingsFromArray:[Error fieldsArrayForMapping]];
    for (NSString *pathPattern in Error.pathPatterns) {
        [[RKObjectManager sharedManager] addResponseDescriptor:[RKResponseDescriptor responseDescriptorWithMapping:errorMapping method:RKRequestMethodAny pathPattern:pathPattern keyPath:nil statusCodes:RKStatusCodeIndexSetForClass(RKStatusCodeClassClientError)]];
    }
}

@end

@implementation ObjectMappingInitializer (Private)

+ (NSArray*)mappableClasses {
    return @[
        [FruiosPaginationResults class],
        [FruioItem class],
        [Pagination class],
        [ContactInfo class],
        [Credentials class],
        [User class]
    ];
}

@end

Alguns exemplos de implementação de MappableEntry:

User.h

@interface User : MappableEntity

@property (nonatomic) long userId;
@property (nonatomic, copy) NSString *username;
@property (nonatomic, copy) NSString *email;
@property (nonatomic, copy) NSString *password;
@property (nonatomic, copy) NSString *token;

- (instancetype)initWithUsername:(NSString*)username email:(NSString*)email password:(NSString*)password;

- (NSDictionary*)registrationData;

@end

Usuário.m

@implementation User

- (instancetype)initWithUsername:(NSString*)username email:(NSString*)email password:(NSString*)password {
    if (self = [super init]) {
        self.username = username;
        self.email = email;
        self.password = password;
    }
    return self;
}

- (NSDictionary*)registrationData {
    return @{
        @"username": self.username,
        @"email": self.email,
        @"password": self.password
    };
}

+ (NSArray*)pathPatterns {
    return @[
        [NSString stringWithFormat:@"/api/%@/users/register", APIVersionString],
        [NSString stringWithFormat:@"/api/%@/users/login", APIVersionString]
    ];
}

+ (NSArray*)fieldsArrayForMapping {
    return @[ @"username", @"email", @"password", @"token" ];
}

+ (NSDictionary*)fieldsDictionaryForMapping {
    return @{ @"id": @"userId" };
}

@end

Agora, sobre o agrupamento de solicitações:

Eu tenho um arquivo de cabeçalho com definição de blocos, para reduzir o comprimento da linha em todas as classes APIRequest:

APICallbacks.h

typedef void(^SuccessCallback)();
typedef void(^SuccessCallbackWithObjects)(NSArray *objects);
typedef void(^ErrorCallback)(NSError *error);
typedef void(^ProgressBlock)(float progress);

E exemplo da minha classe APIRequest que estou usando:

LoginAPI.h

@interface LoginAPI : NSObject

- (void)loginWithCredentials:(Credentials*)credentials onSuccess:(SuccessCallbackWithObjects)onSuccess onError:(ErrorCallback)onError;

@end

LoginAPI.m

@implementation LoginAPI

- (void)loginWithCredentials:(Credentials*)credentials onSuccess:(SuccessCallbackWithObjects)onSuccess onError:(ErrorCallback)onError {
    [[RKObjectManager sharedManager] postObject:nil path:[NSString stringWithFormat:@"/api/%@/users/login", APIVersionString] parameters:[credentials credentialsData] success:^(RKObjectRequestOperation *operation, RKMappingResult *mappingResult) {
        onSuccess(mappingResult.array);
    } failure:^(RKObjectRequestOperation *operation, NSError *error) {
        onError(error);
    }];
}

@end

E tudo o que você precisa fazer no código, basta inicializar o objeto API e chamá-lo sempre que precisar:

SomeViewController.m

@implementation SomeViewController {
    LoginAPI *_loginAPI;
    // ...
}

- (void)viewDidLoad {
    [super viewDidLoad];

    _loginAPI = [[LoginAPI alloc] init];
    // ...
}

// ...

- (IBAction)signIn:(id)sender {
    [_loginAPI loginWithCredentials:_credentials onSuccess:^(NSArray *objects) {
        // Success Block
    } onError:^(NSError *error) {
        // Error Block
    }];
}

// ...

@end

Meu código não é perfeito, mas é fácil definir uma vez e usar em diferentes projetos. Se é interessante para alguém, mb eu poderia gastar algum tempo e criar uma solução universal para isso em algum lugar no GitHub e CocoaPods.

Andrew Cherkashyn
fonte
7

Na minha opinião, toda a arquitetura de software é orientada pela necessidade. Se isso é para fins pessoais ou de aprendizado, decida o objetivo principal e faça com que ele direcione a arquitetura. Se este é um trabalho contratado, a necessidade do negócio é fundamental. O truque é não deixar que coisas brilhantes o distraiam das necessidades reais. Acho isso difícil de fazer. Sempre existem novas coisas brilhantes aparecendo neste negócio e muitas delas não são úteis, mas você nem sempre pode dizer isso com antecedência. Concentre-se na necessidade e esteja disposto a abandonar as más escolhas, se puder.

Por exemplo, recentemente fiz um protótipo rápido de um aplicativo de compartilhamento de fotos para uma empresa local. Como a necessidade comercial era fazer algo rápido e sujo, a arquitetura acabou sendo um código iOS para abrir uma câmera e um código de rede anexado a um botão Enviar que carregava a imagem em uma loja S3 e gravava em um domínio SimpleDB. O código era trivial e o custo mínimo, e o cliente possui uma coleção de fotos escalável acessível pela web com chamadas REST. Barato e burro, o aplicativo tinha muitas falhas e bloqueava a interface do usuário de vez em quando, mas seria um desperdício fazer mais por um protótipo e permitir que eles implantem em sua equipe e gerem milhares de imagens de teste facilmente, sem desempenho ou escalabilidade preocupações. Arquitetura de baixa qualidade, mas se encaixa perfeitamente na necessidade e no custo.

Outro projeto envolveu a implementação de um banco de dados local seguro que sincroniza com o sistema da empresa em segundo plano quando a rede está disponível. Criei um sincronizador em segundo plano que usava o RestKit, pois parecia ter tudo o que eu precisava. Mas eu tive que escrever tanto código personalizado para o RestKit lidar com JSON idiossincrático que eu poderia ter feito tudo mais rápido escrevendo minhas próprias transformações JSON em CoreData. No entanto, o cliente queria trazer esse aplicativo internamente e achei que o RestKit seria semelhante às estruturas usadas em outras plataformas. Estou esperando para ver se foi uma boa decisão.

Novamente, o problema para mim é focar na necessidade e deixar isso determinar a arquitetura. Eu tento como o inferno evitar o uso de pacotes de terceiros, pois eles trazem custos que só aparecem após o aplicativo estar em campo por um tempo. Eu tento evitar criar hierarquias de classe, pois elas raramente valem a pena. Se eu posso escrever algo em um período de tempo razoável, em vez de adotar um pacote que não se encaixa perfeitamente, eu faço. Meu código está bem estruturado para depuração e comentado adequadamente, mas raramente são pacotes de terceiros. Com isso dito, acho o AF Networking muito útil para ignorar e bem estruturado, bem comentado e mantido, e eu o uso muito! O RestKit cobre muitos casos comuns, mas sinto que estive em uma briga quando o uso, e a maioria das fontes de dados que encontro são cheias de peculiaridades e problemas que são melhor tratados com código personalizado. Nos meus últimos aplicativos, apenas uso os conversores JSON internos e escrevo alguns métodos utilitários.

Um padrão que eu sempre uso é tirar as chamadas de rede do segmento principal. Os últimos 4-5 aplicativos que fiz configuraram uma tarefa de timer em segundo plano usando o dispatch_source_create, que acorda de vez em quando e executa tarefas de rede conforme necessário. Você precisa fazer algum trabalho de segurança de encadeamento e garantir que o código de modificação da interface do usuário seja enviado ao encadeamento principal. Também ajuda a fazer a sua integração / inicialização de forma que o usuário não se sinta sobrecarregado ou atrasado. Até agora, isso tem funcionado bastante bem. Eu sugiro olhar para essas coisas.

Por fim, acho que, à medida que trabalhamos mais e à medida que o sistema operacional evolui, tendemos a desenvolver melhores soluções. Levei anos para superar minha crença de que tenho de seguir padrões e projetos que outras pessoas afirmam serem obrigatórios. Se estou trabalhando em um contexto em que isso faz parte da religião local, quero dizer as melhores práticas departamentais de engenharia, então sigo os costumes à risca, é para isso que eles estão me pagando. Mas raramente acho que seguir padrões e desenhos mais antigos é a solução ideal. Eu sempre tento olhar para a solução através do prisma das necessidades dos negócios e construir a arquitetura para corresponder a ela e manter as coisas o mais simples possível. Quando sinto que não há o suficiente, mas tudo funciona corretamente, estou no caminho certo.

Fran K.
fonte
4

Eu uso a abordagem que recebi daqui: https://github.com/Constantine-Fry/Foursquare-API-v2 . Reescrevi essa biblioteca no Swift e você pode ver a abordagem arquitetural dessas partes do código:

typealias OpertaionCallback = (success: Bool, result: AnyObject?) -> ()

class Foursquare{
    var authorizationCallback: OperationCallback?
    var operationQueue: NSOperationQueue
    var callbackQueue: dispatch_queue_t?

    init(){
        operationQueue = NSOperationQueue()
        operationQueue.maxConcurrentOperationCount = 7;
        callbackQueue = dispatch_get_main_queue();
    }

    func checkIn(venueID: String, shout: String, callback: OperationCallback) -> NSOperation {
        let parameters: Dictionary <String, String> = [
            "venueId":venueID,
            "shout":shout,
            "broadcast":"public"]
        return self.sendRequest("checkins/add", parameters: parameters, httpMethod: "POST", callback: callback)
    }

    func sendRequest(path: String, parameters: Dictionary <String, String>, httpMethod: String, callback:OperationCallback) -> NSOperation{
        let url = self.constructURL(path, parameters: parameters)
        var request = NSMutableURLRequest(URL: url)
        request.HTTPMethod = httpMethod
        let operation = Operation(request: request, callbackBlock: callback, callbackQueue: self.callbackQueue!)
        self.operationQueue.addOperation(operation)
        return operation
    }

    func constructURL(path: String, parameters: Dictionary <String, String>) -> NSURL {
        var parametersString = kFSBaseURL+path
        var firstItem = true
        for key in parameters.keys {
            let string = parameters[key]
            let mark = (firstItem ? "?" : "&")
            parametersString += "\(mark)\(key)=\(string)"
            firstItem = false
        }
    return NSURL(string: parametersString.stringByAddingPercentEscapesUsingEncoding(NSUTF8StringEncoding))
    }
}

class Operation: NSOperation {
    var callbackBlock: OpertaionCallback
    var request: NSURLRequest
    var callbackQueue: dispatch_queue_t

    init(request: NSURLRequest, callbackBlock: OpertaionCallback, callbackQueue: dispatch_queue_t) {
        self.request = request
        self.callbackBlock = callbackBlock
        self.callbackQueue = callbackQueue
    }

    override func main() {
        var error: NSError?
        var result: AnyObject?
        var response: NSURLResponse?

        var recievedData: NSData? = NSURLConnection.sendSynchronousRequest(self.request, returningResponse: &response, error: &error)

        if self.cancelled {return}

        if recievedData{
            result = NSJSONSerialization.JSONObjectWithData(recievedData, options: nil, error: &error)
            if result != nil {
                if result!.isKindOfClass(NSClassFromString("NSError")){
                    error = result as? NSError
            }
        }

        if self.cancelled {return}

        dispatch_async(self.callbackQueue, {
            if (error) {
                self.callbackBlock(success: false, result: error!);
            } else {
                self.callbackBlock(success: true, result: result!);
            }
            })
    }

    override var concurrent:Bool {get {return true}}
}

Basicamente, há a subclasse NSOperation que faz o NSURLRequest, analisa a resposta JSON e adiciona o bloco de retorno de chamada com o resultado à fila. A classe principal da API constrói NSURLRequest, inicializa a subclasse NSOperation e a adiciona à fila.

bzz
fonte
3

Usamos algumas abordagens, dependendo da situação. Para a maioria das coisas, o AFNetworking é a abordagem mais simples e robusta, na qual você pode definir cabeçalhos, fazer upload de dados com várias partes, usar GET, POST, PUT & DELETE e existem várias categorias adicionais para o UIKit que permitem, por exemplo, definir uma imagem de um URL. Em um aplicativo complexo com muitas chamadas, às vezes resumimos isso em um método de conveniência próprio, que seria algo como:

-(void)makeRequestToUrl:(NSURL *)url withParameters:(NSDictionary *)parameters success:(void (^)(id responseObject))success failure:(void (^)(AFHTTPRequestOperation *operation, NSError *error))failure;

Existem algumas situações em que o AFNetworking não é apropriado, por exemplo, onde você está criando uma estrutura ou outro componente de biblioteca, pois o AFNetworking já pode estar em outra base de código. Nessa situação, você usaria um NSMutableURLRequest em linha se estiver fazendo uma única chamada ou abstraído para uma classe de solicitação / resposta.

Martin
fonte
Para mim, esta é a melhor e mais clara resposta, felicidades. "É simples assim". @martin, pessoalmente, apenas usamos NSMutableURLRequest o tempo todo; existe algum motivo real para usar o AFNetworking?
Fattie
AFNetworking é apenas realmente conveniente. Para mim, os bloqueios de sucesso e falha valem a pena, pois facilitam o gerenciamento do código. Eu concordo que às vezes é um exagero total.
Martin Martin
Um ponto excelente nos blocos, obrigado por isso. Eu acho que a natureza específica disso tudo mudará com Swift.
Fattie
2

Evito singletons ao projetar meus aplicativos. Eles são típicos para muitas pessoas, mas acho que você pode encontrar soluções mais elegantes em outros lugares. Normalmente, o que faço é criar minhas entidades no CoreData e, em seguida, colocar meu código REST em uma categoria NSManagedObject. Se, por exemplo, eu desejasse criar e postar um novo usuário, eu faria o seguinte:

User* newUser = [User createInManagedObjectContext:managedObjectContext];
[newUser postOnSuccess:^(...) { ... } onFailure:^(...) { ... }];

Eu uso o RESTKit para o mapeamento de objetos e o inicializo na inicialização. Acho que o roteamento de todas as suas chamadas através de um singleton é uma perda de tempo e adiciona um monte de clichê desnecessário.

No NSManagedObject + Extensions.m:

+ (instancetype)createInContext:(NSManagedObjectContext*)context
{
    NSAssert(context.persistentStoreCoordinator.managedObjectModel.entitiesByName[[self entityName]] != nil, @"Entity with name %@ not found in model. Is your class name the same as your entity name?", [self entityName]);
    return [NSEntityDescription insertNewObjectForEntityForName:[self entityName] inManagedObjectContext:context];
}

No NSManagedObject + Networking.m:

- (void)getOnSuccess:(RESTSuccess)onSuccess onFailure:(RESTFailure)onFailure blockInput:(BOOL)blockInput
{
    [[RKObjectManager sharedManager] getObject:self path:nil parameters:nil success:onSuccess failure:onFailure];
    [self handleInputBlocking:blockInput];
}

Por que adicionar classes auxiliares extras quando você pode estender a funcionalidade de uma classe base comum por meio de categorias?

Se você estiver interessado em informações mais detalhadas sobre minha solução, entre em contato. Estou feliz em compartilhar.

Sandy Chapman
fonte
3
Definitivamente, estaria interessado em ler sobre essa abordagem com mais detalhes em uma postagem do blog.
Danyal Aytekin 24/03
0

De uma perspectiva de design puramente de classe, você geralmente terá algo parecido com isto:

  • Seus controladores de visão que controlam uma ou mais visualizações
  • Classe de modelo de dados - Depende realmente de quantas entidades distintas reais você está lidando e como elas estão relacionadas.

    Por exemplo, se você tiver uma matriz de itens a serem exibidos em quatro representações diferentes (lista, gráfico, gráfico etc.), você terá uma classe de modelo de dados para a lista de itens, e mais uma para um item. A lista da classe do item será compartilhada por quatro controladores de exibição - todos filhos de um controlador de barra de guias ou de um controlador de navegação.

    As classes de modelo de dados serão úteis não apenas na exibição de dados, mas também na serialização delas, na qual cada uma delas pode expor seu próprio formato de serialização através dos métodos de exportação JSON / XML / CSV (ou qualquer outra coisa).

  • É importante entender que você também precisa de classes do construtor de solicitações de API que mapeiam diretamente com os terminais da API REST. Digamos que você tenha uma API que efetue login no usuário - portanto, sua classe de construtor de API de login criará uma carga útil POST JSON para a API de login. Em outro exemplo, uma classe de construtor de solicitação de API para a lista de itens de catálogo API criará a sequência de consultas GET para a API correspondente e acionará a consulta REST GET.

    Essas classes do construtor de solicitações de API geralmente recebem dados dos controladores de exibição e também transmitem os mesmos dados para exibir os controladores para atualização da interface do usuário / outras operações. Os controladores de exibição decidirão como atualizar os objetos do Modelo de Dados com esses dados.

  • Finalmente, o coração do cliente REST - dados API classe fetcher que é alheio a todos os tipos de API solicita seus aplicativos marcas. É provável que essa classe seja um singleton, mas, como outros apontaram, não precisa ser um singleton.

    Observe que o link é apenas uma implementação típica e não leva em consideração cenários como sessão, cookies etc., mas é o suficiente para você continuar sem usar estruturas de terceiros.

Nirav Bhatt
fonte
0

Essa pergunta já tem muitas respostas excelentes e abrangentes, mas acho que preciso mencionar isso, já que ninguém mais tem.

Alamofire para Swift. https://github.com/Alamofire/Alamofire

Ele foi criado pelas mesmas pessoas que a AFNetworking, mas foi projetado mais diretamente com o Swift em mente.

matt.writes.code
fonte
0

Eu acho que agora o projeto médio usa a arquitetura MVVM e o Big project usa a arquitetura VIPER e tenta alcançar

  • Programação orientada a protocolo
  • Padrões de design de software
  • Princípio VENDIDO
  • Programação genérica
  • Não se repita (SECO)

Abordagens arquitetônicas para a criação de aplicativos de rede iOS (clientes REST)

A preocupação com a separação de códigos limpos e legíveis evita a duplicação:

import Foundation
enum DataResponseError: Error {
    case network
    case decoding

    var reason: String {
        switch self {
        case .network:
            return "An error occurred while fetching data"
        case .decoding:
            return "An error occurred while decoding data"
        }
    }
}

extension HTTPURLResponse {
    var hasSuccessStatusCode: Bool {
        return 200...299 ~= statusCode
    }
}

enum Result<T, U: Error> {
    case success(T)
    case failure(U)
}

inversão de dependência

 protocol NHDataProvider {
        func fetchRemote<Model: Codable>(_ val: Model.Type, url: URL, completion: @escaping (Result<Codable, DataResponseError>) -> Void)
    }

Responsável principal:

  final class NHClientHTTPNetworking : NHDataProvider {

        let session: URLSession

        init(session: URLSession = URLSession.shared) {
            self.session = session
        }

        func fetchRemote<Model: Codable>(_ val: Model.Type, url: URL,
                             completion: @escaping (Result<Codable, DataResponseError>) -> Void) {
            let urlRequest = URLRequest(url: url)
            session.dataTask(with: urlRequest, completionHandler: { data, response, error in
                guard
                    let httpResponse = response as? HTTPURLResponse,
                    httpResponse.hasSuccessStatusCode,
                    let data = data
                    else {
                        completion(Result.failure(DataResponseError.network))
                        return
                }
                guard let decodedResponse = try? JSONDecoder().decode(Model.self, from: data) else {
                    completion(Result.failure(DataResponseError.decoding))
                    return
                }
                completion(Result.success(decodedResponse))
            }).resume()
        }
    }

Você encontrará aqui a arquitetura GitHub MVVM com o restante API Swift Project

Nazmul Hasan
fonte
0

Na engenharia de software móvel, os mais utilizados são os padrões Clean Architecture + MVVM e Redux.

Arquitetura Limpa + MVVM consistem em 3 camadas: Domínio, Apresentação, Camadas de Dados. Onde a camada de apresentação e a camada de repositórios de dados dependem da camada de domínio:

Presentation Layer -> Domain Layer <- Data Repositories Layer

E a camada de apresentação consiste em ViewModels e Views (MVVM):

Presentation Layer (MVVM) = ViewModels + Views
Domain Layer = Entities + Use Cases + Repositories Interfaces
Data Repositories Layer = Repositories Implementations + API (Network) + Persistence DB

Neste artigo, há uma descrição mais detalhada de Clean Architecture + MVVM https://tech.olx.com/clean-architecture-and-mvvm-on-ios-c9d167d9f5b3

Oleh Kudinov
fonte