SQKDataKit 1.2.2

SQKDataKit 1.2.2

测试已测试
Lang语言 Obj-CObjective C
许可证 自定义
发布最后发布2018年1月

Luke StringerSam OakleyZack BrownDavid Yates维护。



  • Luke Stringer, Sam Oakley, Zack Brown, Ken Boucher, Ste Prescott, Ben Walker 和 David Yates

   

一组类,用于简化与 Core Data 的协同工作,并帮助企业减少冗余代码。提供在多线程环境中使用 NSManagedObjectNSManagedObjectContext 时的便捷方法和类。编码一些有效地导入大型数据集的良好实践。

安装

  • 使用 CocoaPods,将 pod 'SQKDataKit' 添加到您的 Podfile。
  • 根据需要使用 #import <SQKDataKit/SQKDataKit.h>

使用

SQKContextManager

SQKContextManager 是您使用 SQKDataKit 的第一个入口点。它为您创建并管理 NSManagedObjectContext 实例。

初始化

您应该只使用一个 SQKContextManager,因为它维护您的 Core Data 堆栈的持久存储协调器实例。建议您在应用程序的初始加载时创建它,例如在您的 AppDelegate 中。使用并发类型和管理对象模型初始化一个上下文管理器

#import <SQKDataKit/SQKDataKit.h>`

@interface SQKAppDelegate ()
@property (nonatomic, readwrite, strong) SQKContextManager *contextManager;
@end

@implementation SQKAppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    [self setupContextManager];

    return YES;
}

- (void)setupContextManager {
    if (!self.contextManager) {
        NSManagedObjectModel *model = [NSManagedObjectModel mergedModelFromBundles:nil];
        self.contextManager = [[SQKContextManager alloc] initWithStoreType:NSSQLiteStoreType
                                                        managedObjectModel:model
                                            orderedManagedObjectModelNames:@[ @"DataModelName" ]
                                                                  storeURL:nil];
    }
}

@end

对于基于 SQLite 的持久存储,指定 NSSQLiteStoreType。如果您正在编写与 Core Data 交互的单元测试,那么具有 NSInMemoryStoreType 的上下文管理器是有用的,因为更改不会在测试套件运行之间持久化,并且生产 SQLite 数据库的副作用不会污染您的测试。

如果您只有一个数据模型,那么 [NSManagedObjectModel mergedModelFromBundles:nil] 将返回此。

上下文管理器提供了一种方便的方法来获取两种常见的 NSManagedObjectContext 对象。

使用主上下文

只有一个主上下文,并且通过 mainContext 方法获得。您应该在此上下文中使用任何在 UI 线程上与 Core Data 交互的操作,例如当使用 NSFetchedResultsController 时。此上下文使用 NSMainQueueConcurrencyType 进行初始化,因此应仅在主线程上使用。

在后台线程中不要使用主上下文。在主线程上未能正确使用主上下文将导致行为不一致和可能崩溃。为确保安全,如果在非主线程上请求上下文管理器的主上下文,则会抛出异常。

使用私有上下文

私有上下文使用NSPrivateQueueConcurrencyType进行初始化。它们的设计是在主线程之外执行Core Data工作。在某些情况下,在进行Core Data操作时使用后台线程或队列是有益的;特别是当你想确保在Core Data进行长时间运行的任务时,应用程序的用户界面依然保持响应。

通过newPrivateContext方法获取私有上下文。这将根据持久存储的当前状态创建一个新的私有上下文。从概念上讲,你可以将主上下文视为“分支”到另一个(私有)上下文。在拥有新的私有上下文时,你应该只在该创建它的线程上使用它。

你使用私有上下文进行的任何工作和所做的任何更改都独立于主上下文的状态。当你保存私有上下文时,你的SQKContextManager实例会监听保存通知,并代表你将更改合并回主上下文中。然后UI控制器和主上下文中使用对象将自动获得这些更新。

请确保将私有上下文保留在一个属性中。如Apple文档所述

托管对象知道与它们相关联的托管对象上下文,托管对象上下文知道它们包含的托管对象。但是,默认情况下,托管对象和其上下文之间的引用是弱引用。这意味着通常不能依靠上下文来确保托管对象实例的持久性,也不能依靠托管对象的存在来确保上下文的持久性。换句话说,仅仅因为您获取了一个对象,并不意味着它将一直存在。

