Simplificando el uso de API RESTful y la persistencia de datos en iOS con Mantle y Realm

Publicado: 2022-03-11

Todos los desarrolladores de iOS están familiarizados con Core Data, un gráfico de objetos y un marco de persistencia de Apple. Además de la persistencia de datos localmente, el marco viene con una serie de funciones avanzadas, como el seguimiento de cambios de objetos y deshacer. Estas funciones, aunque útiles en muchos casos, no son gratuitas. Requiere una gran cantidad de código repetitivo, y el marco en su conjunto tiene una curva de aprendizaje pronunciada.

En 2014, se lanzó Realm, una base de datos móvil, que revolucionó el mundo del desarrollo. Si todo lo que necesitamos es persistir los datos localmente, Realm es una buena alternativa. Después de todo, no todos los casos de uso requieren las características avanzadas de Core Data. Realm es extremadamente fácil de usar y, a diferencia de Core Data, requiere muy poco código repetitivo. También es seguro para subprocesos y se dice que es más rápido que el marco de persistencia de Apple.

En la mayoría de las aplicaciones móviles modernas, la persistencia de datos resuelve la mitad del problema. A menudo necesitamos obtener datos de un servicio remoto, generalmente a través de una API RESTful. Aquí es donde entra en juego Manto. Es un marco modelo de código abierto para Cocoa y Cocoa Touch. Mantle simplifica significativamente la escritura de modelos de datos para interactuar con las API que usan JSON como formato de intercambio de datos.

Reino y manto para iOS

En este artículo, crearemos una aplicación para iOS que obtiene una lista de artículos junto con enlaces a ellos desde la API de búsqueda de artículos del New York Times v2. La lista se obtendrá mediante una solicitud HTTP GET estándar, con modelos de solicitud y respuesta creados con Mantle. Veremos qué tan fácil es con Mantle manejar transformaciones de valores (por ejemplo, de NSDate a cadena). Una vez que se obtengan los datos, los conservaremos localmente usando Realm. Todo esto con un código repetitivo mínimo.

API RESTful - Primeros pasos

Comencemos creando un nuevo proyecto Xcode de "aplicación maestra-detalle" para iOS llamado "RealmMantleTutorial". Le agregaremos marcos usando CocoaPods. El podfile debe parecerse a lo siguiente:

 pod 'Mantle' pod 'Realm' pod 'AFNetworking'

Una vez que se instalan los pods, podemos abrir el espacio de trabajo MantleRealmTutorial recién creado. Como habrás notado, también se ha instalado el famoso framework AFNetworking. Lo usaremos para realizar solicitudes a la API.

Como se mencionó en la introducción, New York Times proporciona una excelente API de búsqueda de artículos. Para usarlo, uno debe registrarse para obtener una clave de acceso a la API. Esto se puede hacer en http://developer.nytimes.com. Con la clave API en la mano, estamos listos para comenzar con la codificación.

Antes de profundizar en la creación de modelos de datos de Mantle, debemos poner en funcionamiento nuestra capa de red. Vamos a crear un nuevo grupo en Xcode y llamarlo Red. En este grupo crearemos dos clases. Llamemos al primero SessionManager y asegurémonos de que se deriva de AFHTTPSessionManager, que es una clase de administrador de sesiones de AFNetworking , el encantador marco de trabajo en red. Nuestra clase SessionManager será un objeto único que usaremos para realizar solicitudes de obtención a la API. Una vez que se haya creado la clase, copie el código a continuación en la interfaz y los archivos de implementación, respectivamente.

 #import "AFHTTPSessionManager.h" @interface SessionManager : AFHTTPSessionManager + (id)sharedManager; @end
 #import "SessionManager.h" static NSString *const kBaseURL = @"http://api.nytimes.com"; @implementation SessionManager - (id)init { self = [super initWithBaseURL:[NSURL URLWithString:kBaseURL]]; if(!self) return nil; self.responseSerializer = [AFJSONResponseSerializer serializer]; self.requestSerializer = [AFJSONRequestSerializer serializer]; return self; } + (id)sharedManager { static SessionManager *_sessionManager = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ _sessionManager = [[self alloc] init]; }); return _sessionManager; } @end

