个性化阅读
专注于IT技术分析

通过Mantle和Realm简化iOS上的RESTful API使用和数据持久化

本文概述

每个iOS开发人员都熟悉Core Data, 它是Apple的对象图和持久化框架。除了在本地持久存储数据外, 该框架还具有许多高级功能, 例如对象更改跟踪和撤消。这些功能虽然在许多情况下很有用, 但并非免费提供。它需要大量样板代码, 并且框架总体上具有陡峭的学习曲线。

在2014年, 境界, 移动数据库, 发布和风靡世界的发展。如果我们需要的是在本地持久化数据, 境界是一个很好的选择。毕竟, 并非所有用例都需要Core Data的高级功能。境界是非常容易使用, 而不是核心数据, 只需要很少的样板代码。这也是线程安全的, 据说是比苹果的持久化框架更快。

在大多数现代移动应用程序中, 持久数据解决了一半的问题。通常, 我们通常需要通过RESTful API从远程服务中获取数据。这是Mantle发挥作用的地方。它是针对Cocoa和Cocoa Touch的开源模型框架。 Mantle极大地简化了与使用JSON作为数据交换格式的API进行交互的编写数据模型。

iOS版Realm和Mantle

在本文中, 我们将构建一个iOS应用程序, 该应用程序将从《纽约时报》文章搜索API v2中获取文章列表以及指向这些文章的链接。将使用标准HTTP GET请求以及使用Mantle创建的请求和响应模型来获取列表。我们将看到使用Mantle处理值转换(例如, 从NSDate到字符串)有多么容易。提取数据后, 我们将使用Realm在本地将其持久化。所有这些只需最少的样板代码即可。

RESTful API-入门

首先, 为iOS创建一个名为” RealmMantleTutorial”的新的”主从应用程序” Xcode项目。我们将使用CocoaPods向其添加框架。 podfile应该类似于以下内容:

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

安装Pod后, 我们可以打开新创建的MantleRealmTutorial工作区。如你所见, 著名的AFNetworking框架也已安装。我们将使用它来执行对API的请求。

如引言中所述, 《纽约时报》提供了出色的文章搜索API。为了使用它, 需要注册以获得API的访问密钥。可以在http://developer.nytimes.com上完成。有了API密钥, 我们就可以开始进行编码了。

在深入研究创建Mantle数据模型之前, 我们需要启动并运行我们的网络层。让我们在Xcode中创建一个新组, 并将其命名为”网络”。在这个小组中, 我们将创建两个类。让我们调用第一个SessionManager并确保它是从AFHTTPSessionManager派生的, AFHTTPSessionManager是来自AFNetworking(令人愉快的网络框架)的会话管理器类。我们的SessionManager类将是一个单例对象, 我们将使用它来执行对API的获取请求。创建类后, 请分别将下面的代码复制到接口和实现文件中。

#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

使用静态kBaseURL变量中定义的基本URL初始化会话管理器。它还将使用JSON请求和响应序列化器。

现在, 我们将在”网络”组中创建的第二个类称为APIManager。它应从我们新创建的SessionManager类派生。一旦必要的数据模型的创建, 我们将增加ApiManager的方法将被用于请求从API文章的列表。

纽约时报文章搜索API概述

可以在http://developer.nytimes.com/…/article_search_api_v2上找到有关此出色API的官方文档。我们要做的是使用以下端点:

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

…提取使用我们选择的以日期范围为界的搜索查询词找到的文章。例如, 我们可以做的就是要求API返回《纽约时报》上发表的所有与篮球相关的所有文章的清单, 这些文章与2015年7月的前7天有关。根据API文档, 可以这样做我们需要在对该端点的get请求中设置以下参数:

参数
q “篮球”
开始日期 “20150701”
结束日期 “20150707”

API的响应非常复杂。以下是对具有以上参数的请求的响应, 该参数仅限于一篇文章(docs数组中为一项), 为清楚起见省略了许多字段。

{
  "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."
}

我们基本上得到的回应是三个领域。第一个称为响应的响应包含数组docs, 该数组又包含表示文章的项目。其他两个字段是状态和版权。现在我们知道了API的工作原理, 是时候使用Mantle创建数据模型了。

Mantle简介

