MMRecord 1.4.1

MMRecord 1.4.1

测试已测试
Lang语言 Obj-CObjective C
许可证 MIT
发布时间最新版本发布时间2014年12月

Conrad StollConrad Stoll维护。



MMRecord 1.4.1

  • Conrad Stoll

MMRecord 是一个基于块的 无缝 Web 服务集成库,用于 iOS 和 Mac OS X。它利用 Core Data 模型配置来自动从 API 响应创建和填充完整的对象图。它与任何网络库一起工作,易于设置,并包含许多流行的功能,使与 Web 服务一起工作更加容易。以下是请求 App.net Post 记录的请求方式

NSManagedObjectContext *context = [[MMDataManager sharedDataManager] managedObjectContext];

[Post 
 startPagedRequestWithURN:@"stream/0/posts/stream/global"
 data:nil
 context:context
 domain:self
 resultBlock:^(NSArray *posts, ADNPageManager *pageManager, BOOL *requestNextPage) {
     NSLog(@"Posts: %@", posts);
 }
 failureBlock:^(NSError *error) {
     NSLog(@"%@", error);
 }];

继续阅读以了解更多关于如何在您的项目中开始使用 MMRecord 的信息!

入门指南

  • 下载 MMRecord 并尝试包含的示例应用。
  • 继续阅读下方的集成说明。
  • 查看 文档 以获取所有其他详细信息。
  • 查看下方的 示例 以获取特定用法的灵感。
  • 了解 MMRecord 对 Swift辅助功能 的支持。
  • 如果在运行过程中遇到任何问题,请查看一些有用的 调试 建议。

安装 MMRecord


您可以通过使用 CocoaPods 在您的项目中安装 MMRecord。

pod 'MMRecord', '~> 1.4.0'

概述

MMRecord 的设计旨在使从新的 Web 服务请求中获取本地对象尽可能简单和快速。它在后台处理所有获取、创建和填充 NSManagedObject 的过程,所以当您进行请求时,您将得到您可以立即使用的本地对象。无需解析。

该库的设计使其尽可能简单和轻量。以下是 MMRecord 核心类的分析。

核心
MMRecord NSManagedObject的子类MMRecord定义了接口并启动了对象图的填充过程。
  • 请求的入口点
  • 使用已注册的MMServer类进行请求
  • 使用MMRecordResponse
  • 通过基于块的用户界面返回对象
MMServer 是一个定义了由MMRecord.
  • 用于请求界面的抽象类
  • 设计为可子类化

MMRecord Architecture Diagram

支持任何网络框架,包括本地文件和服务器
MMRecordResponse 填充MMRecord处理将响应转换为本地对象的过程的类。
MMRecordProtoRecord 用于在填充过程中作为对象图占位符的容器类。
MMRecordRepresentation 一个定义字典与Core Data之间的映射关系的类NSEntityDescription.
MMRecordMarshaler 负责根据MMRecord填充实例MMRecordRepresentation.
分页
MMServerPageManager 一个定义处理分页接口的抽象类
缓存
MMRecordCache 一个将NSManagedObjectObjectIDs映射到NSCachedURLResponse.
的类
调试 MMRecordDebugger一个管理NSError堆栈的类,提供调试反馈。

MMRecord Population Architecture

子模块
AFServer 一个示例子类,实现了MMServer基于AFNetworking的 1.0.
SessionManagerServer 一个示例子类,实现了MMServer基于AFNetworking的 2.0.
一个子类,可以读取本地的JSON文件。 一个示例子类,实现了MMServerDynamicModel
一个自定义 和子类对,将原始对象字典作为可转换属性存储。MMRecordRepresentationMMRecordMarshaler
ResponseSerializer 和子类对,将原始对象字典作为可转换属性存储。AFHTTPResponseSerializer它创建并返回MMRecord在2.0成功块中的实例。AFNetworking的。 tweaks
一个子模块,实现了对Facebook Tweaks的支持,以调整 响应处理行为。MMRecordMMRecord集成指南

集成指南

MMRecord在使用前需要进行一些基本设置。本指南将向您介绍该配置过程的所有步骤。

服务器类配置

MMRecord需要注册服务器类来发出请求。服务器类应该知道如何对您要集成的API发出请求。服务器实现的要求仅是返回一个响应对象(数组或字典),其中包含您请求的对象。服务器可以使用AFNetworking对一个特定的API执行GET请求。或者它可以加载并返回本地的JSON文件。有两个子模块提供了预构建的服务器,分别使用AFNetworking和本地JSON文件。一般来说,您最好实现自己的服务器来与您使用的API通信。