El administrador de sesión se inicializa con la URL base definida en la variable estática kBaseURL . También utilizará serializadores de solicitud y respuesta JSON.

Ahora la segunda clase que vamos a crear en el grupo Network se llamará APIManager . Se derivará de nuestra clase SessionManager recién creada. Una vez que se creen los modelos de datos necesarios, agregaremos un método a ApiManager que se utilizará para solicitar una lista de artículos de la API.

Descripción general de la API de búsqueda de artículos del New York Times

La documentación oficial de esta excelente API está disponible en http://developer.nytimes.com/…/article_search_api_v2. Lo que vamos a hacer es usar el siguiente punto final:

 http://api.nytimes.com/svc/search/v2/articlesearch

… para buscar artículos encontrados utilizando un término de consulta de búsqueda de nuestra elección delimitado por un rango de fechas. Por ejemplo, lo que podríamos hacer es pedirle a la API que devuelva una lista de todos los artículos que aparecieron en el New York Times que tenían algo que ver con el baloncesto en los primeros siete días de julio de 2015. De acuerdo con la documentación de la API, para hacer eso necesitamos establecer los siguientes parámetros en la solicitud de obtención a ese punto final:

Parámetro Valor
q "baloncesto"
fecha de inicio “20150701”
fecha final “20150707”

La respuesta de la API es bastante compleja. A continuación se muestra la respuesta a una solicitud con los parámetros anteriores limitados a un solo artículo (un elemento en la matriz de documentos) con numerosos campos omitidos para mayor claridad.

 { "response": { "docs": [ { "web_url": "http://www.nytimes.com/2015/07/04/sports/basketball/robin-lopez-and-knicks-are-close-to-a-deal.html", "lead_paragraph": "Lopez, a 7-foot center, joined Arron Afflalo, a 6-foot-5 guard, as the Knicks' key acquisitions in free agency. He is expected to solidify the Knicks' interior defense.", "abstract": null, "print_page": "1", "source": "The New York Times", "pub_date": "2015-07-04T00:00:00Z", "document_type": "article", "news_desk": "Sports", "section_name": "Sports", "subsection_name": "Pro Basketball", "type_of_material": "News", "_id": "5596e7ac38f0d84c0655cb28", "word_count": "879" } ] }, "status": "OK", "copyright": "Copyright (c) 2013 The New York Times Company. All Rights Reserved." }

Lo que básicamente obtenemos como respuesta son tres campos. El primero llamado respuesta contiene la matriz docs , que a su vez contiene elementos que representan artículos. Los otros dos campos son el estado y los derechos de autor . Ahora que sabemos cómo funciona la API, es hora de crear modelos de datos usando Mantle.

Introducción al Manto

Como se mencionó anteriormente, Mantle es un marco de código abierto que simplifica significativamente la escritura de modelos de datos. Comencemos por crear un modelo de solicitud de lista de artículos. Llamemos a esta clase ArticleListRequestModel y asegurémonos de que se deriva de MTLModel , que es una clase de la que se deben derivar todos los modelos de Mantle. Además, hagamos que se ajuste al protocolo MTLJSONSerializing . Nuestro modelo de solicitud debe tener tres propiedades de tipos adecuados: consulta, artículos desde la fecha y artículos a la fecha. Solo para asegurarnos de que nuestro proyecto esté bien organizado, sugiero que esta clase se coloque en el grupo Modelos .

Mantle simplifica la escritura de modelos de datos, reduce el código repetitivo.
Pío

Así es como debería verse el archivo de interfaz de ArticleListRequestModel :

 #import "MTLModel.h" #import "Mantle.h" @interface ArticleListRequestModel : MTLModel <MTLJSONSerializing> @property (nonatomic, copy) NSString *query; @property (nonatomic, copy) NSDate *articlesFromDate; @property (nonatomic, copy) NSDate *articlesToDate; @end

Ahora, si buscamos los documentos para nuestro punto final de búsqueda de artículos o echamos un vistazo a la tabla con los parámetros de solicitud anteriores, notaremos que los nombres de las variables en la solicitud API difieren de los de nuestro modelo de solicitud. Mantle maneja esto de manera eficiente usando el método:

 + (NSDictionary *)JSONKeyPathsByPropertyKey.

