Hola Habr!
Comenzando con Swift 4, tenemos acceso al nuevo protocolo Codificable, que facilita la codificación / decodificación de modelos. Mis proyectos tienen mucho código para las llamadas a la API, y durante el último año he trabajado mucho para optimizar esta enorme variedad de código en algo muy ligero, conciso y simple eliminando el código repetido y usando Codable incluso para solicitudes de varias partes y parámetros de consulta de URL. Resultó ser varias clases excelentes en mi opinión para enviar solicitudes y analizar respuestas del servidor. Además de una conveniente estructura de archivos, que son los controladores para cada grupo de solicitudes, que arraigé al usar Vapor 3 en el back-end. Hace unos días, asigné todos mis desarrollos a una biblioteca separada y la llamé CodyFire. Me gustaría hablar de ella en este artículo.
Descargo de responsabilidad
CodyFire se basa en Alamofire, pero es más que un contenedor en Alamofire, es un enfoque de todo el sistema para trabajar con la API REST para iOS. Es por eso que no me preocupa que Alamofire esté cortando la quinta versión en la que habrá soporte Codificable, porque No matará mi creación.
Inicialización
Comencemos un poco desde lejos, a saber, que a menudo tenemos tres servidores:
dev - para el desarrollo, lo que comenzamos desde Xcode
etapa : para probar antes del lanzamiento, generalmente en TestFlight o InHouse
prod - production, para la AppStore
Y, por supuesto, muchos desarrolladores de iOS son conscientes de la existencia de
Variables de
entorno y los esquemas de inicio en Xcode, pero a lo largo de mi práctica (más de 8 años), el 90% de los desarrolladores escriben manualmente el servidor correcto de forma constante durante las pruebas o antes del ensamblaje, y esto es lo que me gustaría arreglar mostrando un buen ejemplo de cómo hacerlo bien.
Por defecto, CodyFire determina automáticamente en qué entorno se está ejecutando actualmente la aplicación, lo hace muy simple:
#if DEBUG //DEV environment #else if Bundle.main.appStoreReceiptURL?.lastPathComponent == "sandboxReceipt" { //TESTFLIGHT environment } else { //APPSTORE environment } #endif
Por supuesto, esto está bajo el capó, y en el proyecto en AppDelegate solo necesita registrar tres URL
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 } }
Y uno podría estar contento de esto y no hacer nada más.
Pero en la vida real, a menudo necesitamos probar servidores de desarrollo, etapa y producción en Xcode, y para esto le insto a que use esquemas de inicio.

Consejo: en la sección Administrar esquemas , no olvide marcar la casilla de verificación `compartida` para cada esquema para que estén disponibles para todos los desarrolladores del proyecto.
En cada esquema, debe escribir la variable de entorno `env` que puede tomar tres valores: dev, testFlight, appStore.

Y para que estos esquemas funcionen con CodyFire, debe agregar el siguiente código a
AppDelegate.didFinishLaunchingWithOptions después de inicializar CodyFire
CodyFire.shared.setupEnvByProjectScheme()
Además, a menudo el jefe o los evaluadores de su proyecto pueden pedirle que cambie el servidor sobre la marcha en algún lugar de
LoginScreen . Con CodyFire, puede implementar esto fácilmente cambiando el servidor en una línea, cambiando el entorno:
CodyFire.shared.environmentMode = .appStore
Esto funcionará hasta que se reinicie la aplicación, y si desea que se guarde después del inicio, guarde el valor en UserDefaults , verifique cuando la aplicación se inicie en AppDelegate y cambie el entorno al necesario.
Le dije a este importante punto, espero que haya más proyectos en los que el cambio del entorno se haga de manera hermosa. Y al mismo tiempo, ya hemos inicializado la biblioteca.
Estructura de archivos y controladores
Ahora puede hablar sobre mi visión de la estructura de archivos para todas las llamadas API, esto se puede llamar la ideología de CodyFire.
Veamos cómo se ve finalmente en el proyecto.