定义了您的服务器类后,您必须将其注册到MMRecord

[Post registerServerClass:[ADNServer class]];

请注意,您可以在不同的MMRecord子类上注册不同的服务器类。

[Tweet registerServerClass:[TWSocialServer class]];
[User registerServerClass:[MMJSONServer class]];

当您正在处理的一个端点是完整的,但另一个不是,或者位于另一个API上时,这非常有帮助。

AFNetworking

虽然您被鼓励创建自己的特定服务器子类进行集成,但MMRecord确实提供了针对AFNetworking 1.0和AFNetworking 2.0的基类服务器实现作为子模块示例。您可以为AFNetworking 1.0查看AFServer子模块,或为AFNetworking 2.0查看AFMMRecordSessionManagerServer子模块。您可以在Foursquare示例应用中查看新的AFNetworking 2.0服务器。

此外,我们特别提供了AFMMRecordResponseSerializer子模块,用于AFNetworking 2.0。此响应序列化器可用于AFNetworking 2.0,以在AFNetworking成功块中为您提供解析和填充的MMRecord实例。有关更多信息,请参阅这篇博客文章或查看下面的示例

MMRecord子类实现

您需要在MMRecord子类中覆盖一个方法,以告诉解析系统您希望解析的对象(或对象组)的位置。此方法返回一个关键路径,指定相对于响应对象根部的位置。如果您的响应对象是数组,您可以直接返回nil。

在App.net请求中,所有返回的对象都位于一个名为“data”的对象中,所以我们的子类将如下所示MMRecord

@interface ADNRecord : MMRecord
@end

static NSDateFormatter *ADNRecordDateFormatter;

@implementation ADNRecord

+ (NSString *)keyPathForResponseObject {
    return @"data";
}

