Escribir su capa de red en Swift: enfoque orientado al protocolo



Ahora, casi el 100% de las aplicaciones utilizan redes, por lo que todos se enfrentan a la organización y el uso de la capa de red. Hay dos enfoques principales para resolver este problema: usar bibliotecas de terceros o su propia implementación de la capa de red. En este artículo consideraremos la segunda opción e intentaremos implementar una capa de red utilizando todas las características más recientes del lenguaje, utilizando protocolos y enumeraciones. Esto salvará el proyecto de dependencias innecesarias en forma de bibliotecas adicionales. Quienes hayan visto a Moya reconocerán de inmediato muchos detalles similares en la implementación y el uso, tal como están, solo que esta vez lo haremos nosotros mismos sin tocar a Moya y Alamofire.


En esta guía, veremos cómo implementar una capa de red en Swift puro, sin usar bibliotecas de terceros. Una vez que revise este artículo, su código se convertirá

  • orientado al protocolo
  • facil de usar
  • facil de usar
  • tipo seguro
  • para puntos finales se utilizarán enumeraciones


A continuación se muestra un ejemplo de cómo se verá el uso de nuestra capa de red después de su implementación:



Simplemente escribiendo router.request (. Y usando todo el poder de las enumeraciones, veremos todas las opciones de consulta posibles y sus parámetros.

Primero, un poco sobre la estructura del proyecto.

Cada vez que crea algo nuevo, y para poder comprender fácilmente todo en el futuro, es muy importante organizar y estructurar todo correctamente. Creo que una estructura de carpetas correctamente organizada es un detalle importante al construir la arquitectura de la aplicación. Para que podamos tener todo correctamente organizado en carpetas, creémoslas de antemano. Esto se verá como la estructura general de carpetas en el proyecto:



Protocolo de tipo de punto final

En primer lugar, necesitamos definir nuestro protocolo EndPointType . Este protocolo contendrá toda la información necesaria para configurar la solicitud. ¿Qué es una solicitud (punto final)? En esencia, se trata de una URLRequest con todos los componentes relacionados, como encabezados, parámetros de solicitud, cuerpo de solicitud. El protocolo EndPointType es la parte más importante de nuestra implementación de capa de red. Creemos un archivo y asígnele el nombre EndPointType . Coloque este archivo en la carpeta Servicio (no en la carpeta EndPoint, por qué, se aclarará un poco más tarde)



Protocolos HTTP

Nuestro EndPointType contiene varios protocolos que necesitamos para crear una solicitud. Veamos cuáles son estos protocolos.

HTTPMethod

Cree un archivo, asígnele el nombre HTTPMethod y colóquelo en la carpeta Servicio. Este listado se utilizará para establecer el método HTTP de nuestra solicitud.



HTTPTask
Cree un archivo, asígnele el nombre HTTPTask y colóquelo en la carpeta Servicio. HTTPTask es responsable de configurar los parámetros de una solicitud específica. Puede agregarle todas las opciones de consulta que necesite, pero yo, a su vez, haré consultas regulares, consultas con parámetros, consultas con parámetros y encabezados, por lo que solo haré estos tres tipos de consultas.



En la siguiente sección, discutiremos los Parámetros y cómo trabajaremos con ellos.

HTTPHeaders

HTTPHeaders son solo typealias para un diccionario. Puede crearlo en la parte superior de su archivo HTTPTask .

public typealias HTTPHeaders = [String:String] 


Parámetros y codificación

Cree un archivo, asígnele el nombre ParameterEncoding y colóquelo en la carpeta Encoding. Cree typealias para Parámetros , nuevamente será un diccionario normal. Hacemos esto para que el código se vea más comprensible y legible.

 public typealias Parameters = [String:Any] 


A continuación, defina un protocolo ParameterEncoder con una sola función de codificación. El método de codificación tiene dos parámetros: inout URLRequest y Parámetros . INOUT es una palabra clave Swift que define un parámetro de función como referencia. Típicamente, los parámetros se pasan a la función como valores. Cuando escribe inout antes de un parámetro de función en una llamada, define este parámetro como un tipo de referencia. Para obtener más información sobre los argumentos inout, puede seguir este enlace. En resumen, inout le permite cambiar el valor de la variable en sí, que se pasó a la función, y no solo obtener su valor en el parámetro y trabajar con él dentro de la función. El protocolo ParameterEncoder se implementará en JSONParameterEncoder y en URLPameterEncoder .

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


ParameterEncoder contiene una única función cuya tarea es codificar parámetros. Este método puede arrojar un error que necesita ser manejado, así que usamos throw.

También puede ser útil producir errores no estándar, sino errores personalizados. Siempre es bastante difícil descifrar lo que te ofrece Xcode. Cuando tiene todos los errores personalizados y descritos, siempre sabe exactamente qué sucedió. Para hacer esto, definamos una enumeración que herede de Error .



Cree un archivo, asígnele el nombre URLParameterEncoder y colóquelo en la carpeta Codificación .



Este código toma una lista de parámetros, los convierte y los formatea para usarlos como parámetros de URL. Como sabes, algunos caracteres no están permitidos en la URL. Los parámetros también están separados por el símbolo "&", por lo que debemos ocuparnos de esto. También debemos establecer el valor predeterminado para los encabezados si no están establecidos en la solicitud.

Esta es la parte del código que se supone que debe estar cubierta por las pruebas unitarias. La clave es crear una solicitud de URL; de lo contrario, podemos provocar muchos errores innecesarios. Si usa la API abierta, obviamente no querrá usar el volumen completo posible de solicitudes de pruebas fallidas. Si desea saber más sobre las pruebas unitarias, puede comenzar con este artículo.

JSONParameterEncoder

Cree un archivo, asígnele el nombre JSONParameterEncoder y colóquelo en la carpeta Codificación.



Todo es igual que en el caso de URLParameter , solo aquí convertiremos los parámetros para JSON y nuevamente agregaremos los parámetros que definen la codificación "application / json" al encabezado.

Networkrouter

Cree un archivo, asígnele el nombre NetworkRouter y colóquelo en la carpeta Servicio. Comencemos definiendo typealias para el cierre.

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


A continuación, definimos el protocolo NetworkRouter .



NetworkRouter tiene un EndPoint que utiliza para solicitudes y, tan pronto como se completa la solicitud, el resultado de esta solicitud se pasa al cierre de NetworkRouterCompletion . El protocolo también tiene una función de cancelación , que puede usarse para interrumpir las solicitudes de carga y descarga a largo plazo. También usamos el tipo asociado aquí porque queremos que nuestro enrutador admita cualquier tipo de EndPointType . Sin usar el tipo asociado, el enrutador debería tener algún tipo específico que implemente EndPointType . Si desea saber más sobre el tipo asociado, puede leer este artículo .

Enrutador

Cree un archivo, asígnele el nombre Router y colóquelo en la carpeta Servicio. Declaramos una variable privada de tipo URLSessionTask . Todo el trabajo estará en ello. Lo hacemos privado porque no queremos que nadie afuera pueda cambiarlo.



Solicitud

Aquí creamos URLSession usando URLSession.shared , esta es la forma más fácil de crear. Pero recuerde que este método no es el único. Puede usar configuraciones de URLSession más complejas que pueden cambiar su comportamiento. Más sobre esto en este artículo .

La solicitud se crea llamando a la función buildRequest. La llamada a la función se envuelve en do-try-catch, porque las funciones de codificación dentro de buildRequest pueden generar excepciones. La respuesta , los datos y el error se pasan a la finalización.



Solicitud de compilación

Creamos nuestra solicitud utilizando la función buildRequest . Esta función es responsable de todo el trabajo vital en nuestra capa de red. Esencialmente convierte EndPointType a URLRequest . Y tan pronto como EndPoint se convierta en una solicitud, podemos pasarla a la sesión . Aquí están sucediendo muchas cosas, así que echemos un vistazo a los métodos. Primero, examinemos el método buildRequest :

1. Inicializamos la variable de solicitud URLRequest . Configuramos nuestra URL base y agregamos la ruta de la solicitud específica que se utilizará.

2. Asigne request.httpMethod al método http de nuestro EndPoint .

3. Creamos un bloque do-try-catch, porque nuestros codificadores pueden arrojar un error. Al crear un gran bloque do-try-catch, eliminamos la necesidad de crear un bloque separado para cada intento.

4. En switch, verifique route.task .

5. Dependiendo del tipo de tarea, llamamos al codificador correspondiente.



Configurar parámetros

Cree la función configureParameters en el enrutador.



Esta función es responsable de convertir nuestros parámetros de consulta. Dado que nuestra API supone el uso de bodyParameters en forma de JSON y URLParameters convertidos al formato URL, simplemente pasamos los parámetros apropiados a las funciones de conversión correspondientes, que describimos al comienzo del artículo. Si utiliza una API que incluye varios tipos de codificaciones, en este caso recomendaría agregar HTTPTask con una enumeración adicional con el tipo de codificación. Este listado debe contener todos los tipos posibles de codificaciones. Después de eso, en configureParameters agregue un argumento más con esta enumeración. Dependiendo de su valor, cambie usando el interruptor y realice la codificación que necesita.

Agregar encabezados adicionales

Cree la función addAdditionalHeaders en el enrutador.



Simplemente agregue todos los encabezados necesarios a la solicitud.

Cancelar

La función cancelar se verá bastante simple:



Ejemplo de uso

Ahora intentemos usar nuestra capa de red en un ejemplo real. Nos conectaremos a TheMovieDB para recibir datos para nuestra aplicación.

MovieEndPoint

Cree un archivo MovieEndPoint y colóquelo en la carpeta EndPoint. MovieEndPoint es lo mismo que
y TargetType en Moya. Aquí implementamos nuestro propio EndPointType en su lugar. Puede encontrar un artículo que describe cómo usar Moya para un ejemplo similar en este enlace .

 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 } } 


