Los mejores enfoques arquitectónicos para crear aplicaciones de red iOS (clientes REST)

323

Soy un desarrollador de iOS con algo de experiencia y esta pregunta es realmente interesante para mí. Vi muchos recursos y materiales diferentes sobre este tema, pero aún así estoy confundido. ¿Cuál es la mejor arquitectura para una aplicación en red iOS? Me refiero a un marco abstracto básico, patrones, que se ajustarán a todas las aplicaciones de red, ya sea una aplicación pequeña que solo tenga unas pocas solicitudes de servidor o un cliente REST complejo. Apple recomienda usarMVC como un enfoque arquitectónico básico para todas las aplicaciones de iOS, pero MVCni los MVVMpatrones más modernos explican dónde colocar el código lógico de la red y cómo organizarlo en general.

¿Necesito desarrollar algo como MVCS( Spara Service) y en esta Servicecapa poner todas las APIsolicitudes y otra lógica de red, que en perspectiva puede ser realmente compleja? Después de investigar un poco, encontré dos enfoques básicos para esto. Aquí se recomendó crear una clase separada para cada solicitud de red al servicio web API(como LoginRequestclase o PostCommentRequestclase, etc.) que hereda de la clase abstracta de solicitud base AbstractBaseRequesty además de crear un administrador de red global que encapsule el código de red común y otras preferencias (puede ser AFNetworkingpersonalización oRestKit ajuste, si tenemos asignaciones complejas de objetos y persistencia, o incluso una implementación de comunicación de red propia con API estándar). Pero este enfoque me parece una sobrecarga. Otro enfoque es tener algún APIdespachador singleton o clase de administrador como en el primer enfoque, pero no para crear clases para cada solicitud y en su lugar para encapsular cada petición como un método de instancia pública de esta clase como gerente: fetchContacts, loginUsermétodos, etc Por lo tanto, lo Cuál es la mejor y correcta forma? ¿Hay otros enfoques interesantes que aún no conozco?

¿Y debería crear otra capa para todas estas cosas de redes como Service, o NetworkProvidercapa o lo que sea sobre mi MVCarquitectura, o esta capa debería integrarse (inyectarse) en las MVCcapas existentes, por ejemplo Model?

Sé que existen enfoques hermosos, o ¿cómo entonces tales monstruos móviles como el cliente de Facebook o el cliente de LinkedIn lidian con la complejidad exponencialmente creciente de la lógica de redes?

Sé que no hay una respuesta exacta y formal al problema. El objetivo de esta pregunta es recopilar los enfoques más interesantes de desarrolladores experimentados de iOS . El mejor enfoque sugerido será marcado como aceptado y premiado con una recompensa de reputación, otros serán votados. Es sobre todo una pregunta teórica y de investigación. Quiero comprender el enfoque arquitectónico básico, abstracto y correcto para las aplicaciones de red en iOS. Espero una explicación detallada de desarrolladores experimentados.

MainstreamDeveloper00
fuente
14
¿No es esta una pregunta de "lista de compras"? Acabo de rechazar una pregunta que se votó al infierno y se cerró porque se dijo que las preguntas tipo "cuál es el mejor" provocan demasiado debate poco constructivo. ¿Qué hace que esta lista de compras sea una buena pregunta digna de votos positivos y una recompensa mientras que otros se cierran?
Alvin Thompson
1
Normalmente, la lógica de red iría al controlador, lo que alteraría un objeto modelo y notificaría a cualquier delegado u observador.
quellish
1
Muy interesantes preguntas y respuestas. Después de 4 años de codificación de iOS e intentando encontrar la forma más bella de agregar una capa de red a la aplicación. ¿Qué clase debería tener la responsabilidad de gestionar una solicitud de red? Las respuestas a continuación son realmente pertinentes. Gracias
darksider
@ JoeBlow esto no es cierto. La industria de las aplicaciones móviles todavía depende en gran medida de las comunicaciones servidor-cliente.
Scord

Respuestas:

327

I want to understand basic, abstract and correct architectural approach for networking applications in iOS: no existe un enfoque "el mejor" o "el más correcto" para crear una arquitectura de aplicación. Es un muy trabajo creativo. Siempre debe elegir la arquitectura más sencilla y extensible, que será clara para cualquier desarrollador, que comience a trabajar en su proyecto o para otros desarrolladores de su equipo, pero estoy de acuerdo, que puede haber un "bien" y un "mal" "arquitectura.

Usted dijo: collect the most interesting approaches from experienced iOS developersno creo que mi enfoque sea el más interesante o correcto, pero lo he usado en varios proyectos y estoy satisfecho con él. Es un enfoque híbrido de los que ha mencionado anteriormente, y también con mejoras de mis propios esfuerzos de investigación. Me interesan los problemas de los enfoques de construcción, que combinan varios patrones y modismos conocidos. Creo que muchos de los patrones empresariales de Fowler se pueden aplicar con éxito a las aplicaciones móviles. Aquí hay una lista de los más interesantes, que podemos aplicar para crear una arquitectura de aplicación iOS ( en mi opinión ): Capa de servicio , Unidad de trabajo , Fachada remota , Objeto de transferencia de datos ,Puerta de enlace , Supertipo de capa , Caso especial , Modelo de dominio . Siempre debe diseñar correctamente una capa de modelo y no olvidarse nunca de la persistencia (puede aumentar significativamente el rendimiento de su aplicación). Puedes usar Core Datapara esto. Pero no debe olvidar que Core Datano es un ORM o una base de datos, sino un administrador de gráficos de objetos con persistencia como una buena opción. Por lo tanto, muy a menudo Core Datapuede ser demasiado pesado para sus necesidades y puede buscar nuevas soluciones como Realm y Couchbase Lite , o crear su propia capa ligera de mapeo / persistencia de objetos, basada en SQLite sin procesar o LevelDB. También le aconsejo que se familiarice con el diseño impulsado por dominio y CQRS .

Al principio, creo, deberíamos crear otra capa para redes, porque no queremos controladores gordos o modelos pesados ​​y abrumados. No creo en esas fat model, skinny controllercosas. Pero sí creo en el skinny everythingenfoque, porque ninguna clase debería ser gorda, nunca. En general, todas las redes pueden abstraerse como lógica de negocios, por lo tanto, deberíamos tener otra capa, donde podamos ponerla. La capa de servicio es lo que necesitamos:

It encapsulates the application's business logic,  controlling transactions 
and coordinating responses in the implementation of its operations.

En nuestro MVCámbito Service Layeres algo así como un mediador entre el modelo de dominio y los controladores. Hay una variación bastante similar de este enfoque llamada MVCS donde a Storees en realidad nuestra Servicecapa. Storevende instancias de modelos y maneja las redes, el almacenamiento en caché, etc. Quiero mencionar que no debe escribir toda su lógica de negocios y redes en su capa de servicio. Esto también puede considerarse como un mal diseño. Para obtener más información, consulte los modelos de dominio Anemic y Rich . Algunos métodos de servicio y lógica de negocios pueden manejarse en el modelo, por lo que será un modelo "rico" (con comportamiento).