Así es como se debe implementar este método en la implementación de nuestro modelo de solicitud:

 #import "ArticleListRequestModel.h" @implementation ArticleListRequestModel #pragma mark - Mantle JSONKeyPathsByPropertyKey + (NSDictionary *)JSONKeyPathsByPropertyKey { return @{ @"query": @"q", @"articlesFromDate": @"begin_date", @"articlesToDate": @"end_date" }; } @end

La implementación de este método especifica cómo se asignan las propiedades del modelo a sus representaciones JSON. Una vez que se ha implementado el método JSONKeyPathsByPropertyKey , podemos obtener una representación de diccionario JSON del modelo con el método de clase +[MTLJSONAdapter JSONArrayForModels:] .

Una cosa que aún queda, como sabemos por la lista de parámetros, es que ambos parámetros de fecha deben estar en el formato "AAAAMMDD". Aquí es donde Mantle se vuelve muy útil. Podemos agregar una transformación de valor personalizado para cualquier propiedad implementando el método opcional +<propertyName>JSONTransformer . Al implementarlo, le decimos a Mantle cómo se debe transformar el valor de un campo JSON específico durante la deserialización de JSON. También podemos implementar un transformador reversible que se utilizará al crear un JSON a partir del modelo. Dado que necesitamos transformar un objeto NSDate en una cadena, también utilizaremos la clase NSDataFormatter . Aquí está la implementación completa de la clase ArticleListRequestModel :

 #import "ArticleListRequestModel.h" @implementation ArticleListRequestModel + (NSDateFormatter *)dateFormatter { NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; dateFormatter.dateFormat = @"yyyyMMdd"; return dateFormatter; } #pragma mark - Mantle JSONKeyPathsByPropertyKey + (NSDictionary *)JSONKeyPathsByPropertyKey { return @{ @"query": @"q", @"articlesFromDate": @"begin_date", @"articlesToDate": @"end_date" }; } #pragma mark - JSON Transformers + (NSValueTransformer *)articlesToDateJSONTransformer { return [MTLValueTransformer transformerUsingForwardBlock:^id(NSString *dateString, BOOL *success, NSError *__autoreleasing *error) { return [self.dateFormatter dateFromString:dateString]; } reverseBlock:^id(NSDate *date, BOOL *success, NSError *__autoreleasing *error) { return [self.dateFormatter stringFromDate:date]; }]; } + (NSValueTransformer *)articlesFromDateJSONTransformer { return [MTLValueTransformer transformerUsingForwardBlock:^id(NSString *dateString, BOOL *success, NSError *__autoreleasing *error) { return [self.dateFormatter dateFromString:dateString]; } reverseBlock:^id(NSDate *date, BOOL *success, NSError *__autoreleasing *error) { return [self.dateFormatter stringFromDate:date]; }]; } @end

Otra gran característica de Mantle es que todos estos modelos se ajustan al protocolo NSCoding , además de implementar métodos isEqual y hash .

Como ya hemos visto, el JSON resultante de la llamada a la API contiene una matriz de objetos que representan artículos. Si queremos modelar esta respuesta usando Mantle, tendremos que crear dos modelos de datos separados. Uno modelaría objetos que representan artículos (elementos de matriz de documentos ) y el otro modelaría toda la respuesta JSON excepto los elementos de la matriz de documentos. Ahora, no tenemos que mapear todas y cada una de las propiedades del JSON entrante en nuestros modelos de datos. Supongamos que solo estamos interesados ​​en dos campos de objetos de artículo, y esos serían lead_paragraph y web_url . La clase ArticleModel es bastante sencilla de implementar, como podemos ver a continuación.

 #import "MTLModel.h" #import <Mantle/Mantle.h> @interface ArticleModel : MTLModel <MTLJSONSerializing> @property (nonatomic, copy) NSString *leadParagraph; @property (nonatomic, copy) NSString *url; @end
 #import "ArticleModel.h" @implementation ArticleModel #pragma mark - Mantle JSONKeyPathsByPropertyKey + (NSDictionary *)JSONKeyPathsByPropertyKey { return @{ @"leadParagraph": @"lead_paragraph", @"url": @"web_url" }; } @end

