Codificável para solicitações de API e como colocar o código em ordem

Olá Habr!

A partir do Swift 4, temos acesso ao novo protocolo Codable, que facilita a codificação / decodificação de modelos. Meus projetos têm muito código para chamadas de API e, no ano passado, trabalhei muito para otimizar esse enorme conjunto de códigos em algo muito leve, conciso e simples, matando o código repetido e usando o Codable mesmo para solicitações multipartes e parâmetros de consulta de URL. Acabou sendo várias aulas excelentes na minha opinião para enviar solicitações e analisar respostas do servidor. Além de uma estrutura de arquivos conveniente, que são os controladores para cada grupo de solicitações, que eu criei ao usar o Vapor 3 no back-end. Alguns dias atrás, aloquei todos os meus desenvolvimentos em uma biblioteca separada e o chamei de CodyFire. Eu gostaria de falar sobre ela neste artigo.

Isenção de responsabilidade


O CodyFire é baseado no Alamofire, mas é mais do que apenas um invólucro no Alamofire, é uma abordagem de sistema para trabalhar com a API REST para iOS. É por isso que não me preocupo que a Alamofire esteja vendo a quinta versão em que haverá suporte para Codable, porque não matará minha criação.

Inicialização


Vamos começar um pouco de longe, a saber, que geralmente temos três servidores:

dev - para desenvolvimento, o que começamos do Xcode
stage - para teste antes do lançamento, geralmente no TestFlight ou InHouse
prod - produção, para a AppStore

E é claro que muitos desenvolvedores do iOS estão cientes da existência de variáveis de ambiente e dos esquemas de inicialização no Xcode, mas ao longo de minha (mais de 8 anos) prática, 90% dos desenvolvedores escrevem manualmente o servidor certo em alguma constante durante o teste ou antes da montagem, e isso é o que eu gostaria de corrigir, mostrando um bom exemplo de como fazê-lo corretamente.

Por padrão, o CodyFire determina automaticamente em qual ambiente o aplicativo está sendo executado no momento, tornando-o muito simples:

#if DEBUG //DEV environment #else if Bundle.main.appStoreReceiptURL?.lastPathComponent == "sandboxReceipt" { //TESTFLIGHT environment } else { //APPSTORE environment } #endif 

É claro que isso está oculto e, no projeto no AppDelegate, você só precisa registrar três URLs

 import CodyFire @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { let dev = CodyFireEnvironment(baseURL: "http://localhost:8080") let testFlight = CodyFireEnvironment(baseURL: "https://stage.myapi.com") let appStore = CodyFireEnvironment(baseURL: "https://api.myapi.com") CodyFire.shared.configureEnvironments(dev: dev, testFlight: testFlight, appStore: appStore) return true } } 

E pode-se ficar contente com isso e não fazer mais nada.

Mas na vida real, muitas vezes precisamos testar os servidores de desenvolvimento, de estágio e de produção no Xcode, e por isso peço que você use esquemas de inicialização.

imagem
Dica: na seção Gerenciar esquemas , não esqueça de marcar a caixa de seleção `shared` para cada esquema, para que estejam disponíveis para todos os desenvolvedores no projeto.