Siempre uso ampliamente dos bibliotecas: AFNetworking 2.0 y ReactiveCocoa . Creo que es imprescindible para cualquier aplicación moderna que interactúe con la red y los servicios web o que contenga una lógica de interfaz de usuario compleja.

ARQUITECTURA

Al principio creo una APIClientclase general , que es una subclase de AFHTTPSessionManager . Este es un caballo de batalla de todas las redes en la aplicación: todas las clases de servicio le delegan solicitudes REST reales. Contiene todas las personalizaciones del cliente HTTP, que necesito en la aplicación en particular: fijación SSL, procesamiento de errores y creación de NSErrorobjetos directos con razones detalladas de fallas y descripciones de todos APIy errores de conexión (en tal caso, el controlador podrá mostrar mensajes correctos para el usuario), configurando serializadores de solicitud y respuesta, encabezados http y otras cosas relacionadas con la red. Entonces lógicamente divido todas las solicitudes de la API en subservicios o, más correctamente, microservicios : UserSerivces, CommonServices, SecurityServices,FriendsServicesy así sucesivamente, de acuerdo con la lógica empresarial que implementan. Cada uno de estos microservicios es una clase separada. Ellos, juntos, forman a Service Layer. Estas clases contienen métodos para cada solicitud de API, procesan modelos de dominio y siempre devuelven un RACSignalcon el modelo de respuesta analizado o NSErroral llamante.

Quiero mencionar que si tiene una lógica de serialización de modelo compleja, cree otra capa para ella: algo como Data Mapper pero más general, por ejemplo, JSON / XML -> Model mapper. Si tiene caché: créelo también como una capa / servicio separado (no debe mezclar la lógica empresarial con el almacenamiento en caché). ¿Por qué? Porque la capa de almacenamiento en caché correcta puede ser bastante compleja con sus propios problemas. Las personas implementan una lógica compleja para obtener un almacenamiento en caché válido y predecible como, por ejemplo, almacenamiento en caché monoidal con proyecciones basadas en profunctores. Puedes leer sobre esta hermosa biblioteca llamada Carlos para entender más. Y no olvide que Core Data realmente puede ayudarlo con todos los problemas de almacenamiento en caché y le permitirá escribir menos lógica. Además, si tiene alguna lógica entre el repositorioNSManagedObjectContext modelos de solicitud del servidor, puede usarPatrón de , que separa la lógica que recupera los datos y los asigna al modelo de entidad de la lógica de negocios que actúa sobre el modelo. Por lo tanto, le recomiendo usar el patrón de repositorio incluso cuando tenga una arquitectura basada en Core Data. Repositorio puede cosas abstractas, como NSFetchRequest, NSEntityDescription, NSPredicatey así sucesivamente con los métodos de civil como geto put.

Después de todas estas acciones en la capa de Servicio, la persona que llama (controlador de vista) puede hacer algunas cosas complejas asincrónicas con la respuesta: manipulaciones de señal, encadenamiento, mapeo, etc. con la ayuda de ReactiveCocoaprimitivas, o simplemente suscribirse y mostrar resultados en la vista . Me inyecto con la inyección de dependencia en todas estas clases de servicios mis APIClient, lo que se traducirá una llamada de servicio particular, en los correspondientes GET, POST, PUT, DELETE, etc. solicitud al extremo REST. En este caso APIClientse pasa implícitamente a todos los controladores, puede hacer esto explícito con una parametrizada sobre APIClientclases de servicio. Esto puede tener sentido si desea utilizar diferentes personalizaciones deAPIClientpara clases de servicio particulares, pero si, por alguna razón, no desea copias adicionales o está seguro de que siempre usará una instancia en particular (sin personalizaciones) de APIClient- conviértalo en un singleton, pero NO, por favor NO HAGA Haga clases de servicio como singletons.

Luego, cada controlador de vista nuevamente con el DI inyecta la clase de servicio que necesita, llama a los métodos de servicio apropiados y compone sus resultados con la lógica de la interfaz de usuario. Para la inyección de dependencia, me gusta usar BloodMagic o un framework más potente Typhoon . Nunca uso singletons, APIManagerWhateverclase de Dios u otras cosas incorrectas. Porque si llamas a tu clase WhateverManager, esto indica que no conoces su propósito y es una mala elección de diseño . Singletons también es un antipatrón, y en la mayoría de los casos (excepto los raros) es una solución incorrecta . Singleton debe considerarse solo si se cumplen los tres criterios siguientes:

  1. La propiedad de la instancia única no se puede asignar razonablemente;
  2. La inicialización perezosa es deseable;
  3. El acceso global no está previsto de otra manera.

En nuestro caso, la propiedad de la instancia única no es un problema y tampoco necesitamos acceso global después de dividir a nuestro God Manager en servicios, porque ahora solo uno o varios controladores dedicados necesitan un servicio en particular (por ejemplo, UserProfilenecesidades de controladores, UserServicesetc.) .

Siempre debemos respetar los Sprincipios en SOLID y usar la separación de preocupaciones , así que no coloque todos sus métodos de servicio y llamadas de red en una clase, porque es una locura, especialmente si desarrolla una aplicación de gran empresa. Es por eso que debemos considerar la inyección de dependencia y el enfoque de servicios. Considero este enfoque como moderno y post-OO . En este caso, dividimos nuestra aplicación en dos partes: lógica de control (controladores y eventos) y parámetros.

Un tipo de parámetros serían los parámetros ordinarios de "datos". Eso es lo que pasamos alrededor de las funciones, manipular, modificar, persistir, etc. Estas son entidades, agregados, colecciones, clases de casos. El otro tipo serían los parámetros de "servicio". Estas son clases que encapsulan la lógica empresarial, permiten la comunicación con sistemas externos y proporcionan acceso a datos.

Aquí hay un flujo de trabajo general de mi arquitectura, por ejemplo. Supongamos que tenemos un FriendsViewController, que muestra la lista de amigos del usuario y tenemos una opción para eliminar de amigos. Creo un método en mi FriendsServicesclase llamado:

- (RACSignal *)removeFriend:(Friend * const)friend

donde Friendes un objeto modelo / dominio (o puede ser solo un Userobjeto si tienen atributos similares). Bajo el cofre este método análisis sintácticos Frienda NSDictionarylos parámetros JSON friend_id, name, surname, friend_request_idy así sucesivamente. Siempre uso la biblioteca Mantle para este tipo de repetitivo y para mi capa de modelo (análisis hacia adelante y hacia atrás, gestión de jerarquías de objetos anidados en JSON, etc.). Después de analizar que llama APIClient DELETEmétodo para hacer una solicitud de un descanso efectivo y regresa Responseen RACSignalque la persona que llama ( FriendsViewControlleren nuestro caso) para mostrar el mensaje adecuado para el usuario o lo que sea.