Ahora que se ha definido el modelo de artículo, podemos finalizar la definición del modelo de respuesta creando un modelo para la lista de artículos. Así es como se verá el modelo de respuesta de la clase ArticleList.

 #import "MTLModel.h" #import <Mantle/Mantle.h> #import "ArticleModel.h" @interface ArticleListResponseModel : MTLModel <MTLJSONSerializing> @property (nonatomic, copy) NSArray *articles; @property (nonatomic, copy) NSString *status; @end
 #import "ArticleListResponseModel.h" @class ArticleModel; @implementation ArticleListResponseModel #pragma mark - Mantle JSONKeyPathsByPropertyKey + (NSDictionary *)JSONKeyPathsByPropertyKey { return @{ @"articles" : @"response.docs", @"status" : @"status" }; } #pragma mark - JSON Transformer + (NSValueTransformer *)articlesJSONTransformer { return [MTLJSONAdapter arrayTransformerWithModelClass:ArticleModel.class]; } @end

Esta clase tiene solo dos propiedades: estado y artículos . Si lo comparamos con la respuesta del punto final, veremos que el tercer atributo JSON copyright no se asignará al modelo de respuesta. Si observamos el métodoarticlesJSONTransformer , veremos que devuelve un transformador de valor para una matriz que contiene objetos de la clase ArticleModel .

También vale la pena señalar que en el método JSONKeyPathsByPropertyKey , los artículos de propiedad del modelo corresponden a los documentos de la matriz anidados dentro de la respuesta del atributo JSON.

A estas alturas, deberíamos tener tres clases de modelo implementadas: ArticleListRequestModel, ArticleModel y ArticleListResponseModel.

Primera solicitud de API

API tranquila

Ahora que hemos implementado todos los modelos de datos, es hora de volver a la clase APIManager para implementar el método que usaremos para realizar solicitudes GET a la API. El método:

 - (NSURLSessionDataTask *) getArticlesWithRequestModel:(ArticleListRequestModel *)requestModel success:(void (^)(ArticleListResponseModel *responseModel))success failure:(void (^)(NSError *error))failure

toma un modelo de solicitud ArticleListRequestModel como parámetro y devuelve un ArticleListResponseModel en caso de éxito o un NSError en caso contrario. La implementación de este método utiliza AFNetworking para realizar una solicitud GET a la API. Tenga en cuenta que para realizar una solicitud de API exitosa, debemos proporcionar una clave que se puede obtener como se mencionó anteriormente, registrándose en http://developer.nytimes.com.

 #import "SessionManager.h" #import "ArticleListRequestModel.h" #import "ArticleListResponseModel.h" @interface APIManager : SessionManager - (NSURLSessionDataTask *)getArticlesWithRequestModel:(ArticleListRequestModel *)requestModel success:(void (^)(ArticleListResponseModel *responseModel))success failure:(void (^)(NSError *error))failure; @end
 #import "APIManager.h" #import "Mantle.h" static NSString *const kArticlesListPath = @"/svc/search/v2/articlesearch.json"; static NSString *const kApiKey = @"replace this with your own key"; @implementation APIManager - (NSURLSessionDataTask *)getArticlesWithRequestModel:(ArticleListRequestModel *)requestModel success:(void (^)(ArticleListResponseModel *responseModel))success failure:(void (^)(NSError *error))failure{ NSDictionary *parameters = [MTLJSONAdapter JSONDictionaryFromModel:requestModel error:nil]; NSMutableDictionary *parametersWithKey = [[NSMutableDictionary alloc] initWithDictionary:parameters]; [parametersWithKey setObject:kApiKey forKey:@"api-key"]; return [self GET:kArticlesListPath parameters:parametersWithKey success:^(NSURLSessionDataTask *task, id responseObject) { NSDictionary *responseDictionary = (NSDictionary *)responseObject; NSError *error; ArticleListResponseModel *list = [MTLJSONAdapter modelOfClass:ArticleListResponseModel.class fromJSONDictionary:responseDictionary error:&error]; success(list); } failure:^(NSURLSessionDataTask *task, NSError *error) { failure(error); }]; }