如前所述, Mantle是一个开放源代码框架, 可大大简化数据模型的编写。首先创建一个文章列表请求模型。让我们将此类称为ArticleListRequestModel, 并确保它是从MTLModel派生的, MTLModel是所有Mantle模型都应派生的类。另外, 让它符合MTLJSONSerializing协议。我们的请求模型应该具有三个合适类型的属性:query, articlesFromDate和articlesToDate。为了确保我们的项目井井有条, 我建议将此课程放在”模型”组中。

外套简化了数据模型的编写, 减少了样板代码。

这是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

现在, 如果我们查找文章搜索端点的文档或查看上面带有请求参数的表, 我们将注意到API请求中的变量名称与我们的请求模型中的变量名称不同。 Mantle使用以下方法有效处理此问题:

+ (NSDictionary *)JSONKeyPathsByPropertyKey.

在我们的请求模型的实现中应采用以下方法:

#import "ArticleListRequestModel.h"

@implementation ArticleListRequestModel

#pragma mark - Mantle JSONKeyPathsByPropertyKey

+ (NSDictionary *)JSONKeyPathsByPropertyKey {
    return @{
             @"query": @"q", @"articlesFromDate": @"begin_date", @"articlesToDate": @"end_date"
             };
}

@end

此方法的实现指定如何将模型的属性映射到其JSON表示中。一旦实现了方法JSONKeyPathsByPropertyKey, 我们就可以使用类方法+ [MTLJSONAdapter JSONArrayForModels:]获得模型的JSON字典表示形式。

正如我们从参数列表中知道的那样, 剩下的一件事是两个日期参数都必须采用” YYYYMMDD”格式。这是Mantle派上用场的地方。通过实现可选方法+ <propertyName> JSONTransformer, 我们可以为任何属性添加自定义值转换。通过实现它, 我们告诉Mantle在JSON反序列化期间应如何转换特定JSON字段的值。我们还可以实现可逆的转换器, 当从模型创建JSON时将使用该转换器。由于我们需要将NSDate对象转换为字符串, 因此我们还将使用NSDataFormatter类。这是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

Mantle的另一个重要功能是所有这些模型都符合NSCoding协议, 并实现了isEqual和hash方法。

正如我们已经看到的, API调用产生的JSON包含代表文章的对象数组。如果要使用Mantle对该响应进行建模, 则必须创建两个单独的数据模型。一种将对表示文章的对象(docs数组元素)进行建模, 另一种将对JSON响应(除了docs数组的元素)进行建模。现在, 我们不必将传入JSON中的每个属性都映射到我们的数据模型中。假设我们只对文章对象的两个字段感兴趣, 那就是lead_paragraph和web_url。如我们在下面看到的, ArticleModel类非常容易实现。

#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

现在已经定义了商品模型, 我们可以通过为商品列表创建模型来完成响应模型的定义。这是类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

此类仅具有两个属性:状态和文章。如果将其与端点的响应进行比较, 我们将看到第三个JSON属性版权不会映射到响应模型中。如果查看articlesJSONTransformer方法, 我们将看到它为包含ArticleModel类的对象的数组返回一个值转换器。

还值得注意的是, 在方法JSONKeyPathsByPropertyKey中, 模型属性文章对应于嵌套在JSON属性响应中的数组文档。

到目前为止, 我们应该已经实现了三个模型类:ArticleListRequestModel, ArticleModel和ArticleListResponseModel。

第一个API请求

宁静的API

现在我们已经实现了所有数据模型, 是时候回到类APIManager来实现将用于执行对API的GET请求的方法了。方法:

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

将ArticleListRequestModel请求模型作为参数, 并在成功的情况下返回ArticleListResponseModel, 否则返回NSError。此方法的实现使用AFNetworking对API执行GET请求。请注意, 为了成功发送API请求, 我们需要提供一个可以如前所述通过在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);
        
    }];
}

此方法的实现中发生了两个非常重要的事情。首先, 让我们看一下这一行:

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

这里发生的是, 使用MTLJSONAdapter类提供的方法, 我们获得了数据模型的NSDictionary表示形式。该表示反映了将要发送到API的JSON。这就是Mantle之美所在。在ArticleListRequestModel类中实现了JSONKeyPathsByPropertyKey和+ <propertyName> JSONTransformer方法之后, 我们只需一行代码就可以立即获得数据模型的正确JSON表示形式。