注意:与需要您自身负责保留的新私有上下文不同,主上下文由上下文管理器保留。

自动合并

默认情况下,在私有上下文中所做的任何更改在默认情况下都会合并到主上下文中。如果需要出于任何原因禁用此功能,可以将shouldMergeOnSave设置为NO。然后,您将负责在您的用例中合适的时机进行合并。

self.privateContext = [self.contextManager newPrivateContext];
Self.privateContext.shouldMergeOnSave = NO;

并发

Apple文档的引用

Core Data使用线程(或序列化队列)限制来保护托管对象和托管对象上下文(请参阅“Core Data的并发”)。这带来的一个后果是,一个上下文假定默认所有者是分配它的线程或队列——这是由调用其init方法的线程决定的。因此,你不应该在某个线程上初始化上下文然后将它传递给另一个线程。相反,你应该传递一个持久存储协调器的引用,并由接收到的线程/队列创建一个从该存储器派生的新的上下文。如果你使用NSOperation,你必须将上下文创建在主线程(对于序列化队列)或开始线程(对于并发队列)。

当使用 SQKDataKit 时,您不需要为应用程序中使用 Core Data 的每个部分传递持久存储协调器的引用。只需传递一个 SQKContextManager 的实例,因为它会维护持久存储协调器。从您打算在它上进行 Core Data 操作的线程/队列请求 newPrivateContext:。确保只在创建新私人上下文的线程上使用,否则可能会产生意外的副作用和/或崩溃。

NSManagedObject+SQKAdditions

NSManagedObject 添加功能以减少样板代码并简化常见操作,例如创建获取请求或插入对象的新实例。这些方法永不直接在 NSManagedObject 上调用(例如,[NSManagedObject sqk_entityName]),而应在子类上调用。

优化批插入或更新

包括优化批插入或更新的一个方法,这是在从网络服务更新时应用程序中的一个常见模式。此方法将 Apple 指南中找到的模式编码化高效实现查找或创建。使用方法(在后台队列上):

NSArray *dictArray = @[
                       @{@"UserID" : @"123", @"Name" : @"Bob", @"Age" : @65, @"PostIDs" : @[@"abc", @"def"]},
                       @{@"UserID" : @"456", @"Name" : @"Alice", @"Age" : @17, @"PostIDs" : @[@"ghi", @"jkl"]},
                       @{@"UserID" : @"789", @"Name" : @"Charlie", @"Age" : @47, @"PostIDs" : @[@"mno", @"pqr", @"stu"]}
                       ];

self.privateContext = [self.contextManager newPrivateContext];

NSError *error = nil;
[self.privateContext performBlockAndWait:^{
    [User SQK_insertOrUpdate:dictArray
              uniqueModelKey:@"userID" // property name for the primary key of User model
             uniqueRemoteKey:@"UserID"
         propertySetterBlock:^(NSDictionary *dictionary, id managedObject) {
             User *user = (User *)managedObject;
             user.name = dictionary[@"Name"];
             user.age = dictionary[@"Age"];
         }
              privateContext:self.privateContext
                       error:&error];
    [self.privateContext save:nil];  
}];

当处理从网络服务获取的数据时,经常仅知道对象的 GUID。例如,用户可能有多个帖子,但表示该用户的 JSON 对象仅指定这些帖子的 GUID 数组,而不是完整的提交对象本身。例如:

User *user = ... // parsed somewhere else

NSArray *postIDs = @[
                     @"mno",
                     @"pqr"
                     @"stu"
                     ];

NSError *error = nil;
[self.privateContext performBlockAndWait:^{
    [Post sqk_insertOrUpdate:postIDs
              uniqueModelKey:@"postID" // property name for the primary key of Post model
             uniqueRemoteKey:@"self"
         propertySetterBlock:^(NSDictionary *dictionary, id managedObject) {
             Post *post = (Post *)managedObject;
             post.user = user;
         }
              privateContext:self.privateContext
                       error:&error];
    [self.privateContext save:nil];  
}];


警告

出于速度考虑,此方法只执行一个获取请求。因此,您必须注意 propertySetterBlock 中的操作。例如,如果使用与远程数据中相同的 ID 在 propertySetterBlock 中插入对象,则此方法将不知道它已存在,并会再次插入它,从而导致重复。通常,您应该在 propertySetterBlock 中避免启动任何 Core Data 操作 - 您只需要应用设置托管对象属性所需的逻辑。