Hay dos cosas muy importantes que suceden en la implementación de este método. Primero echemos un vistazo a esta línea:

 NSDictionary *parameters = [MTLJSONAdapter JSONDictionaryFromModel:requestModel error:nil];

Lo que sucede aquí es que usando el método provisto por la clase MTLJSONAdapter obtenemos una representación NSDictionary de nuestro modelo de datos. Esa representación refleja el JSON que se enviará a la API. Aquí es donde reside la belleza de Mantle. Habiendo implementado los métodos JSONKeyPathsByPropertyKey y +<propertyName>JSONTransformer en la clase ArticleListRequestModel, podemos obtener la representación JSON correcta de nuestro modelo de datos en muy poco tiempo con solo una línea de código.

Mantle también nos permite realizar transformaciones en la otra dirección. Y eso es exactamente lo que sucede con los datos recibidos de la API. El NSDictionary que recibimos se asigna a un objeto de la clase ArticleListResponseModel utilizando el siguiente método de clase:

 ArticleListResponseModel *list = [MTLJSONAdapter modelOfClass:ArticleListResponseModel.class fromJSONDictionary:responseDictionary error:&error];

Datos persistentes con Realm

Ahora que podemos obtener datos de una API remota, es hora de persistir. Como se mencionó en la introducción, lo haremos usando Realm. Realm es una base de datos móvil y un reemplazo para Core Data y SQLite. Como veremos a continuación, es extremadamente fácil de usar.

Realm, la base de datos móvil definitiva, es un reemplazo perfecto para Core Data y SQLite.
Pío

Para guardar un dato en Realm, primero debemos encapsular un objeto derivado de la clase RLMObject. Lo que debemos hacer ahora es crear una clase modelo que almacene datos para artículos individuales. Esto es lo fácil que es crear una clase de este tipo.

 #import "RLMObject.h" @interface ArticleRealm : RLMObject @property NSString *leadParagraph; @property NSString *url; @end

Y esto podría ser básicamente todo, la implementación de esta clase podría quedar vacía. Tenga en cuenta que las propiedades en la clase de modelo no tienen atributos como no atómico, fuerte o copia. Realm se encarga de ellos y no tenemos que preocuparnos por ellos.

Dado que los artículos que podemos obtener están modelados con el modelo Article de Mante, sería conveniente inicializar los objetos ArticleRealm con objetos de la clase Article . Para hacerlo, agregaremos el método initWithMantleModel a nuestro modelo Realm. Aquí está la implementación completa de la clase ArticleRealm .

 #import "RLMObject.h" #import "ArticleModel.h" @interface ArticleRealm : RLMObject @property NSString *leadParagraph; @property NSString *url; - (id)initWithMantleModel:(ArticleModel *)articleModel; @end
 #import "ArticleRealm.h" @implementation ArticleRealm - (id)initWithMantleModel:(ArticleModel *)articleModel{ self = [super init]; if(!self) return nil; self.leadParagraph = articleModel.leadParagraph; self.url = articleModel.url; return self; } @end

Interactuamos con la base de datos usando objetos de clase RLMRealm . Podemos obtener fácilmente un objeto RLMRealm invocando el método "[RLMRealm defaultRealm]". Es importante recordar que dicho objeto es válido solo dentro del subproceso en el que se creó y no se puede compartir entre subprocesos. Escribir datos en Realm es bastante sencillo. Se debe realizar una sola escritura, o una serie de ellas, dentro de una transacción de escritura. Aquí hay una muestra de escritura en la base de datos:

 RLMRealm *realm = [RLMRealm defaultRealm]; ArticleRealm *articleRealm = [ArticleRealm new]; articleRealm.leadParagraph = @"abc"; articleRealm.url = @"sampleUrl"; [realm beginWriteTransaction]; [realm addObject:articleRealm]; [realm commitWriteTransaction];