Mantle还允许我们在另一个方向上执行转换。而这正是从API接收到的数据所发生的一切。使用以下类方法将我们收到的NSDictionary映射到ArticleListResponseModel类的对象:

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

使用领域持久化数据

现在我们已经能够从远程API提取数据, 是时候将其持久化了。如引言中所述, 我们将使用Realm来实现。 Realm是一个移动数据库, 是Core Data和SQLite的替代品。正如我们将在下面看到的, 它非常易于使用。

Realm是最终的移动数据库, 是Core Data和SQLite的完美替代。

鸣叫

为了在Realm中保存一条数据, 我们首先需要封装一个从RLMObject类派生的对象。我们现在需要做的是创建一个模型类, 该模型类将存储单个文章的数据。创建此类的过程很简单。

#import "RLMObject.h"

@interface ArticleRealm : RLMObject

@property NSString *leadParagraph;
@property NSString *url;

@end

基本上就是这样, 此类的实现可以保持为空。请注意, 模型类中的属性没有非原子, 强或副本之类的属性。 Realm会照顾那些人, 我们不必担心它们。

由于我们可以使用Mante模型Article对模型进行建模, 因此使用Article类的对象初始化ArticleRealm对象将很方便。为此, 我们将initWithMantleModel方法添加到我们的Realm模型中。这是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

我们使用类RLMRealm的对象与数据库进行交互。通过调用方法” [RLMRealm defaultRealm]”, 我们可以轻松获得RLMRealm对象。重要的是要记住, 这样的对象仅在创建它的线程内有效, 并且不能在线程间共享。将数据写入Realm非常简单。一次写入或一系列写入都需要在写入事务中完成。这是写入数据库的示例:

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

以下是发生的情况。首先, 我们创建一个RLMRealm对象以与数据库进行交互。然后创建一个ArticleRealm模型对象(请记住, 它是从RLMRealm类派生的)。最后要保存它, 一个写事务开始, 该对象被添加到数据库, 并且一旦保存, 就提交写事务。如我们所见, 写事务阻塞了调用它们的线程。虽然说Realm很快, 但是如果我们要在主线程上的单个事务中向数据库添加多个对象, 这可能导致UI变得无响应, 直到事务完成为止。一个自然的解决方案是在后台线程上执行这种写事务。

API请求和领域中的持久响应

这是我们使用Realm保留文章所需的全部信息。让我们尝试使用方法执行API请求

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

和Mantle请求和响应模型, 以获取与篮球有任何关系的纽约时报文章(如前面的示例), 并于2015年6月的前7天发布。一旦此类文章列表可用, 我们将其保留在Realm中。下面是执行此操作的代码。它放置在我们应用程序中的表格视图控制器的viewDidLoad方法中。

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

首先, 使用请求模型(1)进行API调用(2), 该模型返回包含文章列表的响应模型。为了使用Realm保留这些文章, 我们需要创建Realm模型对象, 该对象在for循环(4)中进行。还需要注意的是, 由于多个对象保留在单个写事务中, 因此该写事务是在后台线程(3)上执行的。现在, 将所有文章保存在Realm中之后, 我们将它们分配给类属性self.articles(7)。由于稍后将在TableView数据源方法中的主线程上对其进行访问, 因此也可以从主线程上的Realm数据库中检索它们(5)。同样, 要从新线程访问数据库, 需要在该线程上创建一个新的RLMRealm对象(6)。

通过Mantle和Realm简化iOS上的RESTful API使用和数据持久化3

如果从API获取新文章由于某种原因而失败, 则将从失败块中的本地存储中检索现有文章。

本文总结

在本教程中, 我们学习了如何配置Mantle(可可和Cocoa Touch的模型框架), 以便与远程API进行交互。我们还学习了如何使用Realm移动数据库以Mantle模型对象的形式本地保留检索到的数据。

如果你想试用此应用程序, 可以从其GitHub存储库中检索源代码。在运行应用程序之前, 你将需要生成并提供自己的API密钥。

赞(0)
未经允许不得转载:srcmini » 通过Mantle和Realm简化iOS上的RESTful API使用和数据持久化

评论 抢沙发

评论前必须登录!