Si nuestra aplicación es muy grande, tenemos que separar nuestra lógica aún más claramente. Por ejemplo, no siempre es bueno mezclar Repositoryo modelar la lógica con Serviceuno. Cuando describí mi enfoque, dije que el removeFriendmétodo debería estar en la Servicecapa, pero si vamos a ser más pedantes, podemos notar que pertenece mejor Repository. Recordemos qué es el repositorio. Eric Evans le dio una descripción precisa en su libro [DDD]:

Un repositorio representa todos los objetos de cierto tipo como un conjunto conceptual. Actúa como una colección, excepto con una capacidad de consulta más elaborada.

Entonces, a Repositoryes esencialmente una fachada que usa semántica de estilo Colección (Agregar, Actualizar, Eliminar) para proporcionar acceso a datos / objetos. Es por eso que cuando se tiene algo como: getFriendsList, getUserGroups, removeFriendse puede colocar en el Repository, porque la recolección como la semántica está bastante claro aquí. Y código como:

- (RACSignal *)approveFriendRequest:(FriendRequest * const)request;

definitivamente es una lógica de negocios, porque está más allá de las CRUDoperaciones básicas y conecta dos objetos de dominio ( Friendy Request), es por eso que debe colocarse en la Servicecapa. También quiero notar: no cree abstracciones innecesarias . Use todos estos enfoques sabiamente. Porque si abruma su aplicación con abstracciones, esto aumentará su complejidad accidental, y la complejidad causa más problemas en los sistemas de software que cualquier otra cosa

Le describo un "viejo" ejemplo de Objective-C, pero este enfoque puede adaptarse muy fácilmente para el lenguaje Swift con muchas más mejoras, ya que tiene características más útiles y azúcar funcional. Recomiendo utilizar esta biblioteca: Moya . Le permite crear una APIClientcapa más elegante (nuestro caballo de batalla como recordará). Ahora nuestro APIClientproveedor será un tipo de valor (enumeración) con extensiones que se ajustan a los protocolos y que aprovechan la coincidencia de patrones de desestructuración. Las combinaciones rápidas de enumeraciones + patrones nos permiten crear tipos de datos algebraicos como en la programación funcional clásica. Nuestros microservicios utilizarán este APIClientproveedor mejorado como en el enfoque habitual de Objective-C. Para la capa de modelo en lugar de Mantlepuede usar la biblioteca ObjectMappero me gusta usar una biblioteca Argo más elegante y funcional .

Entonces, describí mi enfoque arquitectónico general, que creo que se puede adaptar a cualquier aplicación. Puede haber muchas más mejoras, por supuesto. Te aconsejo que aprendas programación funcional, porque puedes beneficiarte mucho de ella, pero no vayas demasiado lejos también. Eliminar, en general, un estado mutable global excesivo, compartido, crear un modelo de dominio inmutable o crear funciones puras sin efectos secundarios externos es, en general, una buena práctica, y un nuevo Swiftlenguaje fomenta esto. Pero recuerde siempre que sobrecargar su código con patrones funcionales puros y pesados, los enfoques teóricos de categoría es una mala idea, porque hay otras desarrolladores leerán y apoyarán su código, y pueden sentirse frustrados o asustados por elprismatic profunctorsy ese tipo de cosas en tu modelo inmutable. Lo mismo con el ReactiveCocoa: no RACifycodifique demasiado , porque puede volverse ilegible muy rápido, especialmente para los novatos. Úselo cuando realmente puede simplificar sus objetivos y lógica.

Por lo tanto, read a lot, mix, experiment, and try to pick up the best from different architectural approaches. Es el mejor consejo que puedo darte.

Oleksandr Karaberov
fuente
También un enfoque interesante y sólido. Gracias.
MainstreamDeveloper00
1
@darksider Como ya he escrito en mi respuesta: "` Nunca utilizo únicos, clase de Dios APIManagerWhatever u otras cosas mal, porque Singleton es un anti-patrón, y en la mayoría de los casos (excepto en las raras) es una solución equivocada. ". I don't like singletons. I have an opinion that if you decided to use singletons in your project you should have at least three criteria why you do this (I edited my answer). So I inject them (lazy of course and not each time, but vez `) en cada controlador.
Oleksandr Karaberov
14
Hola @alexander ¿Tienes algún proyecto de ejemplo en GitHub? Describes un enfoque muy interesante. Gracias. Pero soy un principiante en el desarrollo de Objective-C. Y para mí es difícil entender algunos aspectos. ¿Quizás puedas subir algún proyecto de prueba en GitHub y dar un enlace?
Denis
1
Hola @AlexanderKaraberov, estoy un poco confundido con respecto a la explicación de la tienda que diste. Supongamos que tengo 5 modelos, para cada uno tengo 2 clases, una que mantiene la creación de redes y otro almacenamiento en caché de objetos. Ahora debería tener una clase Store separada para cada modelo que llame a la función de clase de red y caché o una sola clase Store que tenga todas las funciones para cada modelo, de modo que el controlador siempre acceda a un solo archivo para datos.
meteoros
1
@icodebuster, este proyecto de demostración me ha estado ayudando a comprender muchos de los conceptos descritos aquí: github.com/darthpelo/NetworkLayerExample
31

De acuerdo con el objetivo de esta pregunta, me gustaría describir nuestro enfoque de arquitectura.

Enfoque de la arquitectura

La arquitectura general de nuestra aplicación iOS se basa en los siguientes patrones: capas de servicio , MVVM , enlace de datos de la interfaz de usuario , inyección de dependencias ; y paradigma de programación reactiva funcional .

Podemos dividir una aplicación típica orientada al consumidor en las siguientes capas lógicas:

  • Montaje
  • Modelo
  • Servicios
  • Almacenamiento
  • Gerentes
  • Coordinadores
  • UI
  • Infraestructura

La capa de ensamblaje es un punto de arranque de nuestra aplicación. Contiene un contenedor de Inyección de dependencias y declaraciones de los objetos de la aplicación y sus dependencias. Esta capa también puede contener la configuración de la aplicación (URL, claves de servicios de terceros, etc.). Para este propósito utilizamos la biblioteca Typhoon .

La capa de modelo contiene clases de modelos de dominio, validaciones, asignaciones. Utilizamos la biblioteca Mantle para mapear nuestros modelos: admite la serialización / deserialización en JSONformato y NSManagedObjectmodelos. Para la validación y la representación de formularios de nuestros modelos, utilizamos las bibliotecas FXForms y FXModelValidation .