Lo que sucede aquí es lo siguiente. Primero creamos un objeto RLMRealm para interactuar con la base de datos. Luego se crea un objeto de modelo ArticleRealm (tenga en cuenta que se deriva de la clase RLMRealm ). Finalmente, para guardarlo, comienza una transacción de escritura, el objeto se agrega a la base de datos y una vez que se guarda, se confirma la transacción de escritura. Como podemos ver, las transacciones de escritura bloquean el hilo en el que se invocan. Si bien se dice que Realm es muy rápido, si tuviéramos que agregar varios objetos a la base de datos dentro de una sola transacción en el subproceso principal, eso podría hacer que la interfaz de usuario no responda hasta que finalice la transacción. Una solución natural para eso es realizar una transacción de escritura de este tipo en un subproceso en segundo plano.

Solicitud de API y respuesta persistente en Realm

Esta es toda la información que necesitamos para persistir artículos usando Realm. Intentemos realizar una solicitud de API usando el método

 - (NSURLSessionDataTask *) getArticlesWithRequestModel:(ArticleListRequestModel *)requestModel success:(void (^)(ArticleListResponseModel *responseModel))success failure:(void (^)(NSError *error))failure

y modelos de solicitud y respuesta de Mantle para obtener artículos del New York Times que tuvieran algo que ver (como en el ejemplo anterior) con el baloncesto y se publicaron en los primeros siete días de junio de 2015. Una vez que la lista de dichos artículos esté disponible, lo persistirá en Realm. A continuación se muestra el código que hace eso. Se coloca en el método viewDidLoad del controlador de vista de tabla en nuestra aplicación.

 ArticleListRequestModel *requestModel = [ArticleListRequestModel new]; // (1) requestModel.query = @"Basketball"; requestModel.articlesToDate = [[ArticleListRequestModel dateFormatter] dateFromString:@"20150706"]; requestModel.articlesFromDate = [[ArticleListRequestModel dateFormatter] dateFromString:@"20150701"]; [[APIManager sharedManager] getArticlesWithRequestModel:requestModel // (2) success:^(ArticleListResponseModel *responseModel){ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ // (3) @autoreleasepool { RLMRealm *realm = [RLMRealm defaultRealm]; [realm beginWriteTransaction]; [realm deleteAllObjects]; [realm commitWriteTransaction]; [realm beginWriteTransaction]; for(ArticleModel *article in responseModel.articles){ ArticleRealm *articleRealm = [[ArticleRealm alloc] initWithMantleModel:article]; // (4) [realm addObject:articleRealm]; } [realm commitWriteTransaction]; dispatch_async(dispatch_get_main_queue(), ^{ // (5) RLMRealm *realmMainThread = [RLMRealm defaultRealm]; // (6) RLMResults *articles = [ArticleRealm allObjectsInRealm:realmMainThread]; self.articles = articles; // (7) [self.tableView reloadData]; }); } }); } failure:^(NSError *error) { self.articles = [ArticleRealm allObjects]; [self.tableView reloadData]; }];

En primer lugar, se realiza una llamada a la API (2) con un modelo de solicitud (1), que devuelve un modelo de respuesta que contiene una lista de artículos. Para persistir esos artículos usando Realm, necesitamos crear objetos de modelo de Realm, lo que se lleva a cabo en el ciclo for (4). También es importante tener en cuenta que dado que varios objetos persisten dentro de una sola transacción de escritura, esa transacción de escritura se realiza en un subproceso en segundo plano (3). Ahora, una vez que todos los artículos estén guardados en Realm, los asignamos a la propiedad de clase self.articles (7). Dado que se accederá a ellos más adelante en el subproceso principal en los métodos de origen de datos de TableView, también es seguro recuperarlos de la base de datos de Realm en el subproceso principal (5). Nuevamente, para acceder a la base de datos desde un nuevo subproceso, se debe crear un nuevo objeto RLMRealm (6) en ese subproceso.

Si falla la obtención de nuevos artículos de la API por cualquier motivo, los existentes se recuperan del almacenamiento local en el bloque de fallas.

Terminando

En este tutorial aprendimos a configurar Mantle, un framework modelo para Cocoa y Cocoa Touch, para poder interactuar con una API remota. También aprendimos cómo persistir localmente los datos recuperados en forma de objetos del modelo Mantle utilizando la base de datos móvil de Realm.

En caso de que quieras probar esta aplicación, puedes recuperar el código fuente de su repositorio de GitHub. Deberá generar y proporcionar su propia clave API antes de ejecutar la aplicación.