Escrevendo sua camada de rede no Swift: abordagem orientada a protocolo



Agora, quase 100% dos aplicativos usam redes, para que todos se deparem com a organização e o uso da camada de rede. Existem duas abordagens principais para solucionar esse problema: o uso de bibliotecas de terceiros ou a sua própria implementação da camada de rede. Neste artigo, consideraremos a segunda opção e tentaremos implementar uma camada de rede usando todos os recursos mais recentes da linguagem, usando protocolos e enumerações. Isso salvará o projeto de dependências desnecessárias na forma de bibliotecas adicionais. Aqueles que já viram Moya reconhecerão imediatamente muitos detalhes semelhantes na implementação e uso, do jeito que são, só que desta vez faremos sozinhos sem tocar em Moya e Alamofire.


Neste guia, veremos como implementar uma camada de rede no Swift puro, sem usar bibliotecas de terceiros. Depois de revisar este artigo, seu código se tornará

  • protocolo orientado
  • fácil de usar
  • fácil de usar
  • tipo seguro
  • para endpoints enums serão usados


Abaixo está um exemplo de como o uso da nossa camada de rede cuidará de sua implementação:



Simplesmente escrevendo router.request (. E usando todo o poder das enumerações, veremos todas as opções de consulta possíveis e seus parâmetros.

Primeiro, um pouco sobre a estrutura do projeto

Sempre que você cria algo novo, e para poder entender tudo facilmente no futuro, é muito importante organizar e estruturar tudo corretamente. Acredito que uma estrutura de pastas organizada adequadamente é um detalhe importante ao construir a arquitetura do aplicativo. Para que possamos organizar tudo corretamente em pastas, vamos criá-las antecipadamente. Isso se parecerá com a estrutura geral de pastas no projeto:



Protocolo do tipo de ponto final

Primeiro de tudo, precisamos definir nosso protocolo EndPointType . Este protocolo conterá todas as informações necessárias para configurar a solicitação. O que é uma solicitação (ponto de extremidade)? Em essência, é um URLRequest com todos os componentes relacionados, como cabeçalhos, parâmetros de solicitação, corpo da solicitação. O protocolo EndPointType é a parte mais importante da nossa implementação da camada de rede. Vamos criar um arquivo e denomine -o EndPointType . Coloque esse arquivo na pasta Serviço (não na pasta EndPoint, por quê - ficará claro mais tarde)



Protocolos HTTP

Nosso EndPointType contém vários protocolos que precisamos para criar uma solicitação. Vamos ver o que são esses protocolos.

HTTPMethod

Crie um arquivo, nomeie-o HTTPMethod e coloque-o na pasta Serviço. Esta listagem será usada para definir o método HTTP de nossa solicitação.



HTTPTask
Crie um arquivo, chame -o de HTTPTask e coloque-o na pasta Serviço. HTTPTask é responsável por configurar os parâmetros de uma solicitação específica. Você pode adicionar quantas opções de consulta diferentes forem necessárias, mas eu, por sua vez, vou fazer consultas regulares, consultas com parâmetros, consultas com parâmetros e cabeçalhos, portanto, somente esses três tipos de consulta serão feitos.



Na próxima seção, discutiremos os parâmetros e como trabalharemos com eles

HTTPHeaders

Os HTTPHeaders são apenas tipos de letra para um dicionário. Você pode criá-lo na parte superior do seu arquivo HTTPTask .

public typealias HTTPHeaders = [String:String] 


Parâmetros e codificação

Crie um arquivo, nomeie-o como ParameterEncoding e coloque-o na pasta Encoding. Crie typealias para Parameters , será novamente um dicionário regular. Fazemos isso para tornar o código mais compreensível e legível.

 public typealias Parameters = [String:Any] 


Em seguida, defina um protocolo ParameterEncoder com uma única função de codificação. O método de codificação possui dois parâmetros: inout URLRequest e Parameters . INOUT é uma palavra-chave Swift que define um parâmetro de função como uma referência. Normalmente, os parâmetros são passados ​​para a função como valores. Ao escrever inout antes de um parâmetro de função em uma chamada, você define esse parâmetro como um tipo de referência. Para saber mais sobre argumentos inout, você pode seguir este link. Em resumo, inout permite alterar o valor da variável em si, que foi passada para a função, e não apenas obter seu valor no parâmetro e trabalhar com ele dentro da função. O protocolo ParameterEncoder será implementado no JSONParameterEncoder e no URLPameterEncoder .

 public protocol ParameterEncoder { static func encode(urlRequest: inout URLRequest, with parameters: Parameters) throws } 


ParameterEncoder contém uma única função cuja tarefa é codificar parâmetros. Esse método pode gerar um erro que precisa ser tratado, então usamos o throw.

Também pode ser útil produzir erros não padrão, mas personalizados. É sempre muito difícil descriptografar o que o Xcode oferece a você. Quando você tem todos os erros personalizados e descritos, sempre sabe exatamente o que aconteceu. Para fazer isso, vamos definir uma enumeração que herda de Error .



Crie um arquivo, nomeie-o como URLParameterEncoder e coloque-o na pasta Encoding .



Este código pega uma lista de parâmetros, converte e formata-os para uso como parâmetros de URL. Como você sabe, alguns caracteres não são permitidos no URL. Os parâmetros também são separados pelo símbolo "&", portanto, devemos cuidar disso. Também devemos definir o valor padrão para os cabeçalhos se eles não estiverem definidos na solicitação.

Essa é a parte do código que deve ser coberta por testes de unidade. Construir uma solicitação de URL é a chave, caso contrário, podemos provocar muitos erros desnecessários. Se você usa a API aberta, obviamente não deseja usar todo o volume possível de solicitações para testes com falha. Se você quiser saber mais sobre testes de unidade, comece com este artigo.

JSONParameterEncoder

Crie um arquivo, denomine JSONParameterEncoder e coloque-o na pasta Encoding.



Tudo é o mesmo que no caso de URLParameter , apenas aqui vamos converter os parâmetros para JSON e adicionar novamente os parâmetros que definem a codificação "application / json" ao cabeçalho.

Networkrouter

Crie um arquivo, nomeie-o NetworkRouter e coloque-o na pasta Serviço. Vamos começar definindo tipealias para o fechamento.

 public typealias NetworkRouterCompletion = (_ data: Data?,_ response: URLResponse?,_ error: Error?)->() 


Em seguida, definimos o protocolo NetworkRouter .



O NetworkRouter possui um EndPoint que ele usa para solicitações e, assim que a solicitação é concluída, o resultado dessa solicitação é passado para o fechamento do NetworkRouterCompletion . O protocolo também possui uma função de cancelamento , que pode ser usada para interromper solicitações de carga e descarga de longo prazo. Também usamos o tipo associado aqui, porque queremos que o nosso roteador suporte qualquer tipo de EndPointType . Sem usar o tipo associado, o roteador precisaria ter algum tipo específico que implemente EndPointType . Se você quiser saber mais sobre o tipo associado, leia este artigo .

Roteador

Crie um arquivo, chame-o de roteador e coloque-o na pasta Serviço. Declaramos uma variável privada do tipo URLSessionTask . Todo o trabalho estará nele. Tornamos privado, porque não queremos que ninguém de fora possa alterá-lo.



Pedido

Aqui criamos o URLSession usando o URLSession.shared , esta é a maneira mais fácil de criar. Mas lembre-se de que esse método não é o único. Você pode usar configurações de URLSession mais complexas que podem alterar seu comportamento. Mais sobre isso neste artigo .

A solicitação é criada chamando a função buildRequest.A chamada de função é agrupada em do-try-catch, porque as funções de codificação dentro de buildRequest podem gerar exceções. Resposta , dados e erro são passados ​​para conclusão.



Pedido de compilação

Criamos nossa solicitação usando a função buildRequest . Essa função é responsável por todo o trabalho vital em nossa camada de rede. Converte essencialmente EndPointType em URLRequest . E assim que o EndPoint se transformar em uma solicitação, podemos passar para a sessão . Muitas coisas estão acontecendo aqui, então vamos dar uma olhada nos métodos. Primeiro, vamos examinar o método buildRequest :

1. Inicializamos a variável de solicitação URLRequest . Definimos nosso URL base e adicionamos o caminho da solicitação específica que será usada para ele.

2. Atribua request.httpMethod o método http do nosso EndPoint .

3. Criamos um bloco do-try-catch, porque nossos codificadores podem gerar um erro. Ao criar um grande bloco do-try-catch, eliminamos a necessidade de criar um bloco separado para cada tentativa.

4. No switch, verifique route.task .

5. Dependendo do tipo de tarefa, chamamos o codificador correspondente.



Configurar parâmetros

Crie a função configureParameters no roteador.



Essa função é responsável por converter nossos parâmetros de consulta. Como nossa API assume o uso de bodyParameters na forma de JSON e URLParameters convertidos para o formato de URL, simplesmente passamos os parâmetros apropriados para as funções de conversão correspondentes, descritas no começo do artigo. Se você usar uma API que inclua vários tipos de codificações, nesse caso, eu recomendaria adicionar HTTPTask com uma enumeração adicional com o tipo de codificação. Esta listagem deve conter todos os tipos possíveis de codificações. Depois disso, em configureParameters, adicione mais um argumento com essa enumeração. Dependendo do seu valor, alterne usando switch e faça a codificação necessária.

Adicionar cabeçalhos adicionais

Crie a função addAdditionalHeaders no roteador.



Basta adicionar todos os cabeçalhos necessários à solicitação.

Cancelar

A função de cancelamento parecerá bastante simples:



Exemplo de uso

Agora vamos tentar usar nossa camada de rede em um exemplo real. Vamos nos conectar ao TheMovieDB para receber dados para o nosso aplicativo.

MovieEndPoint

Crie um arquivo MovieEndPoint e coloque-o na pasta EndPoint. MovieEndPoint é o mesmo que
e TargetType em Moya. Aqui, implementamos nosso próprio EndPointType. Um artigo descrevendo como usar o Moya para um exemplo semelhante pode ser encontrado neste link .

 import Foundation enum NetworkEnvironment { case qa case production case staging } public enum MovieApi { case recommended(id:Int) case popular(page:Int) case newMovies(page:Int) case video(id:Int) } extension MovieApi: EndPointType { var environmentBaseURL : String { switch NetworkManager.environment { case .production: return "https://api.themoviedb.org/3/movie/" case .qa: return "https://qa.themoviedb.org/3/movie/" case .staging: return "https://staging.themoviedb.org/3/movie/" } } var baseURL: URL { guard let url = URL(string: environmentBaseURL) else { fatalError("baseURL could not be configured.")} return url } var path: String { switch self { case .recommended(let id): return "\(id)/recommendations" case .popular: return "popular" case .newMovies: return "now_playing" case .video(let id): return "\(id)/videos" } } var httpMethod: HTTPMethod { return .get } var task: HTTPTask { switch self { case .newMovies(let page): return .requestParameters(bodyParameters: nil, urlParameters: ["page":page, "api_key":NetworkManager.MovieAPIKey]) default: return .request } } var headers: HTTPHeaders? { return nil } } 


Moviemodel

Para analisar o modelo de dados MovieModel e JSON no modelo, o protocolo Decodable é usado. Coloque esse arquivo na pasta Modelo .

Nota : para um conhecimento mais detalhado dos protocolos Codable, Decodable e Encodable, você pode ler meu outro artigo , que descreve em detalhes todos os recursos de trabalhar com eles.

 import Foundation struct MovieApiResponse { let page: Int let numberOfResults: Int let numberOfPages: Int let movies: [Movie] } extension MovieApiResponse: Decodable { private enum MovieApiResponseCodingKeys: String, CodingKey { case page case numberOfResults = "total_results" case numberOfPages = "total_pages" case movies = "results" } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: MovieApiResponseCodingKeys.self) page = try container.decode(Int.self, forKey: .page) numberOfResults = try container.decode(Int.self, forKey: .numberOfResults) numberOfPages = try container.decode(Int.self, forKey: .numberOfPages) movies = try container.decode([Movie].self, forKey: .movies) } } struct Movie { let id: Int let posterPath: String let backdrop: String let title: String let releaseDate: String let rating: Double let overview: String } extension Movie: Decodable { enum MovieCodingKeys: String, CodingKey { case id case posterPath = "poster_path" case backdrop = "backdrop_path" case title case releaseDate = "release_date" case rating = "vote_average" case overview } init(from decoder: Decoder) throws { let movieContainer = try decoder.container(keyedBy: MovieCodingKeys.self) id = try movieContainer.decode(Int.self, forKey: .id) posterPath = try movieContainer.decode(String.self, forKey: .posterPath) backdrop = try movieContainer.decode(String.self, forKey: .backdrop) title = try movieContainer.decode(String.self, forKey: .title) releaseDate = try movieContainer.decode(String.self, forKey: .releaseDate) rating = try movieContainer.decode(Double.self, forKey: .rating) overview = try movieContainer.decode(String.self, forKey: .overview) } } 


