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.

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.

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

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 = {
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
._notConnectedToInternetIniciando 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á
geradoDefinindo um tempo limite para uma solicitação
.responseTimeout(30)
um manipulador também pode ser pendurado em um evento de tempo limite
. onTimeout {
caso contrário,
onError será
geradoDefinindo 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)
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'
O APIRequest para ReactiveCoca terá
.signalProducer e para RxSwift
.observableUPD2: Agora você pode executar várias solicitaçõesSe 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
.onProgressUPD3: 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)