La capa de servicios declara los servicios que usamos para interactuar con sistemas externos para enviar o recibir datos que están representados en nuestro modelo de dominio. Por lo general, tenemos servicios para la comunicación con las API del servidor (por entidad), servicios de mensajería (como PubNub ), servicios de almacenamiento (como Amazon S3), etc. Básicamente, los servicios envuelven objetos proporcionados por SDK (por ejemplo, PubNub SDK) o implementan su propia comunicación lógica. Para redes generales, utilizamos la biblioteca AFNetworking .

El objetivo de la capa de almacenamiento es organizar el almacenamiento local de datos en el dispositivo. Usamos Core Data o Realm para esto (ambos tienen pros y contras, la decisión de qué usar se basa en especificaciones concretas). Para la configuración de Core Data utilizamos la biblioteca MDMCoreData y un montón de clases (almacenamientos) (similares a los servicios) que proporcionan acceso al almacenamiento local para cada entidad. Para Realm solo usamos almacenamientos similares para tener acceso al almacenamiento local.

La capa de gerentes es un lugar donde viven nuestras abstracciones / envoltorios.

En un rol de gerente podría ser:

  • Credentials Manager con sus diferentes implementaciones (llavero, NSDefaults, ...)
  • Administrador de sesión actual que sabe cómo mantener y proporcionar sesión de usuario actual
  • Capture Pipeline que proporciona acceso a dispositivos multimedia (grabación de video, audio, toma de fotografías)
  • BLE Manager que proporciona acceso a servicios y periféricos bluetooth
  • Gerente de Geo Ubicación
  • ...

Por lo tanto, en el rol de administrador podría haber cualquier objeto que implemente la lógica de un aspecto particular o preocupación necesaria para el funcionamiento de la aplicación.

Intentamos evitar Singletons, pero esta capa es un lugar donde viven si es necesario.

La capa de coordinadores proporciona objetos que dependen de objetos de otras capas (Servicio, Almacenamiento, Modelo) para combinar su lógica en una secuencia de trabajo necesaria para cierto módulo (característica, pantalla, historia de usuario o experiencia de usuario). Por lo general, encadena operaciones asincrónicas y sabe cómo reaccionar ante sus casos de éxito y fracaso. Como ejemplo, puede imaginar una función de mensajería y el MessagingCoordinatorobjeto correspondiente . La operación de manejo de mensajes de envío podría verse así:

  1. Validar mensaje (capa de modelo)
  2. Guardar mensaje localmente (almacenamiento de mensajes)
  3. Subir archivo adjunto del mensaje (servicio amazon s3)
  4. Actualice el estado del mensaje y las URL de los archivos adjuntos y guarde el mensaje localmente (almacenamiento de mensajes)
  5. Serializar mensaje a formato JSON (capa de modelo)
  6. Publicar mensaje en PubNub (servicio PubNub)
  7. Actualice el estado y los atributos del mensaje y guárdelo localmente (almacenamiento de mensajes)

En cada uno de los pasos anteriores se maneja un error correspondientemente.

La capa de IU consta de las siguientes subcapas:

  1. ViewModels
  2. ViewControllers
  3. Puntos de vista

Para evitar los Controladores de vista masiva, usamos el patrón MVVM e implementamos la lógica necesaria para la presentación de la interfaz de usuario en ViewModels. Un ViewModel generalmente tiene coordinadores y gerentes como dependencias. ViewModels utilizados por ViewControllers y algunos tipos de Vistas (por ejemplo, celdas de vista de tabla). El pegamento entre ViewControllers y ViewModels es el enlace de datos y el patrón de comando. Para que sea posible tener ese pegamento, usamos la biblioteca ReactiveCocoa .

También utilizamos ReactiveCocoa y su RACSignalconcepto como interfaz y tipo de valor de retorno de todos los coordinadores, servicios, métodos de almacenamiento. Esto nos permite encadenar operaciones, ejecutarlas en paralelo o en serie, y muchas otras cosas útiles proporcionadas por ReactiveCocoa.

Intentamos implementar nuestro comportamiento de UI de manera declarativa. El enlace de datos y el diseño automático ayudan mucho a lograr este objetivo.

La capa de infraestructura contiene todos los ayudantes, extensiones y utilidades necesarias para el trabajo de la aplicación.


Este enfoque funciona bien para nosotros y para ese tipo de aplicaciones que generalmente creamos. Pero debe comprender que este es solo un enfoque subjetivo que debe adaptarse / cambiarse para el propósito del equipo concreto.

¡Espero que esto te ayudará!

También puede encontrar más información sobre el proceso de desarrollo de iOS en esta publicación de blog Desarrollo de iOS como servicio

Alex Petropavlovsky
fuente
Comenzó a gustarme esta arquitectura hace unos meses, ¡gracias Alex por compartirla! ¡Me gustaría probarlo con RxSwift en el futuro cercano!
ingaham
18

Debido a que todas las aplicaciones de iOS son diferentes, creo que hay diferentes enfoques a tener en cuenta aquí, pero generalmente voy de esta manera:
creo una clase de administrador central (singleton) para manejar todas las solicitudes de API (generalmente llamadas APICommunicator) y cada método de instancia es una llamada de API . Y hay un método central (no público):

-(RACSignal *)sendGetToServerToSubPath:(NSString *)path withParameters:(NSDictionary *)params;

Para el registro, uso 2 bibliotecas / marcos principales, ReactiveCocoa y AFNetworking. ReactiveCocoa maneja las respuestas de red asíncronas perfectamente, puede hacerlo (sendNext :, sendError :, etc.).
Este método llama a la API, obtiene los resultados y los envía a través de RAC en formato 'sin procesar' (como NSArray lo que devuelve AFNetworking).
Luego, un método como getStuffList:el llamado método anterior se suscribe a su señal, analiza los datos sin procesar en objetos (con algo como Motis) y envía los objetos uno por uno a la persona que llama ( getStuffList:y métodos similares también devuelven una señal a la que el controlador puede suscribirse )
El controlador suscrito recibe los objetos por subscribeNext:bloque y los maneja.

Probé de muchas maneras en diferentes aplicaciones, pero esta funcionó de la mejor manera posible, así que he estado usando esto en algunas aplicaciones recientemente, se adapta a proyectos pequeños y grandes y es fácil de extender y mantener si algo necesita ser modificado.
Espero que esto ayude, me gustaría escuchar las opiniones de los demás sobre mi enfoque y tal vez cómo otros piensan que esto podría mejorarse.