Modelo de película

Para analizar el modelo de datos MovieModel y JSON en el modelo, se utiliza el protocolo Decodable. Coloque este archivo en la carpeta Modelo .

Nota : para conocer más detalladamente los protocolos codificables, decodificables y codificables, puede leer mi otro artículo , que describe en detalle todas las características de trabajar con ellos.

 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) } } 


Administrador de red

Cree un archivo NetworkManager en la carpeta Administrador. Por el momento, NetworkManager contiene solo dos propiedades estáticas: una clave API y una enumeración que describe el tipo de servidor al que conectarse. NetworkManager también contiene un enrutador de tipo MovieApi .



Respuesta de red

Cree la enumeración NetworkResponse en NetworkManager.



Utilizamos esta enumeración cuando procesamos las respuestas a las solicitudes y mostraremos el mensaje correspondiente.

Resultado

Cree una enumeración de resultados en NetworkManager.



Usamos Resultado para determinar si la solicitud fue exitosa o no. Si no es así, le devolveremos un mensaje de error con el motivo.

Procesamiento de solicitud de respuesta

Cree la función handleNetworkResponse . Esta función toma un argumento, como un HTTPResponse, y devuelve Result.



En esta función, dependiendo del statusCode recibido de HTTPResponse, devolvemos un mensaje de error o un signo de una solicitud exitosa. Típicamente, un código en el rango 200..299 significa éxito.