Em cada esquema, você precisa escrever a variável de ambiente `env ', que pode assumir três valores: dev, testFlight, appStore.

imagem

E para que esses esquemas funcionem com o CodyFire, você precisa adicionar o seguinte código ao AppDelegate.didFinishLaunchingWithOptions após inicializar o CodyFire

 CodyFire.shared.setupEnvByProjectScheme() 

Além disso, muitas vezes o chefe ou testadores do seu projeto podem solicitar que você troque o servidor rapidamente em algum lugar do LoginScreen . Com o CodyFire, você pode facilmente implementar isso alternando o servidor em uma linha, alterando o ambiente:

 CodyFire.shared.environmentMode = .appStore 

Isso funcionará até que o aplicativo seja reiniciado e, se você quiser que ele seja salvo após o lançamento, salve o valor em UserDefaults , verifique quando o aplicativo é iniciado no AppDelegate e alterne o ambiente para o necessário.
Eu disse a esse ponto importante, espero que haja mais projetos nos quais a mudança de ambiente seja feita de maneira bonita. E, ao mesmo tempo, já inicializamos a biblioteca.

Estrutura de arquivos e controladores


Agora você pode falar sobre minha visão da estrutura de arquivos para todas as chamadas de API, isso pode ser chamado de ideologia do CodyFire.

Vamos ver como finalmente parece no projeto

imagem

Agora, vamos olhar para as listagens de arquivos, vamos começar com API.swift .

 class API { typealias auth = AuthController typealias post = PostController } 

Os links para todos os controladores estão listados aqui, para que possam ser facilmente chamados através do `API.controller.method`.

 class AuthController {} 

API + Login.swift

 extension AuthController { struct LoginResponse: Codable { var token: String } static func login(email: String, password: String) -> APIRequest<LoginResponse> { return APIRequest("login").method(.post) .basicAuth(email: email, password: password) .addCustomError(.notFound, "User not found") } } 

Neste decorador, declaramos uma função para chamar nossa API:

- especificar ponto final
- método HTTP POST
- use wrapper para autenticação básica
- declare o texto desejado para uma resposta específica do servidor (isso é conveniente)
- e indicar o modelo pelo qual os dados serão decodificados

O que permanece escondido?

- não é necessário especificar o URL completo do servidor, porque já está definido globalmente
- não precisou indicar que esperamos receber 200 OK se estiver tudo bem
200 OK é o código de status padrão esperado pelo CodyFire para todas as solicitações; nesse caso, os dados são decodificados e um retorno de chamada é chamado; tudo está bem, aqui estão seus dados.
Além disso, em algum lugar no código da sua tela de login, você pode simplesmente ligar

 API.auth.login(email: "test@mail.com", password: "qwerty").onError { error in switch error.code { case .notFound: print(error.description) //: User not found default: print(error.description) } }.onSuccess { token in //TODO:  auth token    print("Received auth token: "+ token) } 

onError e onSuccess são apenas uma pequena parte dos retornos de chamada que o APIRequest pode retornar, falaremos sobre eles mais tarde.

No exemplo de entrada, consideramos apenas a opção quando os dados retornados são decodificados automaticamente, mas você pode dizer que pode implementá-los e estará certo. Portanto, vamos considerar a possibilidade de enviar dados de acordo com o modelo usando o formulário de registro como exemplo.

API + Signup.swift

 extension AuthController { struct SignupRequest: JSONPayload { let email, password: String let firstName, lastName, mobileNumber: String init(email: String, password: String, firstName: String, lastName: String, mobileNumber: String) { self.email = email self.password = password self.firstName = firstName self.lastName = lastName self.mobileNumber = mobileNumber } } struct SignupResponse: Codable { let token: String } static func signup(_ request: SignupRequest) -> APIRequest<SignupResponse> { return APIRequest("signup", payload: request).method(.post) .addError(.conflict, "Account already exists") } } 

Ao contrário do login, durante o registro, transmitimos uma grande quantidade de dados.

Neste exemplo, temos um modelo SignupRequest que está em conformidade com o protocolo JSONPayload (portanto, o CodyFire entende o tipo de carga útil) para que o corpo da nossa solicitação esteja no formato JSON. Se você precisar de x-www-form-urlencoded, use FormURLEncodedPayload .

Como resultado, você obtém uma função simples que aceita o modelo de carga útil
 API.auth.signup(request) 

e que, se for bem-sucedido, retornará um modelo de resposta específico.

Eu acho legal, né?

Mas e se multipart?


Vejamos um exemplo quando você pode criar uma postagem .

Post + Create.swift

 extension PostController { struct CreateRequest: MultipartPayload { var text: String var tags: [String] var images: [Attachment] var video: Data init (text: String, tags: [String], images: [Attachment], video: Data) { self.text = text self.tags = tags self.images = images self.video = video } } struct Post: Codable { let text: String let tags: [String] let linksToImages: [String] let linkToVideo: String } static func create(_ request: CreateRequest) -> APIRequest<CreateRequest> { return APIRequest("post", payload: request).method(.post) } } 

Esse código poderá enviar um formulário de várias partes com uma matriz de arquivos de imagem e com um vídeo.
Vamos ver como chamar um despacho. Aqui está o momento mais interessante sobre o anexo .

 let videoData = FileManager.default.contents(atPath: "/path/to/video.mp4")! let imageAttachment = Attachment(data: UIImage(named: "cat")!.jpeg(.high)!, fileName: "cat.jpg", mimeType: .jpg) let payload = PostController.CreateRequest(text: "CodyFire is awesome", tags: ["codyfire", "awesome"], images: [imageAttachment], video: videoData) API.post.create(payload).onProgress { progress in print(" : \(progress)") }.onError { error in print(error.description) }.onSuccess { createdPost in print("  : \(createdPost)") } 

Anexo é um modelo no qual, além de Dados, o nome do arquivo e seu MimeType também são transmitidos.

Se você já enviou um formulário de várias partes da Swift usando o Alamofire ou um URLRequest nu , tenho certeza de que você apreciará a simplicidade do CodyFire .

Agora, exemplos mais simples, mas não menos interessantes, de chamadas GET.

Post + Get.swift

 extension PostController { struct ListQuery: Codable { let offset, limit: Int init (offset: Int, limit: Int) { self.offset = offset self.limit = limit } } static func get(_ query: ListQuery? = nil) -> APIRequest<[Post]> { return APIRequest("post").query(query) } static func get(id: UUID) -> APIRequest<Post> { return APIRequest("post/" + id.uuidString) } } 

O exemplo mais simples é

 API.post.get(id:) 

que no onSuccess retornará o modelo Post para você.

Aqui está um exemplo mais interessante.

 API.post.get(PostController.ListQuery(offset: 0, limit: 100)) 

que usa um modelo ListQuery como entrada,
que APIRequest eventualmente converte em um caminho de URL do formulário

 post?limit=0&offset=100 

e retornará a matriz [Post] para onSuccess .

Claro, você pode escrever o caminho da URL à moda antiga, mas agora você sabe que pode totalmente codificar.

O exemplo final de solicitação será DELETE

Postar + Excluir.swift

 extension PostController { static func delete(id: UUID) -> APIRequest<Nothing> { return APIRequest("post/" + id.uuidString) .method(.delete) .desiredStatusCode(.noContent) } } 

Existem dois pontos interessantes.

- o tipo de retorno é APIRequest, especifica o tipo genérico Nothing , que é um modelo Codable vazio.
- indicamos explicitamente que esperamos receber 204 neste caso, e o CodyFire só ligará para o sucesso neste caso.

Você já sabe como chamar esse ponto de extremidade do seu ViewController.

Mas existem duas opções, a primeira com onSuccess e a segunda sem. Vamos olhar para ele

 API.post.delete(id:).execute() 

Ou seja, se não lhe interessa se a solicitação funciona , você pode simplesmente chamar .execute () e pronto , caso contrário, será iniciada após a declaração onSuccess do manipulador.

Funções Disponíveis


Autorização de cada solicitação


Para assinar cada API de solicitação com qualquer cabeçalho http, é usado um manipulador global, que você pode definir em algum lugar do AppDelegate . Além disso, você pode usar o modelo [String: String] ou Codable clássico para escolher.

Exemplo para portador de autorização.

1. Codificável (recomendável)
 CodyFire.shared.fillCodableHeaders = { struct Headers: Codable { //NOTE:  nil,     headers var Authorization: String? var anythingElse: String } return Headers(Authorization: nil, anythingElse: "hello") } 

2. [String: String] clássica
 CodyFire.shared.fillHeaders = { guard let apiToken = LocalAuthStorage.savedToken else { return [:] } return ["Authorization": "Bearer \(apiToken)"] } 

Adicione seletivamente alguns cabeçalhos http à solicitação


Isso pode ser feito ao criar o APIRequest, por exemplo:

 APIRequest("some/endpoint").headers(["someKey": "someValue"]) 

Tratamento de solicitações não autorizadas


Você pode processá-los globalmente, por exemplo, no AppDelegate

 CodyFire.shared.unauthorizedHandler = { //   WelcomeScreen } 

ou localmente em cada solicitação

 API.post.create(request).onNotAuthorized { //   } 

Se a rede não estiver disponível


 API.post.create(request). onNetworkUnavailable { //   ,  ,     } 
caso contrário, em onError, você receberá um erro ._notConnectedToInternet

Iniciando algo antes do início da solicitação


Você pode definir .onRequestStarted e começar a mostrar, por exemplo, um carregador nele.
Este é um local conveniente, porque não é chamado se a Internet não estiver disponível e você não precisa mostrar o carregador por nada, por exemplo.

Como desativar / ativar a saída de log globalmente


 CodyFire.shared.logLevel = .debug CodyFire.shared.logLevel = .error CodyFire.shared.logLevel = .info CodyFire.shared.logLevel = .off 

Como desativar a saída de log para uma única solicitação


 .avoidLogError() 

Processe logs à sua maneira


 CodyFire.shared.logHandler = { level, text in print("  CodyFire: " + text) } 

Como definir o código de resposta http esperado do servidor


Como eu disse acima, por padrão, o CodyFire espera receber 200 OK e , se receber, começa a analisar dados e chama o sucesso .

Mas o código esperado pode ser definido na forma de uma enum conveniente, por exemplo, para 201 CREATED

 .desiredStatusCode(.created) 

ou você pode até definir o código esperado personalizado

 .desiredStatusCode(.custom(777)) 

Cancelar pedido


 .cancel() 

e você pode descobrir que a solicitação foi cancelada declarando um manipulador .onCancellation

 .onCancellation { //   } 

caso contrário, onError será gerado

Definindo um tempo limite para uma solicitação


 .responseTimeout(30) //   30  

um manipulador também pode ser pendurado em um evento de tempo limite

 . onTimeout { //    } 

caso contrário, onError será gerado

Definindo um tempo limite interativo extra


Este é o meu recurso favorito. Um cliente dos EUA uma vez me perguntou sobre ela, porque ele não gostou que o formulário de login funcionasse rápido demais, na opinião dele, não parecia natural, como se fosse uma falsificação, não uma autorização.

A idéia é que ele quisesse que a verificação de e-mail / senha durasse 2 segundos ou mais. E se durar apenas 0,5 segundos, você precisará lançar outro 1,5 e só então chamar o sucesso . E se demorar exatamente 2 ou 2,5 segundos, ligue imediatamente para o Sucess .

 .additionalTimeout(2) // 2     

Codificador / decodificador de data personalizado


O CodyFire possui sua própria enumeração DateCodingStrategy , na qual existem três valores

- secondsSince1970
- milissegundosSince1970
- formatado (_ customDateFormatter: DateFormatter)

DateCodingStrategy pode ser definido de três maneiras e separadamente para decodificação e codificação
- globalmente no AppDelegate

 CodyFire.shared.dateEncodingStrategy = .secondsSince1970 let customDateFormatter = DateFormatter() CodyFire.shared.dateDecodingStrategy = .formatted(customDateFormatter) 

- para um pedido

 APIRequest("some/endpoint") .dateDecodingStrategy(.millisecondsSince1970) .dateEncodingStrategy(.secondsSince1970) 

- ou mesmo separadamente para cada modelo, você só precisa que o modelo corresponda a CustomDateEncodingStrategy e / ou CustomDateDecodingStrategy .

 struct SomePayload: JSONPayload, CustomDateEncodingStrategy, CustomDateDecodingStrategy { var dateEncodingStrategy: DateCodingStrategy var dateDecodingStrategy: DateCodingStrategy } 

Como adicionar ao projeto


A biblioteca está disponível no GitHub sob a licença MIT.

Atualmente, a instalação está disponível apenas através do CocoaPods
 pod 'CodyFire' 


Eu realmente espero que o CodyFire seja útil para outros desenvolvedores do iOS, simplifique o desenvolvimento para eles e, em geral, torne o mundo um pouco melhor e as pessoas mais gentis.

Isso é tudo, obrigado pelo seu tempo.

UPD: ReactiveCocoa e suporte a RxSwift adicionados
 pod 'ReactiveCodyFire' # ReactiveCocoa pod 'RxCodyFire' # RxSwift #      'CodyFire',     

O APIRequest para ReactiveCoca terá .signalProducer e para RxSwift .observable

UPD2: Agora você pode executar várias solicitações
Se for importante obter o resultado de cada consulta, use .and ()
Máximo neste modo, você pode executar até 10 solicitações, elas serão executadas estritamente uma após a outra.
 API.employee.all() .and(API.office.all()) .and(API.car.all()) .and(API.event.all()) .and(API.post.all()) .onError { error in print(error.description) }.onSuccess { employees, offices, cars, events, posts in //    !!! } 

onRequestStarted, onNetworkUnavailable, onCancellation, onNotAuthorized, onTimeout também estão disponíveis.
onProgress - ainda em desenvolvimento

Se você não se importa com os resultados de suas consultas, pode usar .flatten ()
 [API.employee.all(), API.office.all(), API.car.all()].flatten().onError { print(error.description) }.onSuccess { print("flatten finished!") } 
Para executá-los ao mesmo tempo, basta adicionar .concurrent (por: 3), isso permitirá que três solicitações sejam executadas simultaneamente, qualquer número pode ser especificado.
Para ignorar erros de consulta com falha, adicione .avoidCancelOnError ()
Para obter progresso, adicione .onProgress

UPD3: agora você pode definir um servidor separado para cada solicitação
É necessário criar os endereços de servidor necessários em algum lugar, por exemplo, como este
 let server1 = ServerURL(base: "https://server1.com", path: "v1") let server2 = ServerURL(base: "https://server2.com", path: "v1") let server3 = ServerURL(base: "https://server3.com") 
E agora você pode usá-los diretamente na inicialização da solicitação antes de especificar o terminal
 APIRequest(server1, "endpoint", payload: payloadObject) APIRequest(server2, "endpoint", payload: payloadObject) APIRequest(server3, "endpoint", payload: payloadObject) 
ou você pode especificar o servidor após inicializar a solicitação
 APIRequest("endpoint", payload: payloadObject).serverURL(server1) 

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


All Articles