Rickye
fuente
2
Gracias por la respuesta +1. Buen enfoque. Os dejo la pregunta. Puede ser que tengamos otros enfoques de otros desarrolladores.
MainstreamDeveloper00
1
Me gusta una variación de este enfoque: uso un administrador central de API que se encarga de la mecánica de comunicarse con la API. Sin embargo, trato de hacer toda la funcionalidad expuesta en mis objetos modelo. Los modelos proporcionarán métodos como + (void)getAllUsersWithSuccess:(void(^)(NSArray*))success failure:(void(^)(NSError*))failure;y - (void)postWithSuccess:(void(^)(instancetype))success failure:(void(^)(NSError*))failure;que hacen los preparativos necesarios y luego llaman al administrador de API.
jsadler
1
Este enfoque es sencillo, pero a medida que aumenta el número de API, se hace más difícil mantener el administrador de API singleton. Y cada nueva API agregada se relacionará con el administrador, sin importar a qué módulo pertenezca esta API. Intente usar github.com/kevin0571/STNetTaskQueue para administrar las solicitudes de API.
Kevin
Aparte del punto de por qué anuncia su biblioteca, que está lo más lejos posible de mi solución y mucho más complicada, he probado este enfoque en innumerables proyectos, tanto pequeños como grandes, como se mencionó, y lo he estado utilizando exactamente lo mismo desde que escribí esta respuesta. Con convenciones de nomenclatura inteligentes, no es nada difícil de mantener.
Rickye
8

En mi situación, generalmente uso la biblioteca ResKit para configurar la capa de red. Proporciona un análisis fácil de usar. Reduce mi esfuerzo en configurar el mapeo para diferentes respuestas y otras cosas.

Solo agrego algo de código para configurar el mapeo automáticamente. Defino la clase base para mis modelos (no protocolo debido a la gran cantidad de código para verificar si algún método está implementado o no, y menos código en los modelos):

MappableEntry.h

@interface MappableEntity : NSObject

+ (NSArray*)pathPatterns;
+ (NSArray*)keyPathes;
+ (NSArray*)fieldsArrayForMapping;
+ (NSDictionary*)fieldsDictionaryForMapping;
+ (NSArray*)relationships;

@end

MappableEntry.m

@implementation MappableEntity

+(NSArray*)pathPatterns {
    return @[];
}

+(NSArray*)keyPathes {
    return nil;
}

+(NSArray*)fieldsArrayForMapping {
    return @[];
}

+(NSDictionary*)fieldsDictionaryForMapping {
    return @{};
}

+(NSArray*)relationships {
    return @[];
}

@end

Las relaciones son objetos que representan objetos anidados en respuesta:

RelationshipObject.h

@interface RelationshipObject : NSObject

@property (nonatomic,copy) NSString* source;
@property (nonatomic,copy) NSString* destination;
@property (nonatomic) Class mappingClass;

+(RelationshipObject*)relationshipWithKey:(NSString*)key andMappingClass:(Class)mappingClass;
+(RelationshipObject*)relationshipWithSource:(NSString*)source destination:(NSString*)destination andMappingClass:(Class)mappingClass;

@end

RelationshipObject.m

@implementation RelationshipObject

+(RelationshipObject*)relationshipWithKey:(NSString*)key andMappingClass:(Class)mappingClass {
    RelationshipObject* object = [[RelationshipObject alloc] init];
    object.source = key;
    object.destination = key;
    object.mappingClass = mappingClass;
    return object;
}

+(RelationshipObject*)relationshipWithSource:(NSString*)source destination:(NSString*)destination andMappingClass:(Class)mappingClass {
    RelationshipObject* object = [[RelationshipObject alloc] init];
    object.source = source;
    object.destination = destination;
    object.mappingClass = mappingClass;
    return object;
}

@end

Luego estoy configurando la asignación para RestKit de esta manera:

ObjectMappingInitializer.h

@interface ObjectMappingInitializer : NSObject

+(void)initializeRKObjectManagerMapping:(RKObjectManager*)objectManager;

@end

ObjectMappingInitializer.m

@interface ObjectMappingInitializer (Private)

+ (NSArray*)mappableClasses;

@end

@implementation ObjectMappingInitializer

+(void)initializeRKObjectManagerMapping:(RKObjectManager*)objectManager {

    NSMutableDictionary *mappingObjects = [NSMutableDictionary dictionary];

    // Creating mappings for classes
    for (Class mappableClass in [self mappableClasses]) {
        RKObjectMapping *newMapping = [RKObjectMapping mappingForClass:mappableClass];
        [newMapping addAttributeMappingsFromArray:[mappableClass fieldsArrayForMapping]];
        [newMapping addAttributeMappingsFromDictionary:[mappableClass fieldsDictionaryForMapping]];
        [mappingObjects setObject:newMapping forKey:[mappableClass description]];
    }

    // Creating relations for mappings
    for (Class mappableClass in [self mappableClasses]) {
        RKObjectMapping *mapping = [mappingObjects objectForKey:[mappableClass description]];
        for (RelationshipObject *relation in [mappableClass relationships]) {
            [mapping addPropertyMapping:[RKRelationshipMapping relationshipMappingFromKeyPath:relation.source toKeyPath:relation.destination withMapping:[mappingObjects objectForKey:[relation.mappingClass description]]]];
        }
    }

    // Creating response descriptors with mappings
    for (Class mappableClass in [self mappableClasses]) {
        for (NSString* pathPattern in [mappableClass pathPatterns]) {
            if ([mappableClass keyPathes]) {
                for (NSString* keyPath in [mappableClass keyPathes]) {
                    [objectManager addResponseDescriptor:[RKResponseDescriptor responseDescriptorWithMapping:[mappingObjects objectForKey:[mappableClass description]] method:RKRequestMethodAny pathPattern:pathPattern keyPath:keyPath statusCodes:RKStatusCodeIndexSetForClass(RKStatusCodeClassSuccessful)]];
                }
            } else {
                [objectManager addResponseDescriptor:[RKResponseDescriptor responseDescriptorWithMapping:[mappingObjects objectForKey:[mappableClass description]] method:RKRequestMethodAny pathPattern:pathPattern keyPath:nil statusCodes:RKStatusCodeIndexSetForClass(RKStatusCodeClassSuccessful)]];
            }
        }
    }

    // Error Mapping
    RKObjectMapping *errorMapping = [RKObjectMapping mappingForClass:[Error class]];
    [errorMapping addAttributeMappingsFromArray:[Error fieldsArrayForMapping]];
    for (NSString *pathPattern in Error.pathPatterns) {
        [[RKObjectManager sharedManager] addResponseDescriptor:[RKResponseDescriptor responseDescriptorWithMapping:errorMapping method:RKRequestMethodAny pathPattern:pathPattern keyPath:nil statusCodes:RKStatusCodeIndexSetForClass(RKStatusCodeClassClientError)]];
    }
}

@end

@implementation ObjectMappingInitializer (Private)

+ (NSArray*)mappableClasses {
    return @[
        [FruiosPaginationResults class],
        [FruioItem class],
        [Pagination class],
        [ContactInfo class],
        [Credentials class],
        [User class]
    ];
}

@end

Algunos ejemplos de implementación de MappableEntry:

Usuario.h

@interface User : MappableEntity

@property (nonatomic) long userId;
@property (nonatomic, copy) NSString *username;
@property (nonatomic, copy) NSString *email;
@property (nonatomic, copy) NSString *password;
@property (nonatomic, copy) NSString *token;

- (instancetype)initWithUsername:(NSString*)username email:(NSString*)email password:(NSString*)password;

- (NSDictionary*)registrationData;

@end

Usuario.m

@implementation User

- (instancetype)initWithUsername:(NSString*)username email:(NSString*)email password:(NSString*)password {
    if (self = [super init]) {
        self.username = username;
        self.email = email;
        self.password = password;
    }
    return self;
}

- (NSDictionary*)registrationData {
    return @{
        @"username": self.username,
        @"email": self.email,
        @"password": self.password
    };
}

+ (NSArray*)pathPatterns {
    return @[
        [NSString stringWithFormat:@"/api/%@/users/register", APIVersionString],
        [NSString stringWithFormat:@"/api/%@/users/login", APIVersionString]
    ];
}

+ (NSArray*)fieldsArrayForMapping {
    return @[ @"username", @"email", @"password", @"token" ];
}

+ (NSDictionary*)fieldsDictionaryForMapping {
    return @{ @"id": @"userId" };
}

@end

Ahora sobre el ajuste de solicitudes:

Tengo un archivo de encabezado con definición de bloques, para reducir la longitud de línea en todas las clases APIRequest:

APICallbacks.h

typedef void(^SuccessCallback)();
typedef void(^SuccessCallbackWithObjects)(NSArray *objects);
typedef void(^ErrorCallback)(NSError *error);
typedef void(^ProgressBlock)(float progress);

Y Ejemplo de mi clase APIRequest que estoy usando:

LoginAPI.h

@interface LoginAPI : NSObject

- (void)loginWithCredentials:(Credentials*)credentials onSuccess:(SuccessCallbackWithObjects)onSuccess onError:(ErrorCallback)onError;

@end

LoginAPI.m

@implementation LoginAPI

- (void)loginWithCredentials:(Credentials*)credentials onSuccess:(SuccessCallbackWithObjects)onSuccess onError:(ErrorCallback)onError {
    [[RKObjectManager sharedManager] postObject:nil path:[NSString stringWithFormat:@"/api/%@/users/login", APIVersionString] parameters:[credentials credentialsData] success:^(RKObjectRequestOperation *operation, RKMappingResult *mappingResult) {
        onSuccess(mappingResult.array);
    } failure:^(RKObjectRequestOperation *operation, NSError *error) {
        onError(error);
    }];
}

@end

Y todo lo que necesita hacer en código, simplemente inicialice el objeto API y llámelo siempre que lo necesite:

SomeViewController.m

@implementation SomeViewController {
    LoginAPI *_loginAPI;
    // ...
}

- (void)viewDidLoad {
    [super viewDidLoad];

    _loginAPI = [[LoginAPI alloc] init];
    // ...
}

// ...

- (IBAction)signIn:(id)sender {
    [_loginAPI loginWithCredentials:_credentials onSuccess:^(NSArray *objects) {
        // Success Block
    } onError:^(NSError *error) {
        // Error Block
    }];
}

// ...

@end

Mi código no es perfecto, pero es fácil de configurar una vez y usar para diferentes proyectos. Si es interesante para alguien, podría pasar algo de tiempo y encontrar una solución universal en algún lugar de GitHub y CocoaPods.

Andrew Cherkashyn
fuente
7

En mi opinión, toda la arquitectura de software está impulsada por la necesidad. Si esto es para fines de aprendizaje o personales, entonces decida el objetivo principal y haga que eso impulse la arquitectura. Si este es un trabajo por contrato, entonces la necesidad del negocio es primordial. El truco es no dejar que las cosas brillantes te distraigan de las necesidades reales. Encuentro esto difícil de hacer. Siempre hay cosas nuevas y brillantes que aparecen en este negocio y muchas de ellas no son útiles, pero no siempre se puede decir por adelantado. Concéntrese en la necesidad y esté dispuesto a abandonar las malas elecciones si puede.

Por ejemplo, recientemente hice un prototipo rápido de una aplicación para compartir fotos para un negocio local. Dado que la necesidad comercial era hacer algo rápido y sucio, la arquitectura terminó siendo un código de iOS para abrir una cámara y un código de red adjunto a un botón de envío que cargó la imagen en una tienda S3 y escribió en un dominio SimpleDB. El código era trivial y el costo mínimo, y el cliente tiene una colección de fotos escalable accesible a través de la web con llamadas REST. Barato y tonto, la aplicación tenía muchos defectos y en ocasiones bloqueaba la interfaz de usuario, pero sería un desperdicio hacer más por un prototipo y les permite desplegarse en su personal y generar miles de imágenes de prueba fácilmente sin rendimiento o escalabilidad preocupaciones Arquitectura mala, pero se ajusta a la necesidad y cuesta perfectamente.

Otro proyecto consistió en implementar una base de datos segura local que se sincroniza con el sistema de la empresa en segundo plano cuando la red está disponible. Creé un sincronizador de fondo que usaba RestKit, ya que parecía tener todo lo que necesitaba. Pero tuve que escribir tanto código personalizado para RestKit para tratar con JSON idiosincrásico que podría haberlo hecho todo más rápido escribiendo mi propio JSON en las transformaciones de CoreData. Sin embargo, el cliente quería traer esta aplicación internamente y sentí que RestKit sería similar a los marcos que usaban en otras plataformas. Espero a ver si esa fue una buena decisión.

Nuevamente, el problema para mí es centrarme en la necesidad y dejar que eso determine la arquitectura. Trato de evitar el uso de paquetes de terceros, ya que traen costos que solo aparecen después de que la aplicación ha estado en el campo por un tiempo. Intento evitar hacer jerarquías de clase, ya que rara vez dan sus frutos. Si puedo escribir algo en un período de tiempo razonable en lugar de adoptar un paquete que no encaja perfectamente, entonces lo hago. Mi código está bien estructurado para la depuración y está debidamente comentado, pero los paquetes de terceros rara vez lo están. Dicho esto, considero que las redes AF son demasiado útiles para ignorarlas y están bien estructuradas, bien comentadas y mantenidas, ¡y las uso mucho! RestKit cubre muchos casos comunes, pero siento que he estado en una pelea cuando lo uso, y la mayoría de las fuentes de datos que encuentro están llenas de peculiaridades y problemas que se manejan mejor con código personalizado. En mis últimas aplicaciones, solo uso los convertidores JSON integrados y escribo algunos métodos de utilidad.