Gerente de rede

Crie um arquivo NetworkManager na pasta Manager. No momento, o NetworkManager contém apenas duas propriedades estáticas: uma chave de API e uma enumeração que descreve o tipo de servidor ao qual se conectar. O NetworkManager também contém um roteador do tipo MovieApi .



Resposta de rede

Crie a enumeração NetworkResponse no NetworkManager.



Usamos essa enumeração ao processar respostas a solicitações e exibiremos a mensagem correspondente.

Resultado

Crie uma enumeração de resultados no NetworkManager.



Usamos Result para determinar se a solicitação foi bem-sucedida ou não. Caso contrário, retornaremos uma mensagem de erro com o motivo.

Processamento de resposta de solicitação

Crie a função handleNetworkResponse . Essa função usa um argumento, como um HTTPResponse, e retorna Result.



Nesta função, dependendo do statusCode recebido do HTTPResponse, retornamos uma mensagem de erro ou um sinal de uma solicitação bem-sucedida. Normalmente, um código no intervalo de 200..299 significa sucesso.

Fazendo uma solicitação de rede

Então, fizemos tudo para começar a usar nossa camada de rede, vamos tentar fazer uma solicitação.

Solicitaremos uma lista de novos filmes. Crie uma função e chame -a de getNewMovies .