SQKManagedObjectController

跟踪您获取的任何 NSManagedObjects 至关重要。如果您持有对象的引用,但它在其他地方(可能是作为后台同步操作的一部分)被删除,那么当您尝试访问它时,将会引发异常,并且应用可能会崩溃。也许它在后台被编辑了——但是详细视图不知道,因此您显示了过时的信息。

NSFetchedResultsController 避免这些问题,因为它监听 Core Data 通知并保持自身的更新。如果您需要一个由 Core Data 支持的 tableview,始终在可能的情况下使用 NSFetchedResultsController

在其他情况下,NSFetchedResultsController 是一项较大的解决方案。一个 SQKManagedObjectController 就像 FRC 一样,但更简单——它管理获取请求,保留对象,并在需要时刷新它们。

初始化

NSFetchRequest *request = [Commit SQK_fetchRequest];
self.controller = [[SQKManagedObjectController alloc] initWithFetchRequest:request
                                                          managedObjectContext:[self.contextManager mainContext]];
[self.controller performFetch:&error];                                                        

或者,如果您已经有想要管理的对象(例如,它们传递给详细视图)

SQKManagedObjectController *objectsController = [[SQKManagedObjectController alloc] initWithWithManagedObjects:[self.controller managedObjects]];

委托

当对象被检索(调用 performFetch: 的结果)、修改、插入或删除时,会调用控制器代理的方法。这些方法包括

-(void)controller:(SQKManagedObjectController*)controller
        fetchedObjects:(NSIndexSet*)fetchedObjectIndexes error:(NSError**)error;
-(void)controller:(SQKManagedObjectController*)controller
        didSaveObjects:(NSIndexSet*)savedObjectIndexes;
-(void)controller:(SQKManagedObjectController*)controller
        didInsertObjects:(NSIndexSet*)insertedObjectIndexes;
-(void)controller:(SQKManagedObjectController*)controller
        didDeleteObjects:(NSIndexSet*)deletedObjectIndexes;

索引集合包含在 controller.managedObjects 中被检索、插入、编辑或删除的对象的索引。该对象的集合会通过监控保存通知自动保持最新状态 - 新的对象会根据指定的检索请求被添加,现有对象则通过 refreshObject:mergeChanges: 更新。之后,您需要决定如何使用这些信息 - 例如,更新一些可见数据,或者从堆栈中弹出视图控制器。

如果您更喜欢使用块而不是代理,则可以设置 fetchedObjectsBlocksavedObjectsBlockinsertedObjectsBlockdeletedObjectsBlock,或者同时使用代理。请注意,如果两者都设置了,则会先调用代理方法。

并发

通常,此类只为从主线程使用,在主线程的上下文中使用对象。在其他任何情况下,您的使用效果可能会有所不同。

SQKFetchedTableViewController

上面,我告诉您如果您有一个以 Core Data 支持的表格视图,应该使用 NSFetchedResultsController。但“太多 样板代码!”您抱怨,“如果有一种简单的方法来创建一个以 Core Data 支持的、可搜索、可过滤的 UITableView 控制器该多好!”

SQKFetchedTableViewController 以更简单的方式进行 replicating,常用的模式之一就是一个可搜索的以 Core Data 支持的表格视图。它必须被子类化。

请参阅示例项目中 SQKCommitsViewController 的实现。

使用

SQKFetchedTableViewController 进行子类化并覆盖以下方法

- (void)fetchedResultsController:(NSFetchedResultsController *)fetchedResultsController configureCell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath;

这个方法将设置显示的单元格。您应从自己的 tableView:cellForRowAtIndexPath: 方法中调用此方法。

还有

- (NSFetchRequest *)fetchRequestForSearch:(NSString *)searchString;

在这里,您必须返回一个针对指定搜索字符串的 NSFetchRequest。如果 searchString 是 nil,则返回未过滤的数据集。这将在用户输入搜索字符串时多次调用。

分区索引

要在 SQKFetchedTableViewController 子类中使用分区索引

- (NSString *)sectionKeyPathForSearchableFetchedResultsController:(SQKFetchedTableViewController *)controller 
{
    return @"uppercaseFirstLetterTitle"; // the sectionKeyPath
}