+ (NSDateFormatter *)dateFormatter {
    if (!ADNRecordDateFormatter) {
        NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
        [dateFormatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ssZ"]; // "2012-11-21T03:57:39Z"
        ADNRecordDateFormatter = dateFormatter;
    }

    return ADNRecordDateFormatter;
}

@end

还有一些可选方法您可以实现在MMRecord上。其中一种方法返回一个用于填充属性类型的日期格式化器。您可以覆盖此方法以使用格式化的日期字符串填充日期属性。默认支持Unix数字时间戳日期。

注意,这些方法是在一个名为ADNRecord的类上实现的,它是一个MMRecord的子类ADNRecord。其他实体是

的子类,不需要自己实现这些方法。

模型配置Core Data的

NSManagedObjectModelNSEntityDescription非常有用。从高层次来看,模型由一系列实体组成。同样,API通常由一系列端点组成。MMRecord利用这一惯例将实体表示映射到端点表示,从而创建从API响应中创建原生对象。

主键

MMRecord在存在唯一标识给定实体类型记录的方法时表现最好。这样,它可以获取现有记录(如果存在)并更新它,而不是创建一个重复的记录。要指定实体的主键,我们利用实体的用户信息字典。将主键属性名称指定为值,将MMRecordEntityPrimaryAttributeKey指定为键。

MMRecord Primary Key

请注意,主键可以是任何属性,包括关系。如果使用关系作为主键,MMRecord将尝试获取父对象并在关系中搜索关联对象。

MMRecord Relationship Primary Key

如果您知道要填充的记录的键不在用作填充记录的API响应字典中,也可以在填充时注入主键。以下是一个使用此功能示例的例子:[以下](https://github.com/mutualmobile/MMRecord/#mmrecordoptions-and-primary-key-injection)。此选项不打算取代适当的模型配置,但可用于额外的灵活性。您可以将其用于创建给定记录自己的唯一标识符,并通过解析字典中的内容来实现。

备选属性名称

有时,你可能需要为你的实体中的一个属性定义一个别名。这可能有多种原因。比如,你可能不喜欢你的Core Data属性名中包含下划线?或者API响应已更改,你不希望改变你的NSManagedObject属性名。或者,属性值实际上在子对象中,你需要将其提升到根级别。这正是MMRecordAttributeAlternateNameKey的作用所在。你可以在任何属性或关系用户信息字典上定义这个键。这个键的值可以是别名,也可以是用于定位该属性的备选键Path。

MMRecord Alternate Name Key

为了参考,以下是App.net用户对象的截断版本,以说明这些配置值是如何确定的

{
    "id": "1", // note this is a string
    "username": "johnappleseed",
    "name": "John Appleseed",
    "avatar_image": {
        "height": 512,
        "width": 512,
        "url": "https://example.com/avatar_image.jpg",
        "is_default": false
    },
    "cover_image": {
        "width": 320,
        "height": 118,
        "url": "https://example.com/cover_image.jpg",
        "is_default": false
    },
    "counts": {
        "following": 100,
        "followers": 200,
        "posts": 24,
        "stars": 76
    }
}

示例用法

这里有一些使用MMRecord的请求类型的示例。请注意,AFMMRecordResponseSerializer是MMRecord的一个子规范。

标准请求

+ (void)favoriteTweetsWithContext:(NSManagedObjectContext *)context
                           domain:(id)domain
                      resultBlock:(void (^)(NSArray *tweets))resultBlock
                     failureBlock:(void (^)(NSError *))failureBlock {
    [Tweet startRequestWithURN:@"favorites/list.json"
                          data:nil
                       context:context
                        domain:self
                   resultBlock:resultBlock
                  failureBlock:failureBlock];
}

分页请求

@interface Post : ADNRecord
+ (void)getStreamPostsWithContext:(NSManagedObjectContext *)context
                           domain:(id)domain
                      resultBlock:(void (^)(NSArray *posts, ADNPageManager *pageManager, BOOL *requestNextPage))resultBlock
                     failureBlock:(void (^)(NSError *error))failureBlock;
@end

@implementation Post
+ (void)getStreamPostsWithContext:(NSManagedObjectContext *)context
                           domain:(id)domain
                      resultBlock:(void (^)(NSArray *posts, ADNPageManager *pageManager, BOOL *requestNextPage))resultBlock
                     failureBlock:(void (^)(NSError *error))failureBlock {
    [self startPagedRequestWithURN:@"stream/0/posts/stream/global"
                              data:nil
                           context:context
                            domain:self
                       resultBlock:resultBlock
                      failureBlock:failureBlock];
}
@end

批量请求

[Tweet startBatchedRequestsInExecutionBlock:^{
    [Tweet
     timelineTweetsWithContext:context
     domain:self
     resultBlock:^(NSArray *tweets, MMServerPageManager *pageManager, BOOL *requestNextPage) {
         NSLog(@"Timeline Request Complete");
     }
     failureBlock:^(NSError *error) {
         NSLog(@"%@", error);
     }];

    [Tweet
     favoriteTweetsWithContext:context
     domain:self
     resultBlock:^(NSArray *tweets, MMServerPageManager *pageManager, BOOL *requestNextPage) {
         NSLog(@"Favorites Request Complete");
     }
     failureBlock:^(NSError *error) {
         NSLog(@"%@", error);
     }];
} withCompletionBlock:^{
    NSLog(@"Request Complete");
}];

获取第一个请求

NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"User"];
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"SELF.name contains[c] %@", name];
NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"name" ascending:YES];
fetchRequest.predicate = predicate;
fetchRequest.sortDescriptors = @[sortDescriptor];

[self
 startRequestWithURN:[NSString stringWithFormat:@"stream/0/users/%@", name]
 data:nil
 context:context
 domain:domain
 fetchRequest:fetchRequest
 customResponseBlock:nil resultBlock:^(NSArray *records, id customResponseObject, BOOL requestComplete) {
     if (resultBlock != nil) {
         resultBlock(records, requestComplete);
     }
 }
 failureBlock:failureBlock];

AFMMRecordResponseSerializer

MMFoursquareSessionManager *sessionManager = [MMFoursquareSessionManager sharedClient];

NSManagedObjectContext *context = [[MMDataManager sharedDataManager] managedObjectContext];
AFHTTPResponseSerializer *HTTPResponseSerializer = [AFJSONResponseSerializer serializer];

AFMMRecordResponseSerializationMapper *mapper = [[AFMMRecordResponseSerializationMapper alloc] init];
[mapper registerEntityName:@"Venue" forEndpointPathComponent:@"venues/search?"];

AFMMRecordResponseSerializer *serializer =
    [AFMMRecordResponseSerializer serializerWithManagedObjectContext:context
                                            responseObjectSerializer:HTTPResponseSerializer
                                                        entityMapper:mapper];

sessionManager.responseSerializer = serializer;

[[MMFoursquareSessionManager sharedClient]
 GET:@"venues/search?ll=30.25,-97.75"
 parameters:requestParameters
 success:^(NSURLSessionDataTask *task, id responseObject) {
     NSArray *venues = responseObject;

     self.venues = venues;

     [self.tableView reloadData];
 } 
 failure:failureBlock];