Vamos dar um passo a passo:

1. Definimos o método getNewMovies com dois argumentos: o número da página de paginação e o manipulador de conclusão, que retorna uma matriz opcional de modelos de filme ou um erro opcional.

2. Ligue para o roteador . Passamos o número da página e concluímos o processo no fechamento.

3. URLSession retorna um erro se não houver rede ou não foi possível fazer uma solicitação por qualquer motivo. Observe que este não é um erro de API; esses erros ocorrem no cliente e geralmente ocorrem devido à baixa qualidade da conexão com a Internet.

4. Precisamos transmitir nossa resposta ao HTTPURLResponse , porque precisamos acessar a propriedade statusCode .

5. Declare o resultado e inicialize-o usando o método handleNetworkResponse

6. Sucesso significa que a solicitação foi bem-sucedida e recebemos a resposta esperada. Depois, verificamos se os dados vieram com a resposta e, se não, simplesmente terminamos o método por meio de retorno.

7. Se a resposta vier com dados, é necessário analisar os dados recebidos no modelo. Depois disso, passamos a matriz de modelos resultante para conclusão.

8. Em caso de erro, basta passar o erro para conclusão .

É isso, é assim que nossa própria camada de rede funciona no Swift puro, sem usar nenhuma dependência na forma de pods e bibliotecas de terceiros. Para fazer uma solicitação de API de teste para obter uma lista de filmes, crie um MainViewController com a propriedade NetworkManager e chame o método getNewMovies através dele.

  class MainViewController: UIViewController { var networkManager: NetworkManager! init(networkManager: NetworkManager) { super.init(nibName: nil, bundle: nil) self.networkManager = networkManager } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .green networkManager.getNewMovies(page: 1) { movies, error in if let error = error { print(error) } if let movies = movies { print(movies) } } } } 


Bônus pequeno

Você teve situações no Xcode quando não entendeu que tipo de espaço reservado é usado em um local específico? Por exemplo, veja o código que acabamos de escrever para o roteador .



Nós mesmos determinamos o NetworkRouterCompletion , mas mesmo neste caso, é fácil esquecer que tipo e como usá-lo. Mas nosso amado Xcode cuidou de tudo, e basta clicar duas vezes no espaço reservado e o Xcode substituirá o tipo desejado.



Conclusão

Agora, temos uma implementação de uma camada de rede orientada a protocolo, que é muito fácil de usar e que você sempre pode personalizar de acordo com suas necessidades. Compreendemos sua funcionalidade e como todos os mecanismos funcionam.

Você pode encontrar o código fonte neste repositório .

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


All Articles