- (NSArray *)sectionIndexTitlesForTableView:(UITableView *)tableView 
{
    // No section indexes if searching
    if (self.searchIsActive) {
        return nil;
    }
    return self.sectionIndexes;
}

 (NSString *)tableView:(UITableView *)tableView
        titleForHeaderInSection:(NSInteger)section
{
    return [[[UILocalizedIndexedCollation currentCollation] sectionTitles] objectAtIndex:section];
}

- (NSArray *)sectionIndexTitlesForTableView:(UITableView *)tableView 
{
    return [[UILocalizedIndexedCollation currentCollation] sectionIndexTitles];
}

- (NSInteger)tableView:(UITableView *)tableView
    sectionForSectionIndexTitle:(NSString *)title
                        atIndex:(NSInteger)index
{
    return [[UILocalizedIndexedCollation currentCollation] sectionForSectionIndexTitleAtIndex:index];
}

SQKCoreDataOperation

当您需要在主线程之外使用 Core Data 执行任务时,请使用 SQKCoreDataOperation。

您需要对其进行子类化并重写 performWorkWithPrivateContext: 方法,在这里您应该使用 Core Data 执行工作。操作将使用其 SQKContextManager 获取一个私有管理的对象上下文。这将传递给 performWorkPrivateContext: 方法供您使用。当您的工作完成时,通过传入您使用的私有上下文调用 completeAndSave 方法。这将保存(私有)管理的对象上下文,将更改合并到主上下文,并完成操作。

将操作添加到一个不是 mainQueue 的 NSOperationQueue 中,这样就可以在主线程之外进行计算。由于使用了私有上下文,因此对插入、更新、删除等进行操作时**必须**在后台线程中完成,并且使用正确的操作队列将确保这一点。

使用

如何进行子类化

#import "AnimalImportOperation.h"
#import "Animal.h"
#import "NSManagedObject+SQKAdditions.h"

@interface AnimalImportOperation ()
@end

@implementation AnimalImportOperation

- (void)performWorkPrivateContext:(NSManagedObjectContext *)context {
    id animalJSON = [self animalJSONFromWebservice];

    [Animal SQK_insertOrUpdate:animalJSON
                uniqueModelKey:@"animalID"
               uniqueRemoteKey:@"IDAnimal"
           propertySetterBlock:^(NSDictionary *dictionary, id managedObject) {
               Animal *animal = (Animal *)managedObject;
               animal.name = dictionary[@"Name"];
               animal.age = dictionary[@"Age"];
           }
                privateContext:self.privateContext
                         error:NULL];
    [self completeAndSave];
}

- (id)animalJSONFromWebservice {
    NSURL *URL = [NSURL URLWithString:@"http://webservice.com/v1/animal"];
    NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:URL];
    [request setHTTPMethod:@"GET"];
    [request setValue:@"application/json" forHTTPHeaderField:@"Accept"];
    [request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];

    NSData *reponseData = [NSURLConnection sendSynchronousRequest:request returningResponse:NULL error:NULL];
    id JSON = reponseData != nil ? [NSJSONSerialization JSONObjectWithData:reponseData options:0 error:NULL] : nil;

    return JSON
}

@end

与操作队列一起使用。

NSOperationQueue *operationQueue = [[NSOperationQueue alloc] init]; // background thread queue

AnimalImportOperation *importOperation = [[AnimalImportOperation alloc] initWithContextManager:self.contextManager];

[importOperation setCompletionBlock:^{
    // Completion logic here
    [[NSOperationQueue mainQueue] addOperationWithBlock:^{
        // You may want to perform this on the main thread
    }];
}];

[self.operationQueue addOperation:importOperation];

致谢

许可

版权(c)3Squared Ltd

任何人获得本软件和相关文档文件(以下简称“软件”)的副本,都可以免费地处理该软件,包括(但不限于)使用、复制、修改、合并、发布、分发、许可或许可转让软件的副本,并允许向软件提供的人这样做,前提是遵守以下条件:

必须包含上述版权声明和本许可声明于软件的全部复制件或主要部分。

软件按“原样”提供,不提供任何形式的明示或暗示保证,包括但不限于适销性、适用于特定目的和不侵犯专利权。在任何情况下,作者或版权持有者不应对由于合同、侵权或其他原因而产生的任何索赔、损害或其他责任承担责任,无论这些索赔、损害或其他责任是否因此软件或其使用或其他方式产生。