使用 Mantle 和 Realm 在 iOS 上简化 RESTful API 使用和数据持久性
已发表: 2022-03-11每个 iOS 开发人员都熟悉 Core Data,这是一个来自 Apple 的对象图和持久性框架。 除了在本地保存数据外,该框架还具有许多高级功能,例如对象更改跟踪和撤消。 这些功能虽然在很多情况下很有用,但并不是免费提供的。 它需要大量样板代码,而且整个框架的学习曲线很陡峭。
2014 年,移动数据库 Realm 发布并席卷了开发世界。 如果我们只需要在本地保存数据,Realm 是一个不错的选择。 毕竟,并非所有用例都需要 Core Data 的高级功能。 Realm 非常易于使用,与 Core Data 相比,它只需要很少的样板代码。 它也是线程安全的,据说比 Apple 的持久性框架更快。
在大多数现代移动应用程序中,持久化数据解决了一半的问题。 我们经常需要从远程服务获取数据,通常是通过 RESTful API。 这就是 Mantle 发挥作用的地方。 它是 Cocoa 和 Cocoa Touch 的开源模型框架。 Mantle 显着简化了与使用 JSON 作为数据交换格式的 API 交互的数据模型的编写。
在本文中,我们将构建一个 iOS 应用程序,该应用程序从纽约时报文章搜索 API v2 获取文章列表以及指向它们的链接。 该列表将使用标准 HTTP GET 请求获取,并使用 Mantle 创建请求和响应模型。 我们将看到使用 Mantle 处理值转换(例如从 NSDate 到字符串)是多么容易。 获取数据后,我们将使用 Realm 将其保存在本地。 所有这些都使用最少的样板代码。
RESTful API - 入门
让我们首先为 iOS 创建一个名为“RealmMantleTutorial”的新“Master-Detail Application”Xcode 项目。 我们将使用 CocoaPods 为其添加框架。 podfile 应类似于以下内容:
pod 'Mantle' pod 'Realm' pod 'AFNetworking'
安装 pod 后,我们可以打开新创建的MantleRealmTutorial工作区。 如您所见,著名的 AFNetworking 框架也已安装。 我们将使用它来执行对 API 的请求。
正如介绍中提到的,纽约时报提供了一个优秀的文章搜索 API。 为了使用它,需要注册以获取 API 的访问密钥。 这可以在 http://developer.nytimes.com 上完成。 有了 API 密钥,我们就可以开始编码了。
在我们深入研究创建 Mantle 数据模型之前,我们需要启动并运行我们的网络层。 让我们在 Xcode 中创建一个新组并将其命名为 Network。 在这个组中,我们将创建两个类。 让我们调用第一个SessionManager并确保它是从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 请求和响应序列化程序。
现在我们要在Network组中创建的第二个类将被称为APIManager 。 它应派生自我们新创建的SessionManager类。 一旦创建了必要的数据模型,我们将向ApiManager添加一个方法,该方法将用于从 API 请求文章列表。
纽约时报文章搜索 API 概述
这个优秀 API 的官方文档可以在 http://developer.nytimes.com/.../article_search_api_v2 找到。 我们要做的是使用以下端点:
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." }
我们基本上得到的响应是三个字段。 第一个称为response包含数组docs ,该数组又包含表示文章的项目。 另外两个字段是status和copyright 。 现在我们知道了 API 的工作原理,是时候使用 Mantle 创建数据模型了。
地幔简介
如前所述,Mantle 是一个开源框架,可显着简化数据模型的编写。 让我们从创建文章列表请求模型开始。 让我们调用这个类ArticleListRequestModel并确保它派生自MTLModel ,这是一个所有 Mantle 模型都应该派生的类。 另外让我们让它符合MTLJSONSerializing协议。 我们的请求模型应该具有三个合适类型的属性:query、 articlesFromDate和articleToDate 。 为了确保我们的项目井井有条,我建议将此类放在模型组中。
下面是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
这个类只有两个属性: status和articles 。 如果我们将其与来自端点的响应进行比较,我们将看到第三个 JSON 属性版权不会映射到响应模型中。 如果我们查看articleJSONTransformer方法,我们会看到它为包含ArticleModel类对象的数组返回一个值转换器。
还值得注意的是,在JSONKeyPathsByPropertyKey方法中,模型属性文章对应于嵌套在 JSON 属性response中的数组文档。
现在我们应该已经实现了三个模型类:ArticleListRequestModel、ArticleModel 和 ArticleListResponseModel。
第一个 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。 这就是地幔之美的所在。 在 ArticleListRequestModel 类中实现JSONKeyPathsByPropertyKey和+<propertyName>JSONTransformer
方法后,我们只需一行代码即可立即获得数据模型的正确 JSON 表示。
Mantle 还允许我们在另一个方向上执行转换。 这正是从 API 接收到的数据所发生的情况。 我们收到的 NSDictionary 使用以下类方法映射到 ArticleListResponseModel 类的对象中:
ArticleListResponseModel *list = [MTLJSONAdapter modelOfClass:ArticleListResponseModel.class fromJSONDictionary:responseDictionary error:&error];
使用 Realm 持久化数据
现在我们能够从远程 API 获取数据,是时候持久化它了。 正如介绍中提到的,我们将使用 Realm 来完成。 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)。 同样,要从新线程访问数据库,需要在该线程上创建 (6) 一个新的 RLMRealm 对象。
如果由于某种原因从 API 获取新文章失败,则会从故障块中的本地存储中检索现有文章。
包起来
在本教程中,我们学习了如何配置 Mantle,一个用于 Cocoa 和 Cocoa Touch 的模型框架,以便与远程 API 交互。 我们还学习了如何使用 Realm 移动数据库以 Mantle 模型对象的形式在本地保存检索到的数据。
如果您想试用此应用程序,可以从其 GitHub 存储库中检索源代码。 在运行应用程序之前,您需要生成并提供自己的 API 密钥。