MMRecordOptions 示例

MMRecordOptions 是一种定制请求行为的方式。你可以使用它的方法之一是指定一系列选项后应用到以下请求的块。这允许你执行插入新主键等操作或指定孤立删除行为。

主键注入

MMRecordOptions *options = [Post defaultOptions];

options.entityPrimaryKeyInjectionBlock = ^id(NSEntityDescription *entity,
                                             NSDictionary *dictionary,
                                             MMRecordProtoRecord *parentProtoRecord) {
    if ([[entity name] isEqualToString:@"CoverImage"]) {
        if ([[parentProtoRecord.entity name] isEqualToString:@"User"]) {
            if (parentProtoRecord.primaryKeyValue != nil) {
                return parentProtoRecord.primaryKeyValue;
            }
        }
    }

    return nil;
};

[Post setOptions:options];

[Post
 getStreamPostsWithContext:context
 domain:self
 resultBlock:^(NSArray *posts, ADNPageManager *pageManager, BOOL *requestNextPage) {
    [self populatePostsTableWithPosts:posts];
 }
 failureBlock:failureBlock];

孤立删除

MMRecordOptions *options = [Tweet defaultOptions];

options.deleteOrphanedRecordBlock = ^(MMRecord *orphan,
                                      NSArray *populatedRecords,
                                      id responseObject,
                                      BOOL *stop) {
    Tweet *tweet = (Tweet *)orphan;

    if ([tweet isFavorite]) {
        return NO;
    }

    return YES;
};

[Tweet setOptions:options];

[Tweet
 timelineTweetsWithContext:context
 domain:self
 resultBlock:^(NSArray *tweets, MMServerPageManager *pageManager, BOOL *requestNextPage) {
     self.tweets = tweets;
     [self.tableView reloadData];
 }
 failureBlock:failureBlock];

Swift 示例

虽然MMRecord是用Objective-C实现的,但你也可以使用库从Swift中构建你的模型。当你用Swift构建模型时,你应该注意的主要事情是,实体管理对象类名需要是全命名空间的。以下是一个例子。

MMRecord Model Configuration for Swift

请注意,在Swift中,使用MMRecordAtlassian作为Issue类的命名空间。这是因为默认命名空间是您项目的程序名称。请注意,使用特殊字符或空格作为您的产品名称可能会导致问题。通常这些字符会替换您的命名空间中的下划线,但为了最佳效果,请使用单个单词作为您的产品名称以避免问题。

你还应该记得在你的Objective-C桥梁头文件中导入MMRecord.h及其任何您使用的子规范。然后,你就可以准备好构建你的MMRecord模型了!

这里有一些在Swift中使用MMRecord的示例。

Swift MMRecord 子类实现

import CoreData

class Plan: ATLRecord {
    @NSManaged var name: NSString
    @NSManaged var id: NSString

    override class func keyPathForResponseObject() -> String {
        return "plans.plan"
    }
}

标准Swift请求

Plan.startRequestWithURN("/plans",
    data: nil,
    context: managedObjectContext,
    domain: self,
    resultBlock: {records in
        var results: Plan[] = records as Plan[]

        self.plans = results
        self.tableView.reloadData()
    },
    failureBlock: { error in

    })

带有 MMRecordOptions 的 Swift 请求

var options = Issue.defaultOptions()

options.entityPrimaryKeyInjectionBlock = {(entity, dictionary, parentProtoRecord) -> NSCopying in
    let dict = dictionary as Dictionary
    let key: AnyObject? = dict["id"]
    let returnKey = key as String
    return returnKey
}

options.recordPrePopulationBlock = { protoRecord in
    let proto: MMRecordProtoRecord = protoRecord
    let entity: NSEntityDescription = protoRecord.entity

    var dictionary: AnyObject! = proto.dictionary.mutableCopy()
    var mutableDictionary: NSMutableDictionary = dictionary as NSMutableDictionary
    var primaryKey: AnyObject! = ""

    if (entity.name == "OutwardLink") {
        primaryKey = mutableDictionary.valueForKeyPath("outwardIssue.key")
    }

    if (entity.name == "InwardLink") {
        primaryKey = mutableDictionary.valueForKeyPath("inwardIssue.key")
    }

    mutableDictionary.setValue(primaryKey, forKey: "PrimaryKey")

    proto.dictionary = mutableDictionary
}

Issue.setOptions(options)

Issue.startRequestWithURN("/issue",
    data: nil,
    context: managedObjectContext,
    domain: self,
    resultBlock: { records in
        var results: Issue[] = records as Issue[]

        self.results = results
        self.tableView.reloadData()
    },
    failureBlock: { error in

    })