Hacer una solicitud de red

Entonces, hemos hecho todo lo posible para comenzar a usar nuestra capa de red, intentemos hacer una solicitud.

Solicitaremos una lista de nuevas películas. Cree una función y asígnele el nombre getNewMovies .



Vamos a hacerlo paso a paso:

1. Definimos el método getNewMovies con dos argumentos: el número de página de paginación y el controlador de finalización, que devuelve una matriz opcional de modelos de películas o un error opcional.

2. Enrutador de llamadas. Pasamos el número de página y finalizamos el proceso en el cierre.

3. URLSession devuelve un error si no hay red o si no fue posible realizar una solicitud por algún motivo. Tenga en cuenta que esto no es un error de API, tales errores ocurren en el cliente y generalmente ocurren debido a la mala calidad de la conexión a Internet.

4. Necesitamos enviar nuestra respuesta a HTTPURLResponse , porque necesitamos acceder a la propiedad statusCode .

5. Declare el resultado e inicialícelo usando el método handleNetworkResponse

6. El éxito significa que la solicitud fue exitosa y recibimos la respuesta esperada. Luego verificamos si los datos vinieron con la respuesta, y si no, simplemente finalizamos el método mediante retorno.

7. Si la respuesta viene con datos, entonces es necesario analizar los datos recibidos en el modelo. Después de eso, pasamos el conjunto resultante de modelos a la finalización.

8. En caso de error, simplemente pase el error a su finalización .

Eso es todo, así es como nuestra propia capa de red funciona en Swift puro, sin usar dependencias en forma de pods y bibliotecas de terceros. Para realizar una solicitud de solicitud de prueba para obtener una lista de películas, cree un MainViewController con la propiedad NetworkManager y llame al método getNewMovies a través de él.

  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) } } } } 


Pequeño bono

¿Tuviste situaciones en Xcode cuando no entendiste qué tipo de marcador de posición se usa en un lugar en particular? Por ejemplo, mire el código que acabamos de escribir para el enrutador .



Determinamos el NetworkRouterCompletion nosotros mismos, pero incluso en este caso es fácil olvidar de qué tipo es y cómo usarlo. Pero nuestro querido Xcode se encargó de todo, y es suficiente con hacer doble clic en el marcador de posición y Xcode sustituirá al tipo deseado.



Conclusión

Ahora tenemos una implementación de una capa de red orientada al protocolo, que es muy fácil de usar y que siempre puede personalizar según sus necesidades. Entendimos su funcionalidad y cómo funcionan todos los mecanismos.

Puede encontrar el código fuente en este repositorio .

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


All Articles