Ahora veamos los listados de archivos, comencemos con
API.swift .
class API { typealias auth = AuthController typealias post = PostController }
Los enlaces a todos los controladores se enumeran aquí para que puedan llamarse fácilmente a través de `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") } }
En este decorador, declaramos una función para llamar a nuestra API:
- especificar punto final
- Método HTTP POST
- use wrapper para autenticación básica
- declarar el texto deseado para una respuesta específica del servidor (esto es conveniente)
- e indique el modelo mediante el cual se decodificarán los datos
¿Qué queda oculto?
- no es necesario especificar la URL completa del servidor, ya está configurado globalmente
- no tenía que indicar que esperamos recibir
200 OK si todo está bien
200 OK es el código de estado predeterminado esperado por CodyFire para todas las solicitudes, en cuyo caso se decodifican los datos y se llama a una devolución de llamada, que todo está bien, aquí están sus datos.
Además, en algún lugar del código de su
pantalla de inicio de sesión, simplemente puede llamar
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 y
onSuccess son solo una pequeña parte de las devoluciones de llamada que APIRequest puede devolver, hablaremos de ellas más adelante.
En el ejemplo de entrada, consideramos solo la opción cuando los datos devueltos se decodifican automáticamente, pero puede decir que usted mismo podría implementarlos, y tendrá razón. Por lo tanto, consideremos la posibilidad de enviar datos según el modelo utilizando el formulario de registro como ejemplo.
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") } }
A diferencia del inicio de sesión, durante el registro transmitimos una gran cantidad de datos.
En este ejemplo, tenemos un modelo
SignupRequest que se ajusta al protocolo
JSONPayload (por lo tanto, CodyFire entiende el tipo de carga útil) para que el cuerpo de nuestra solicitud tenga la forma de JSON. Si necesita x-www-form-urlencoded, use
FormURLEncodedPayload .
Como resultado, obtienes una función simple que acepta el modelo de carga útil
API.auth.signup(request)
y que, si tiene éxito, le devolverá un modelo de respuesta específico.
Creo que es genial, ¿verdad?
Pero, ¿y si es multiparte?
Veamos un ejemplo cuando puedes crear una
publicación .
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) } }
Este código podrá enviar un formulario multiparte con una variedad de archivos de imagen y con un video.
Veamos cómo llamar a un despacho. Aquí está el momento más interesante sobre
Adjunto .
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)") }
El archivo
adjunto es un modelo en el que, además de
Datos, también se transmiten
el nombre
del archivo y su MimeType.
Si alguna vez envió un formulario de varias partes de Swift usando Alamofire o una
URLRequest desnuda
, estoy seguro de que apreciará la simplicidad de
CodyFire .
Ahora ejemplos más simples pero no menos interesantes de llamadas 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) } }
El ejemplo más simple es
API.post.get(id:)
que en
onSuccess le devolverá el modelo
Post .
Aquí hay un ejemplo más interesante.
API.post.get(PostController.ListQuery(offset: 0, limit: 100))
que toma un modelo
ListQuery como entrada,
qué APIRequest finalmente se convierte en una ruta URL del formulario
post?limit=0&offset=100
y devolverá la matriz
[Post] a
onSuccess .
Por supuesto, puede escribir la ruta URL a la antigua usanza, pero ahora sabe que puede codificar totalmente.
La solicitud de ejemplo final será BORRAR
Publicar + Eliminar.swift extension PostController { static func delete(id: UUID) -> APIRequest<Nothing> { return APIRequest("post/" + id.uuidString) .method(.delete) .desiredStatusCode(.noContent) } }
Hay dos puntos interesantes.
- el tipo de retorno es APIRequest, especifica el tipo genérico
Nothing , que es un modelo
codificable vacío.
- indicamos explícitamente que esperamos recibir 204 SIN CONTENIDO, y CodyFire solo llamará a
onSuccess en este caso.
Ya sabe cómo llamar a este punto final desde su ViewController.
Pero hay dos opciones, la primera con
onSuccess y la segunda sin. Lo miraremos
API.post.delete(id:).execute()
Es decir, si no le importa si la solicitud
funciona , simplemente puede llamar a
.execute () y eso es todo; de lo contrario, comenzará después de la declaración
onSuccess del controlador.
Funciones disponibles
Autorización de cada solicitud.
Para firmar cada API de solicitud con cualquier encabezado http, se utiliza un controlador global, que puede establecer en algún lugar de
AppDelegate . Además, puede usar el modelo clásico [String: String] o Codable para elegir.
Ejemplo para el titular de la autorización.
1. Codificable (recomendado)
CodyFire.shared.fillCodableHeaders = { struct Headers: Codable { //NOTE: nil, headers var Authorization: String? var anythingElse: String } return Headers(Authorization: nil, anythingElse: "hello") }
2. Clásico [Cadena: Cadena]
CodyFire.shared.fillHeaders = { guard let apiToken = LocalAuthStorage.savedToken else { return [:] } return ["Authorization": "Bearer \(apiToken)"] }
Agregue selectivamente algunos encabezados http a la solicitud
Esto se puede hacer al crear APIRequest, por ejemplo:
APIRequest("some/endpoint").headers(["someKey": "someValue"])
Manejo de solicitudes no autorizadas
Puede procesarlos globalmente, por ejemplo en
AppDelegate CodyFire.shared.unauthorizedHandler = {
o localmente en cada solicitud
API.post.create(request).onNotAuthorized {
Si la red no está disponible
API.post.create(request). onNetworkUnavailable {
de lo contrario, en
onError obtendrá un error
._notConnectedToInternetComenzar algo antes de que comience la solicitud
Puede establecer
.onRequestStarted y comenzar a mostrar, por ejemplo, un cargador en él.
Este es un lugar conveniente, porque no se llama en caso de falta de Internet, y no tiene que presentarse en vano para mostrar un cargador, por ejemplo.
Cómo deshabilitar / habilitar la salida de registro globalmente
CodyFire.shared.logLevel = .debug CodyFire.shared.logLevel = .error CodyFire.shared.logLevel = .info CodyFire.shared.logLevel = .off
Cómo deshabilitar la salida del registro para una sola solicitud
.avoidLogError()
Procese los registros a su manera
CodyFire.shared.logHandler = { level, text in print(" CodyFire: " + text) }
Cómo configurar el código de respuesta http esperado del servidor
Como dije anteriormente, de manera predeterminada, CodyFire espera recibir
200 OK, y si lo hace, comienza a analizar datos y llama a
Success .
Pero el código esperado se puede establecer en forma de una enumeración conveniente, por ejemplo, para
201 CREATED .desiredStatusCode(.created)
o incluso puedes configurar el código esperado personalizado
.desiredStatusCode(.custom(777))
Cancelar solicitud
.cancel()
y puede descubrir que la solicitud se canceló declarando un controlador
.onCancellation .onCancellation {
de lo contrario, se
generará onErrorEstablecer un tiempo de espera para una solicitud
.responseTimeout(30)
un controlador también se puede colgar en un evento de tiempo de espera
. onTimeout {
de lo contrario, se
generará onErrorEstablecer un tiempo de espera adicional interactivo
Esta es mi característica favorita. Una vez, un cliente de EE. UU. Me preguntó por ella, porque no le gustó que el formulario de inicio de sesión funcionara demasiado rápido, en su opinión, no parecía natural, como si fuera una falsificación, no una autorización.
La idea es que quería que la verificación del correo electrónico / contraseña durara 2 segundos o más. Y si dura solo 0.5 segundos, entonces necesita lanzar otros 1.5 y solo luego llamar a
Success . Y si toma exactamente 2 o 2.5 segundos, llame a
onSuccess inmediatamente.
.additionalTimeout(2)
Codificador / decodificador de fecha personalizado
CodyFire tiene su propia enumeración
DateCodingStrategy , en la que hay tres valores
- segundosDesde1970
- milisegundos desde1970
- formateado (_ customDateFormatter: DateFormatter)
DateCodingStrategy se puede configurar de tres maneras y por separado para decodificar y codificar
- globalmente en
AppDelegate CodyFire.shared.dateEncodingStrategy = .secondsSince1970 let customDateFormatter = DateFormatter() CodyFire.shared.dateDecodingStrategy = .formatted(customDateFormatter)
- para una solicitud
APIRequest("some/endpoint") .dateDecodingStrategy(.millisecondsSince1970) .dateEncodingStrategy(.secondsSince1970)
- o incluso por separado para cada modelo, solo necesita que el modelo coincida con
CustomDateEncodingStrategy y / o
CustomDateDecodingStrategy .
struct SomePayload: JSONPayload, CustomDateEncodingStrategy, CustomDateDecodingStrategy { var dateEncodingStrategy: DateCodingStrategy var dateDecodingStrategy: DateCodingStrategy }
Cómo agregar al proyecto
La biblioteca está disponible en GitHub bajo la licencia MIT.La instalación actualmente solo está disponible a través de CocoaPods
pod 'CodyFire'
Realmente espero que CodyFire sea útil para otros desarrolladores de iOS, simplificará el desarrollo para ellos y, en general, hará que el mundo sea un poco mejor y la gente sea más amable.
Eso es todo, gracias por tu tiempo.
UPD: Soporte ReactiveCocoa y RxSwift agregado pod 'ReactiveCodyFire'
APIRequest para ReactiveCoca tendrá
.signalProducer , y para RxSwift
.observableUPD2: ahora puede ejecutar múltiples solicitudesSi es importante para usted obtener el resultado de cada consulta, use
.and ()Máximo en este modo, puede ejecutar hasta 10 solicitudes, se ejecutarán estrictamente una tras otra.
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 también están disponibles.
onProgress - todavía en desarrollo
Si no le importan los resultados de sus consultas, puede usar
.flatten () [API.employee.all(), API.office.all(), API.car.all()].flatten().onError { print(error.description) }.onSuccess { print("flatten finished!") }
Para ejecutarlos al mismo tiempo, simplemente agregue
.concurrent (por: 3) esto permitirá que tres solicitudes se ejecuten simultáneamente, se puede especificar cualquier número.
Para omitir errores de consulta fallidos, agregue
.avoidCancelOnError ()Para obtener progreso, agregue
.onProgressUPD3: ahora puede configurar un servidor separado para cada solicitudEs necesario crear las direcciones de servidor necesarias en algún lugar, por ejemplo, así
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")
Y ahora puede usarlos directamente en la inicialización de la solicitud antes de especificar el punto final
APIRequest(server1, "endpoint", payload: payloadObject) APIRequest(server2, "endpoint", payload: payloadObject) APIRequest(server3, "endpoint", payload: payloadObject)
o puede especificar el servidor después de inicializar la solicitud
APIRequest("endpoint", payload: payloadObject).serverURL(server1)