微调

MMRecord 还提供了 TweakModel 子规范,以实现对 Facebook 微调的支持。您可以使用微调来修改 MMRecord 解析和填充参数的大部分内容。如果您正在开发一个 API 正在变动且仍在积极开发中的应用程序,这将非常有用。微调的用户界面将向您展示数据模型中 MMRecord 实体的列表、每个实体的主键、用于填充各种属性的所有键以及指向数据模型中该实体实例的关键路径。这是您如何使用它的。

#define FBMMRecordTweakModelDefine
    [FBMMRecordTweakModel loadTweaksForManagedObjectModel:
        [MMDataManager sharedDataManager].managedObjectModel];

这就是您需要的内容来启用您的 MMRecord 项目的微调。作为最佳实践,您应该只在调试模式下使用 #define。

设置完成后,以下是含 MMRecord 的微调界面。

MMRecord Tweaks UI

调试

MMRecordDebugger 是一个类,由 MMRecord 使用来向您提供有关模型配置和 MMRecord 如何处理来自您的服务器类的响应的调试信息。您可以使用 MMRecordDebugger 帮助解决模型配置中可能存在的问题,或识别与响应格式的偏差。

MMRecord 被设计得尽可能快和简单,以便从 Web 服务中序列化托管对象。该库的一个目标是提供有意义的自定义方式来支持所有类型的响应格式,同时仍保持一个易于使用的顶级界面,无需大量配置和设置。在大多数情况下,MMRecord 用户所需的配置和定制数量将取决于您的 Web 服务的响应格式的复杂程度。

在处理请求时,如果 MMRecord 遇到错误,它可能会根据错误的严重程度采取一些措施。

  • 断言。在一些情况下,例如,正在填充的托管对象类不是 MMRecord 的子类时,会抛出断言。
  • 日志。在许多情况下,MMRecord 将将包含错误的消息记录到控制台。默认情况下,MMRecord 不会实际打印任何内容到控制台,除非您手动指定日志级别。这是出于安全考虑。
  • 非失败错误。在有些情况下,MMRecord 将创建一个描述问题的 NSError 并将其与 MMRecordDebugger 相关联。但是,如果错误不够严重,则请求不会失败。
  • 失败错误。在几个情况下,MMRecord 将创建一个描述它在处理请求时遇到的严重问题的 NSError。这些错误与调试器关联,并将传递回失败块,指示请求失败的原因。

如果您遇到请求问题,第一步应该是启用 MMRecord 日志记录,使用以下命令。

[MMRecord setLoggingLevel:MMRecordLoggingLevelAll];

您可以将日志级别逐步降低,以接收更细粒度的日志信息,但是从最高级别开始以便更全面地了解情况是一个好主意。

如果您的请求失败,您可以使用传递到失败块的 NSError 对象来审查有关失败的各种数据。失败块中的错误参数实际上将包括 MMRecordDebugger 实例,其中包含在处理请求时遇到的全部错误以及与关键错误相关的各种状态。

调试器已被附加到 userInfo 字典中的 NSError。以下是如何使用它的一个示例。

     failureBlock:^(NSError *error) {
         NSDictionary *recordDictionary = [[error userInfo] valueForKey:MMRecordDebuggerParameterRecordDictionary];

         MMRecordDebugger *debugger = [[error userInfo] valueForKey:MMRecordDebuggerKey];
         NSArray *allErrors = [debugger errorsEncounteredWhileHandlingResponse];
         id responseObject = [debugger responseObject];
         NSString *entityName = [[debugger initialEntity] name];
     }];

如果您遇到希望跟踪的错误,或有关于某些错误严重性的建议,请创建一个问题或发起一项拉取请求。

需求

MMRecord 1.4.0 及更高版本需要 iOS 6.0 或更高版本,或者 Mac OS 10.8 及更高版本(64位并使用现代 Cocoa 运行时)。请参考以下链接获取更多详细信息:iOS 6.0 及以上版本Mac OS 10.8 及以上版本(需要使用现代 Cocoa 运行时的 64 位系统)。

ARC

MMRecord 使用 ARC。

如果您的项目未使用 ARC,则在所有 MMRecord 源文件中需要设置 -fobjc-arc 编译器标志。

鸣谢

MMRecord 由 Conrad StollMutual Mobile 创建。

许可

MMRecord 遵循 MIT 许可协议。更多信息请参阅 LICENSE 文件。