Un patrón que siempre uso es sacar las llamadas de red del hilo principal. Las últimas 4-5 aplicaciones que he realizado configuraron una tarea de temporizador en segundo plano utilizando dispatch_source_create que se activa cada cierto tiempo y realiza tareas de red según sea necesario. Debe hacer un trabajo de seguridad de subprocesos y asegurarse de que el código de modificación de la interfaz de usuario se envíe al subproceso principal. También ayuda a hacer su incorporación / inicialización de tal manera que el usuario no se sienta abrumado o retrasado. Hasta ahora esto ha funcionado bastante bien. Sugiero buscar en estas cosas.

Finalmente, creo que a medida que trabajamos más y a medida que el sistema operativo evoluciona, tendemos a desarrollar mejores soluciones. Me ha llevado años superar mi creencia de que tengo que seguir patrones y diseños que otras personas afirman que son obligatorios. Si estoy trabajando en un contexto donde eso es parte de la religión local, ejem, me refiero a las mejores prácticas de ingeniería departamentales, entonces sigo las costumbres al pie de la letra, eso es por lo que me están pagando. Pero rara vez encuentro que seguir diseños y patrones más antiguos es la solución óptima. Siempre trato de ver la solución a través del prisma de las necesidades del negocio y construir la arquitectura para que coincida y mantener las cosas lo más simples posible. Cuando siento que no hay suficiente allí, pero todo funciona correctamente, entonces estoy en el camino correcto.

Fran K.
fuente
4

Utilizo el enfoque que obtuve de aquí: https://github.com/Constantine-Fry/Foursquare-API-v2 . He reescrito esa biblioteca en Swift y puedes ver el enfoque arquitectónico de estas partes del código:

typealias OpertaionCallback = (success: Bool, result: AnyObject?) -> ()

class Foursquare{
    var authorizationCallback: OperationCallback?
    var operationQueue: NSOperationQueue
    var callbackQueue: dispatch_queue_t?

    init(){
        operationQueue = NSOperationQueue()
        operationQueue.maxConcurrentOperationCount = 7;
        callbackQueue = dispatch_get_main_queue();
    }

    func checkIn(venueID: String, shout: String, callback: OperationCallback) -> NSOperation {
        let parameters: Dictionary <String, String> = [
            "venueId":venueID,
            "shout":shout,
            "broadcast":"public"]
        return self.sendRequest("checkins/add", parameters: parameters, httpMethod: "POST", callback: callback)
    }

    func sendRequest(path: String, parameters: Dictionary <String, String>, httpMethod: String, callback:OperationCallback) -> NSOperation{
        let url = self.constructURL(path, parameters: parameters)
        var request = NSMutableURLRequest(URL: url)
        request.HTTPMethod = httpMethod
        let operation = Operation(request: request, callbackBlock: callback, callbackQueue: self.callbackQueue!)
        self.operationQueue.addOperation(operation)
        return operation
    }

    func constructURL(path: String, parameters: Dictionary <String, String>) -> NSURL {
        var parametersString = kFSBaseURL+path
        var firstItem = true
        for key in parameters.keys {
            let string = parameters[key]
            let mark = (firstItem ? "?" : "&")
            parametersString += "\(mark)\(key)=\(string)"
            firstItem = false
        }
    return NSURL(string: parametersString.stringByAddingPercentEscapesUsingEncoding(NSUTF8StringEncoding))
    }
}

class Operation: NSOperation {
    var callbackBlock: OpertaionCallback
    var request: NSURLRequest
    var callbackQueue: dispatch_queue_t

    init(request: NSURLRequest, callbackBlock: OpertaionCallback, callbackQueue: dispatch_queue_t) {
        self.request = request
        self.callbackBlock = callbackBlock
        self.callbackQueue = callbackQueue
    }

    override func main() {
        var error: NSError?
        var result: AnyObject?
        var response: NSURLResponse?

        var recievedData: NSData? = NSURLConnection.sendSynchronousRequest(self.request, returningResponse: &response, error: &error)

        if self.cancelled {return}

        if recievedData{
            result = NSJSONSerialization.JSONObjectWithData(recievedData, options: nil, error: &error)
            if result != nil {
                if result!.isKindOfClass(NSClassFromString("NSError")){
                    error = result as? NSError
            }
        }

        if self.cancelled {return}

        dispatch_async(self.callbackQueue, {
            if (error) {
                self.callbackBlock(success: false, result: error!);
            } else {
                self.callbackBlock(success: true, result: result!);
            }
            })
    }

    override var concurrent:Bool {get {return true}}
}

Básicamente, hay una subclase de NSOperation que hace NSURLRequest, analiza la respuesta JSON y agrega el bloque de devolución de llamada con el resultado a la cola. La clase API principal construye NSURLRequest, inicializa esa subclase NSOperation y la agrega a la cola.

bzz
fuente
3

Utilizamos algunos enfoques dependiendo de la situación. Para la mayoría de las cosas, AFNetworking es el enfoque más simple y robusto, ya que puede configurar encabezados, cargar datos de varias partes, usar GET, POST, PUT & DELETE y hay un montón de categorías adicionales para UIKit que le permiten, por ejemplo, establecer una imagen desde una url En una aplicación compleja con muchas llamadas, a veces resumimos esto en un método de conveniencia propio que sería algo como:

-(void)makeRequestToUrl:(NSURL *)url withParameters:(NSDictionary *)parameters success:(void (^)(id responseObject))success failure:(void (^)(AFHTTPRequestOperation *operation, NSError *error))failure;

Sin embargo, hay algunas situaciones en las que AFNetworking no es apropiado, como cuando está creando un marco u otro componente de la biblioteca, ya que AFNetworking ya puede estar en otra base de código. En esta situación, utilizaría una NSMutableURLRequest en línea si realiza una sola llamada o se abstrae en una clase de solicitud / respuesta.

Martín
fuente
Para mí esta es la mejor y más clara respuesta, saludos. "Es así de simple". @martin, personalmente solo usamos NSMutableURLRequest todo el tiempo; ¿Hay alguna razón real para usar AFNetworking?
Fattie
AFNetworking es realmente conveniente. Para mí, los bloques de éxito y fracaso hacen que valga la pena, ya que hace que el código sea más fácil de administrar. Sin embargo, acepto que a veces es una exageración total.
Martin
Un excelente punto en los bloques, gracias por eso. Supongo que la naturaleza específica de todo esto cambiará con Swift.
Fattie
2

Evito los singletons al diseñar mis aplicaciones. Son una opción típica para muchas personas, pero creo que puedes encontrar soluciones más elegantes en otros lugares. Por lo general, lo que hago es construir mis entidades en CoreData y luego poner mi código REST en una categoría NSManagedObject. Si, por ejemplo, quisiera crear y PUBLICAR un nuevo usuario, haría esto:

User* newUser = [User createInManagedObjectContext:managedObjectContext];
[newUser postOnSuccess:^(...) { ... } onFailure:^(...) { ... }];

Uso RESTKit para el mapeo de objetos y lo inicializo al inicio. Considero que enrutar todas sus llamadas a través de un singleton es una pérdida de tiempo y agrega muchas repeticiones que no son necesarias.

En NSManagedObject + Extensions.m:

+ (instancetype)createInContext:(NSManagedObjectContext*)context
{
    NSAssert(context.persistentStoreCoordinator.managedObjectModel.entitiesByName[[self entityName]] != nil, @"Entity with name %@ not found in model. Is your class name the same as your entity name?", [self entityName]);
    return [NSEntityDescription insertNewObjectForEntityForName:[self entityName] inManagedObjectContext:context];
}

En NSManagedObject + Networking.m:

- (void)getOnSuccess:(RESTSuccess)onSuccess onFailure:(RESTFailure)onFailure blockInput:(BOOL)blockInput
{
    [[RKObjectManager sharedManager] getObject:self path:nil parameters:nil success:onSuccess failure:onFailure];
    [self handleInputBlocking:blockInput];
}

¿Por qué agregar clases auxiliares adicionales cuando puede ampliar la funcionalidad de una clase base común a través de categorías?

Si está interesado en información más detallada sobre mi solución, hágamelo saber. Estoy feliz de compartir

Sandy Chapman
fuente
3
Definitivamente estaría interesado en leer sobre este enfoque con más detalle en una publicación de blog.
Danyal Aytekin
0

Desde una perspectiva de diseño puramente de clase, generalmente tendrá algo como esto:

  • Sus controladores de vista controlan una o más vistas
  • Clase de modelo de datos : realmente depende de cuántas entidades distintas reales esté tratando y cómo estén relacionadas.

    Por ejemplo, si tiene una matriz de elementos para mostrar en cuatro representaciones diferentes (lista, gráfico, gráfico, etc.), tendrá una clase de modelo de datos para la lista de elementos, uno más para un elemento. La lista de clases de elementos será compartida por cuatro controladores de vista: todos los hijos de un controlador de barra de pestañas o un controlador de navegación.

    Las clases de modelos de datos serán útiles no solo para mostrar datos, sino también para serializarlos en los que cada uno de ellos puede exponer su propio formato de serialización a través de métodos de exportación JSON / XML / CSV (o cualquier otra cosa).

  • Es importante comprender que también necesita clases de creador de solicitudes de API que se asignen directamente con sus puntos finales de API REST. Supongamos que tiene una API que inicia sesión en el usuario, por lo que su clase de generador de API de inicio de sesión creará la carga útil POST JSON para la API de inicio de sesión. En otro ejemplo, una clase de generador de solicitud de API para la lista de elementos de catálogo API creará una cadena de consulta GET para la API correspondiente y activará la consulta REST GET.

    Estas clases de generador de solicitudes de API generalmente recibirán datos de los controladores de vista y también pasarán los mismos datos a los controladores de vista para la actualización de la interfaz de usuario / otras operaciones. Los controladores de vista decidirán cómo actualizar los objetos del Modelo de datos con esos datos.

  • Finalmente, el corazón del cliente REST : la clase de captador de datos API que no tiene en cuenta todo tipo de solicitudes API que realiza su aplicación. Es probable que esta clase sea un singleton, pero como otros señalaron, no tiene que ser un singleton.

    Tenga en cuenta que el enlace es solo una implementación típica y no tiene en cuenta escenarios como sesión, cookies, etc., pero es suficiente para que pueda continuar sin usar marcos de terceros.

Nirav Bhatt
fuente
0

Esta pregunta ya tiene muchas respuestas excelentes y extensas, pero siento que debo mencionarla ya que nadie más la tiene.

Alamofire para Swift. https://github.com/Alamofire/Alamofire

Fue creado por las mismas personas que AFNetworking, pero está diseñado más directamente con Swift en mente.

matt.writes.code
fuente
0

Creo que por ahora el proyecto mediano usa la arquitectura MVVM y el proyecto grande usa la arquitectura VIPER y trata de lograr

  • Programación orientada al protocolo
  • Patrones de diseño de software
  • Principio VENDIDO
  • Programación genérica
  • No te repitas (SECO)

Y enfoques arquitectónicos para crear aplicaciones de red iOS (clientes REST)

La preocupación por la separación del código limpio y legible evita la duplicación:

import Foundation
enum DataResponseError: Error {
    case network
    case decoding

    var reason: String {
        switch self {
        case .network:
            return "An error occurred while fetching data"
        case .decoding:
            return "An error occurred while decoding data"
        }
    }
}

extension HTTPURLResponse {
    var hasSuccessStatusCode: Bool {
        return 200...299 ~= statusCode
    }
}

enum Result<T, U: Error> {
    case success(T)
    case failure(U)
}

inversión de dependencia

 protocol NHDataProvider {
        func fetchRemote<Model: Codable>(_ val: Model.Type, url: URL, completion: @escaping (Result<Codable, DataResponseError>) -> Void)
    }

Responsable principal:

  final class NHClientHTTPNetworking : NHDataProvider {

        let session: URLSession

        init(session: URLSession = URLSession.shared) {
            self.session = session
        }

        func fetchRemote<Model: Codable>(_ val: Model.Type, url: URL,
                             completion: @escaping (Result<Codable, DataResponseError>) -> Void) {
            let urlRequest = URLRequest(url: url)
            session.dataTask(with: urlRequest, completionHandler: { data, response, error in
                guard
                    let httpResponse = response as? HTTPURLResponse,
                    httpResponse.hasSuccessStatusCode,
                    let data = data
                    else {
                        completion(Result.failure(DataResponseError.network))
                        return
                }
                guard let decodedResponse = try? JSONDecoder().decode(Model.self, from: data) else {
                    completion(Result.failure(DataResponseError.decoding))
                    return
                }
                completion(Result.success(decodedResponse))
            }).resume()
        }
    }

Encontrarás aquí la arquitectura GitHub MVVM con el resto API Swift Project

Nazmul Hasan
fuente
0

En ingeniería de software móvil, los más utilizados son los patrones Clean Architecture + MVVM y Redux.

Clean Architecture + MVVM consta de 3 capas: Dominio, Presentación, capas de datos. Donde la capa de presentación y la capa de repositorios de datos dependen de la capa de dominio:

Presentation Layer -> Domain Layer <- Data Repositories Layer

Y la capa de presentación consta de modelos de vista y vistas (MVVM):

Presentation Layer (MVVM) = ViewModels + Views
Domain Layer = Entities + Use Cases + Repositories Interfaces
Data Repositories Layer = Repositories Implementations + API (Network) + Persistence DB

En este artículo, hay una descripción más detallada de Clean Architecture + MVVM https://tech.olx.com/clean-architecture-and-mvvm-on-ios-c9d167d9f5b3

Oleh Kudinov
fuente