iOS-数据持久化-CoreData
CoreData详解
介绍:
在Cocoa环境下,如果你想使用数据库(如sqlite),你可以使用sql语句的方式通过相关的工具类进行数据库的直接操作。当然你也可以通过别人封装之后的一些简单框架,使得你的操作更加简单(如FMDB BNRPersistence)。
Cocoa框架本身提供了CoreData这个API可方便的让开发者通过操作对象的方式在操作数据库。CoreData是一个对象图(object graph)以及持久化的管理框架。我们可以通过CoreData创对象,设置好象之间的关系,然后将其持久化(我们甚至可以使用内存数据库),或者从硬盘上将持久化后的数据加载到内存中。对象图,我们可以创建一个个的对象,并维持不同对象之间的关系,一对一,一对多等。
CoreData有大量的特性,诸如支持Redo,Undo的功能,这些很多Document based的程序中显得非常的有用。提供数据model结构变化轻量级的迁移方案。CoreData还通过Binding特性和控件的紧密结合,这样使得只需要少量的代码便可以完成强大的功能,下面是一个例子
http://www.timisted.net/blog/archive/multiple-windows-with-core-data/
存储方式
Core Data可以将数据存储为XML,二进制文件或SQLite文件。在Mac OS X 10.5 Leopard及以后的版本中,开发者也可以通过继承NSPersistentStore类以创建自定义的存储格式。每种方法都有其优缺点,例如XML的可读性,SQLite的节约空间等。
Core Data的这一方面类似于原始的Enterprise Objects Framework(EOF)系统,但EOF中开发者可以使用相对简洁的查询方式,而在Core Data中,只能使用一个语法类似SQL子集的查询语言,称为Predicate。Core Data是标准化的,可以自由的读写Xcode数据模型文件(通常是.xcdatamodel文件)。
与EOF不同,Core Data目前没有设计多用户或多线程访问模式。模型迁移通常也需要代码,若其它开发者依赖于某个数据模型,则该数据模型的设计者可能在模型发生改变时需要与新数据模型一起提供版本转换代码。
操作简介
Core Data由相对庞大的类继承体系组成,但开发者需要关注的接口只是其中的一个相对小的子集。
一般需要定义以下Core Data的三个必备
NSPersistentStoreCoordinator *persistentStoreCoordinator;
NSManagedObjectModel *managedObjectModel;
NSManagedObjectContext *managedObjectContext;
以及使用时需要用到的
NSFetchedResultsController *fetchedResultsController;
使用步骤:
还记得我们每次使用CoreData的时候系统都会给我们创建一些代码吗?
1 #pragma mark - Core Data 堆栈 2 //返回 被管理的对象上下文 3 - (NSManagedObjectContext *)managedObjectContext 4 { 5 if (_managedObjectContext) { 6 return _managedObjectContext; 7 } 8 9 NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator]; 10 if (coordinator) { 11 _managedObjectContext = [[NSManagedObjectContext alloc] init]; 12 [_managedObjectContext setPersistentStoreCoordinator:coordinator]; 13 } 14 return _managedObjectContext; 15 } 16 17 // 返回 持久化存储协调者 18 - (NSPersistentStoreCoordinator *)persistentStoreCoordinator 19 { 20 if (_persistentStoreCoordinator) { 21 return _persistentStoreCoordinator; 22 } 23 24 NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:@"CoreDataNotes.sqlite"]; 25 26 _persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]]; 27 28 [_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType 29 configuration:nil 30 URL:storeURL 31 options:nil 32 error:nil]; 33 34 return _persistentStoreCoordinator; 35 } 36 37 // 返回 被管理的对象模型 38 - (NSManagedObjectModel *)managedObjectModel 39 { 40 if (_managedObjectModel) { 41 return _managedObjectModel; 42 } 43 NSURL *modelURL = [[NSBundle mainBundle] URLForResource:@"CoreDataNotes" withExtension:@"momd"]; 44 _managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL]; 45 return _managedObjectModel; 46 } 47 48 #pragma mark - 应用程序沙箱 49 // 返回应用程序Docment目录的NSURL类型 50 - (NSURL *)applicationDocumentsDirectory 51 { 52 return [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject]; 53 }
下面是使用Core的示例代码
1 static NoteDAO *sharedManager = nil; 2 3 + (NoteDAO*)sharedManager 4 { 5 static dispatch_once_t once; 6 dispatch_once(&once, ^{ 7 8 sharedManager = [[self alloc] init]; 9 [sharedManager managedObjectContext]; 10 11 }); 12 return sharedManager; 13 } 14 15 16 //插入Note方法 17 -(int) create:(Note*)model 18 { 19 20 NSManagedObjectContext *cxt = [self managedObjectContext]; 21 22 NoteManagedObject *note = [NSEntityDescription insertNewObjectForEntityForName:@"Note" inManagedObjectContext:cxt]; 23 [note setValue: model.content forKey:@"content"]; 24 [note setValue: model.date forKey:@"date"]; 25 26 note.date = model.date; 27 note.content = model.content; 28 29 NSError *savingError = nil; 30 if ([self.managedObjectContext save:&savingError]){ 31 NSLog(@"插入数据成功"); 32 } else { 33 NSLog(@"插入数据失败"); 34 return -1; 35 } 36 37 return 0; 38 } 39 40 //删除Note方法 41 -(int) remove:(Note*)model 42 { 43 44 NSManagedObjectContext *cxt = [self managedObjectContext]; 45 46 NSEntityDescription *entityDescription = [NSEntityDescription 47 entityForName:@"Note" inManagedObjectContext:cxt]; 48 49 NSFetchRequest *request = [[NSFetchRequest alloc] init]; 50 [request setEntity:entityDescription]; 51 52 NSPredicate *predicate = [NSPredicate predicateWithFormat: 53 @"date = %@", model.date]; 54 [request setPredicate:predicate]; 55 56 NSError *error = nil; 57 NSArray *listData = [cxt executeFetchRequest:request error:&error]; 58 if ([listData count] > 0) { 59 NoteManagedObject *note = [listData lastObject]; 60 [self.managedObjectContext deleteObject:note]; 61 62 NSError *savingError = nil; 63 if ([self.managedObjectContext save:&savingError]){ 64 NSLog(@"删除数据成功"); 65 } else { 66 NSLog(@"删除数据失败"); 67 return -1; 68 } 69 } 70 71 return 0; 72 } 73 74 //修改Note方法 75 -(int) modify:(Note*)model 76 { 77 NSManagedObjectContext *cxt = [self managedObjectContext]; 78 79 NSEntityDescription *entityDescription = [NSEntityDescription 80 entityForName:@"Note" inManagedObjectContext:cxt]; 81 82 NSFetchRequest *request = [[NSFetchRequest alloc] init]; 83 [request setEntity:entityDescription]; 84 85 NSPredicate *predicate = [NSPredicate predicateWithFormat: 86 @"date = %@", model.date]; 87 [request setPredicate:predicate]; 88 89 NSError *error = nil; 90 NSArray *listData = [cxt executeFetchRequest:request error:&error]; 91 if ([listData count] > 0) { 92 NoteManagedObject *note = [listData lastObject]; 93 note.content = model.content; 94 95 NSError *savingError = nil; 96 if ([self.managedObjectContext save:&savingError]){ 97 NSLog(@"修改数据成功"); 98 } else { 99 NSLog(@"修改数据失败"); 100 return -1; 101 } 102 } 103 return 0; 104 } 105 106 //查询所有数据方法 107 -(NSMutableArray*) findAll 108 { 109 NSManagedObjectContext *cxt = [self managedObjectContext]; 110 111 NSEntityDescription *entityDescription = [NSEntityDescription 112 entityForName:@"Note" inManagedObjectContext:cxt]; 113 114 NSFetchRequest *request = [[NSFetchRequest alloc] init]; 115 [request setEntity:entityDescription]; 116 117 NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"date" ascending:YES]; 118 [request setSortDescriptors:@[sortDescriptor]]; 119 120 NSError *error = nil; 121 NSArray *listData = [cxt executeFetchRequest:request error:&error]; 122 123 NSMutableArray *resListData = [[NSMutableArray alloc] init]; 124 125 for (NoteManagedObject *mo in listData) { 126 Note *note = [[Note alloc] init]; 127 note.date = mo.date; 128 note.content = mo.content; 129 [resListData addObject:note]; 130 } 131 132 return resListData; 133 } 134 135 //按照主键查询数据方法 136 -(Note*) findById:(Note*)model 137 { 138 NSManagedObjectContext *cxt = [self managedObjectContext]; 139 140 NSEntityDescription *entityDescription = [NSEntityDescription 141 entityForName:@"Note" inManagedObjectContext:cxt]; 142 143 NSFetchRequest *request = [[NSFetchRequest alloc] init]; 144 [request setEntity:entityDescription]; 145 146 NSPredicate *predicate = [NSPredicate predicateWithFormat: 147 @"date = %@",model.date]; 148 [request setPredicate:predicate]; 149 150 NSError *error = nil; 151 NSArray *listData = [cxt executeFetchRequest:request error:&error]; 152 153 if ([listData count] > 0) { 154 NoteManagedObject *mo = [listData lastObject]; 155 156 Note *note = [[Note alloc] init]; 157 note.date = mo.date; 158 note.content = mo.content; 159 160 return note; 161 } 162 return nil; 163 }
CoreData高级常识
关于CoreData貌似实际开发中很少用到,基本上是个有九个公司不会使用它,因为都说是性能不好,但是作为一个程序员,了解及其使用时必须了,
下面是我从一位大神那里搬过来的一下Core详细介绍,相信以后总有一天会帮我解决不少学习CoreData中的问题!
一、技术概览
1. Core Data 功能初窥
对于处理诸如对象生命周期管理、对象图管理等日常任务,Core Data框架提供了广泛且自动化的解决方案。它有以下特性。
(注:对象图-Object graph的解释:在面向对象编程中,对象之间有各种关系,例如对象直接引用另外的对象,或是通过引用链间接的引用其他对象,这些关系组成了网状的结构。 我们把这些对象(和它们之间的联系)成为对象图。 对象图可大可小,有繁有简。 只包含单个字符串对象的数组就是一个简单的代表;而包含了application对象,引用windows, menus和相关视图对象、其他对象这样的结构就是复杂对象图的例子——这是在说mainwindow.xib。
有时,你可能想要把这样的对象图转化形式,让它们可以被保存到文件中,以使其他的进程或其他的机器可以再次将保存的内容读出,重购对象。 这样的过程常被成之为“归档”(Archiving)。
有些对象图是不完整的——通常称之为局部对象图(partial object graphs)。局部对象图包含了“占位符”(Placeholder)对象,所谓”占位符“,就是一些暂时无内容的对象,它们将再后期被具体化。一个典 型的例子就是nib文件中包含的File's Owner对象。
1) 对于key-value coding 和key-value observing完整且自动化的支持
除了为属性整合KVC和KVO的访问方法外, Core Data还整合了适当的集合访问方法来处理多值关系。
2) 自动验证属性(property)值
Core Data中的managed object扩展了标准的KVC 验证方法,以保证单个的数值在可接受的范围之内,从而使组合的值有意义。(需校准翻译)
3) 支持跟踪修改和撤销操作
对于撤销和重做的功能,除过用基本的文本编辑外,Core Data还提供内置的管理方式。
4) 关系的维护
Core Data管理数据的变化传播,包括维护对象间关系的一致性。
5) 在内存中和界面上分组、过滤、组织数据
6) 自动支持对象存储在外部数据仓库的功能
7) 创建复杂请求
你不需要动手去写复杂的SQL语句,就可以创建复杂的数据请求。方法是在“获取请求”(fetch request)中关联NSPredicate(又看到这个东东了,之前用它做过正则)。NSPrdicate支持基本的功能、相关子查询和其他高级的 SQL特性。它还支持正确的Unicode编码(不太懂,请高人指点), 区域感知查询(据说就是根据区域、语言设置调整查询的行为)、排序和正则表达式。
8) 延迟操作(原文为Futures(faulting)直译为期货,这里个人感觉就是延迟操作的形象说法。请高人指教)。
Core Data 使用延迟加载(lazy loading)的方式减少内存负载。 它还支持部分实体化延迟加载,和“写时拷贝”的数据共享机制。(写时拷贝,说的是在复制对象的时候,实际上不生成新的空间,而是让对象共享一块存储区域, 在其内容发生改变的时候再分配)。
9) 合并的策略
Core Data 内置了版本跟踪和乐观锁定(optimistic locking)来支持多用户写入冲突的解决。
注:乐观锁,假定数据一般不出现冲突,所以在数据提交更新的时候,才对数据的冲突进行检测,如果冲突了,就返回冲突信息。
10) 数据迁移
就开发工作和运行时资源来说,处理数据库架构的改变总是很复杂。Core Data的schema migration工具可以简化应对数据库结构变化的任务, 而且在某些情况下,允许你执行高效率的数据库原地迁移工作。
11) 可选择针对程序Controller层的集成,来支持UI的显示同步
Core Data在iPhone OS之上 提供NSFetchedResultsController对象来做相关工作,在Mac OS X上,我们用Cocoa提供的绑定(Binding)机制来完成。
2. 为何要使用Core Data
使用Core Data有很多原因,其中最简单的一条就是:它能让你为Model层写的代码的行数减少为原来的50%到70%。 这归功于之前提到的Core Data的特性。更妙的是,对于上述特性你也既不用去测试,也不用花功夫去优化。
Core Data拥有成熟的代码,这些代码通过单元测试来保证品质。应用Core Data的程序每天被世界上几百万用户使用。通过了几个版本的发布,已经被高度优化。 它能利用Model层的信息和运行时的特性,而不通过程序层的代码实现。 除了提供强大的安全支持和错误处理外,它还提供了最优的内存扩展性,可实现有竞争力的解决方案。不使用Core Data的话,你需要花很长时间来起草自己的方案,解决各种问题,这样做效率不高。
除了Core Data本身的优点之外,使用它还有其他的好处: 它很容易和Mac OS X系统的Tool chain集成;利用Model设计工具可以按图形化方式轻松创建数据库的结构;你可以用Instruments的相关模板来测试Core Data的效率并debug。 在Mac OS X的桌面程序中,Core Data还和Interface Builder集成(打开Inspector可以看到有binding的选项,这个东东iPhone上木有。。。),按照model来创建UI变的更简单 了。 这些功能能更进一步的帮助你缩短设计、开发、测试程序的周期。
3. Core Data不是。。。
看了前面的介绍之后,我们还需要了解一下关于Core Data常见的误解:
1) Core Data不是一个关系型数据库,也不是关系型数据库管理系统(RDBMS)。
Core Data 为数据变更管理、对象存储、对象读取恢复的功能提供了支持。 它可以使用SQLite作为持久化存储的类型。 它本身并不是一个数据库(这点很重要,比如,你可以使用Core Data来记录数据变更,管理数据,但并不能用它向文件内存储数据)。
2) Core Data不是银弹
它并不能取代你写代码的工作。虽然可以纯粹使用XCode的数据建模工具和Interface Builder来编写复杂程序,但在更多的程序中,你都自己动手写代码。
3) Core Data并不依赖于Cocoa Bindings
Core Data + Cocoa Binding = 减少代码数量。但Core Data完全可以在没有bindings的条件下使用。例如,可以编写一个没有UI,但包含Core Data的程序。
二、Core Data基础
1. Core Data基本架构
在大部分程序中,你要能通过某种方式打开一个包含对象归档的文件, 这个文件内至少要有一个根对象的引用。另外,还得能将所有的对象归档到文件中,如果你想要实现撤销的功能,就还要记录对象的更改情况。例如,在 Employee的示例程序中,你要能打开一个包含有employee和department对象归档的文件,而且这个文件至少包含了一个根对象——这 里,是一个包含所有employee的数组——请参考例图Figure 1。 相应的,你还要能将程序中的employee、department对象归档到文件中去。
Figure 1 按照Core Data文档结构管理的对象示意图
使用Core Data的框架,大多数的功能都可以自动实现,因为我们有managed object context(管理对象的上下文,有时直接叫"Context")。managed object context就像是一个关卡,通过它可以访问框架底层的对象——这些对象的集合我们称之为"persistence stack"(数据持久栈)。 managed object context作为程序中对象和外部的数据存储的中转站。栈的底部是persistence object stores(持久化数据存储),请看Figure 2的示意图。
Figure 2 使用Core Data的文档管理示意图
Core Data的使用并不限制在基于文档的程序中(document-based application)。你也能创建一个包含Core Data 的Utility程序(请查看Core Data Utility tutorial文档)。当然其他类型的程序也都可以使用Core Data。
被管理对象和上下文(Managed Objects and Contexts)
你可以把被管理对象上下文想象成一个”聪明“的便笺簿。当你从数据持久层获取对象时,就把这些临时的数据拷贝拿到写在自己的便笺簿上(当然,在便笺上对象 会 “恢复”以前的对象图结构)。然后你就可以随心所欲的修改这些值了(本子是你的,随便画都可以),除非你保存这些数据变化,否则持久层的东西是不会变 的。(跟修改文件后要保存是一个道理)。
附在Core Data框架中模型对象(Model objects)常被称为“被管理对象”(Managed objects)。所有的被管理对象都要通过上下文进行注册。使用上下文,你可以在对象图中添加、删除对象,并记录对象的更改(包括单个对象,或是对象间 的关系)。记录更改后就能支持撤销和重做的功能。同时,上下文还能保证关系更改后对象图的完整性。
如果你想要保存所做的修改, 上下文会保证对象的有效性。在验证有效性后,更改会被写入到persistent store(持久化存储层)中。你在程序中的添加和删除动作都会被作用在存储的数据中。
在你的一个程序中,可能存在多个上下文。 对于数据存储(store)中的每个对象,对应的都有唯一的一个被管理对象(managed object)和上下文相关联(详情请查看"Faulting and Uniquing"文档)。换个角度来想,在persistent store中存储的对象有可能被用在不同的上下文中,每个上下文都有与之对应的被管理对象,被管理对象可以被独立的修改,这样就可能在存储 时导致数据的不一致。Core Data提供了许多解决这个问题的途径(请查看"Using Managed Object"一章)。
获取数据的请求(Fetch Requests)
要使用上下文来获取数据,你需要创建相应的请求(Fetch request)。 Fetch request对象包含你想获取的对象的描述。例如:“所有 Employee”,或“所有的Employee,department是marketing,按薪资降序排列”。Fetch Request包含三个部分。使用最简单的写法,必须指定实体(Entity)的名称,这就暗示了,每次智能获得一种类型的实体。 Fetch Request 还可以包含谓词(predicate)——注:有些地方也把这个叫断言,个人感觉谓词更准确些。谓词将描述对象需要满足的条件(这就和我们在SQL里加的 限定条件差不多,正如前面的"All Employees, in the Marketing department")。另外,Fetch Request还可包含一个用于描述排序方式的对象(熟悉的Order by操作)。如图Figure3所示:
Core Data追求高执行效率。 它是“需求驱动”的,因此只会创建你确实需要的对象。对象图不需要保留所有在数据存储层中的对象。单纯指定数据持久层的动作不会将其中所有的数据放到上下 文中去。 当你想从数据存储层中获取某些对象的时候,你只会得到那些你请求的(有点罗嗦,总的意思就是需要时获取,获取的就是需要的)。如果你不在需要这个对象的时 候,默认情况下它会被释放。(当然,只是释放这个对象,而不是从对象图中移除该对象)。——注:个人感觉有点像重新拷了一个文件的某些部分,不用了就在副 本中删除,不会影响原件。
持久化存储助理(Persistent Store Coordinator)
之前提到过,程序中的对 象和外部存储的数据通过Core Data框架中的一系列对象进行协调,这一系列的对象总的被称为持久存储栈(Persistence stack)。在栈顶是被管理对象上下文(Managed object context),而栈底是持久化对象存储层(Persistence object store)。在它们之间就是持久化存储助理。
事实上,持久化存储助理定义了一个栈。从设计方面考虑,它就是可以作为上下 文的”外观“, 这样多个数据存储(Persistence store)看起来就像是一个。 然后上下文就可以根据这些数据存储来创建对象图了。持久化存储助理智能关联一个被管理对象的模型。如果你像要把不同的实体放到不同的存储中去,就需要为你 的模型实体做“分区”,方式是通过定义被管理对象模型的configurations。(请参考"Configurations"一章)。
Figure 4演示了这样的一个结构:employees和departments存储在一个文件中,customers和companies存储在另外一个文件中。当你要获取对象的时候,它们从相关的文件中自动获取;当保存时,又被归档到相应的文件中。
Figure 4存储栈—改
持久化存储(Persistent Stores)
持久化存储是和单独的一个文件或外部的数据关联的,它负责将数据和上下文中的对象进行对应。通常,需要你直接和持久化对象存储打交道的地方,就是指定新 的、 和程序进行关联的外部数据的位置(例如,当用户打开或保存一个文档)。大多数需要访问持久化存储的动作都由上下文来完成。
程序的代码—— 特别是和被管理对象相关的部分——不应该对持久化存储做任何假设(也就是不需要自己考虑存储的方式或过程)。 Core Data对几种文件格式有原生的支持。你可以选择一种自己程序需要的。假设在某个阶段你决定换一种文件的格式,而又不想修改程序的框架,而且,你的程序做 了适当的抽象(注:这个就属于设计方面的东东了),这时,你就能尝到使用Core Data的甜头了。例如,在最初的设计中,程序只从本地文件中获取数据,而你的程序没有去硬指定对应数据的获取位置,而是可以在后期指定从远程位置添加新 的数据类型,这样你就可以使用新的类型,而不需要修改代码。(这段还是感觉翻的不太合适)。
重要提示:
虽然Core Dta支持SQLite作为一种存储类型,但它不能使用任意的SQLite数据库。Core Data在使用的过程种自己创建这个数据库。(详情,请参考"Persistence Store Features")。
持久化文档(Persistent Documents)
你可以通过代码的方式创建和配置持久存储栈,但在多数情况下,你只是想创建一个基于文档 的应用程序(Document-based application,这个是mac上的)来读写文件。这时,用NSDocument的子类NSPersistentDocument可以让你感受到使 用Core Data的便利。默认状况下,NSPersistentDocument就已经创建了它自己的持久存储栈,其中包含了上下文,和单个的持久对象存储,来处 理这样文档和外部数据“一对一”的映射关系。
NSPersistentDocument类提供了访问文档的上下文的方法,也实现了标准的NSDocument方法来通过Core Data读写文件。 一般说来,你不需要编写额外的代码来处理对象的持久化。
持久化文档的撤销(undo)操作也被集成在被管理对象的上下文中。
被管理对象和被管理对象模型(Managed Objects and the Managed Object Model)
为 了管理对象图,也为了提供对象持久化的功能,Core Data需要对对象有很强的描述能力。被管理对象模型就是程序中对象、实体描述的概要图,如图Figure 5所示。创建模型的常用做法是通过Xcode的图形化建模工具Date Model Design tool。但是如果你愿意的话,也可以在运行时通过代码来建模。
Figure 5 有两个实体的对象模型
Figure 6 带有两个属性和一个关系的的实体描述
被管理对象模型(Managed Object Models)
多数Core Data的功能依赖于你创建的,用来描述程序的实体及其属性、关系的模型图。 模型图由NSManagedObjectModel所表示。一般说来,模型的信息越充实,Core Data能提供的功能就越好。 下文讲解了对象模型的特性,以及如何在程序中创建、使用对象模型。
被管理对象模型的特性
被管理对象模型是 NSManagedObjectModel的实例。它描述了你在程序中使用的实体的概要信息。(如果读者不了解entity、property、 attribute和relationship的含义,请先查看"Core Data Basics"和"Cocoa Design Patterns"文档中的"Object Modeling"一节)
实体(Entities)
模型包含了NSEntityDescription对象,NSEntityDescription对象指代了模型的实体。关于实体由两个重要特征:名称 (name)和类名(name of class)。你应该弄清楚实体、实体的类和作为实体实例的被管理对象之间的区别。
NSEntityDescription 对象可包含NSAttributeDescription对象(指代实体的attribute)和NSRelationshipDescription对 象(指代实体间的relationship)。实体也可能包含fetched属性,该属性由NSFetchedPropertyDescription指 代,模型中有对应的fetch请求的模板,fetch请求由NSFetchRequest所指代。
实体的继承关系
实体的继承和类 的继承很类似,当然,也同样有用。 如果你有若干个相似的实体,就可以抽离出它们的共有特性作为一个“父实体”,就省去了在多个实体中都指定相同的属性。 例如,你可以定义一个包含firstName和lastName的“Person”实体,然后在定义子实体"Employee"和"Customer"。
如果是使用Xcode的可视化建模工具来创建模型,你就可以通过如下图的方式为一个实体指定父级实体。
Figure1 Xcode中为一个实体指定父实体
抽象实体
你可以把一个实体指定为“抽象实体”,也就是说,你不打算使用这个实体来创建实例。通常,当你想把这个实体作为父实体,而有子实体来实现详细内容的时候, 就 把它声明“抽象实体”。(和抽象类很像)。例如,在一个绘图程序中,你可能会设计一个Graphic实体,它包含了x和y坐标信息、颜色、绘制区域,而你 不会去创建一个Graphic的实例,而是使用具体的子实体——Circle、TextArea、Line。(这些基本的东西就不给大牛们再罗嗦 了。。。)
Properties(属性,这个和Attributes的意思一样,实在区别不出来,只好上英语了)
实体的 Properties是它的attributes和relationship,包含了fetched属性(如果有的话)。每个property都有名称和 类型。 Attribute也可能有默认值。property的名称不能和NSObject和NSManagedObject类中的无参方法名相同。例如,不能把 property命名为"description"。
临时属性(Transient Property)也是作为模型的一部分,但是不作为实体实例的数据保存在持久存储层。 Core Data也会跟踪临时属性的变化,以备撤销操作时使用。
注意:如果你用模型外的信息对临时属性执行撤销操作,Core Data将不会使用旧值,调用你的set方法——它只会更新快照信息(snapshot information)。(这段怪怪的,用到的话在修改一下翻译吧)
Attributes
Core Data内部支持各种attribute的类型,例如string,date,integer(NSString, NSDate, NSNumber)。如果你使用那些不支持的数据,你需要用到在“Non-Standard Persistent Attributes”介绍到的技术。
你可以将一个attribute声明为“可选”(optional),可选的attribute不 必须有值,但是,不鼓励你将属性置空——尤其是数字值(更好的解决方案是使用强制的值,在这里,我们用默认值,例如0)。 这样做的原因是为了配合SQL中对于空值NULL做比较的操作:NULL不同于Objective-C中的nil。 数据库中的NULL不同于0,搜索0值的操作不会匹配到值为NULL的列。
false == (NULL == 0)
false == (NULL != 0)
而且,在数据库中,NULL也不等于空字符串或是空的数据对象:
false == (NULL == @"")
false == (NULL != @"")
它们之间一点关系都没有。
关系(Relationships)
Core Data支持对一、对多的关系,也支持fetched属性。 Fetched property表示了一种“弱”的、单项的关系。 在employees和departments的例子中, department 的一个fetched property可能是“最近雇佣人”(recent hires),而反过来,employee不会拥有这样的关系。
获取数据请求的模板(Fetch Request Templates)
我们使用NSFetchRequest类来描述数据请求,利用数据请求从持久存储(persistent store)中获取对象。 经常需要多次执行同样的请求,或是执行某种模式的请求,但是其中包含可变的元素(如查找条件)——这些元素经常有用户提供。 例如,在运行的时候,你要根据用户需要获取某个作者在某个指定日期后的出版的所有出版物。
你可以预定义请求,把它们作为模板存储在被管理对象模型中。 预定义的模板在你需要的时候就可以取出使用。通常情况下,我们通过Xcode的data modeling tool工具创建请求模板。模板可以包含变量,如图Figure 2所示。
Figure 2 Xcode predicate builder
关于Fetch request templates的详细信息,请查看"Accessing and Using a Managed Object Model at Runtime"的描述。
用户信息字典(User Info Dictionaries)
模型中的许多元素,诸如entities, attributes, relationships,都有相关的用户信息字典。用熟悉的键-值对,你可以向其中放置任何你需要的数据。这里常用的信息有实体的版本详情,还有针对 fetched property,给谓词(predicate)用的值。
配置(Configurations)
配置包含了一个名称和若干个相关的实体。实体的集合是可以重叠的——这就是说,一个实体可以出现在多个配置中。在代码中,我们使用 setEntities: forConfiguration:的方法来指定配置。也可以用Xcode的建模工具来指定(选中某个实体,就在属性窗口的第三个,就是一个小扳手的符 号)。要获取某项配置的实体,需要用entitiesForConfiguration:的方法。
一般说来,如果你想把不同的实体存放在不同的存储中去,就可能用到配置。一个持久化存储助理(persistent store coordinator)只能有一个被管理对象模型。所以,默认情况下,和助理关联的某个存储必须包含同样的实体。要想绕过这个限制,你可以创建一个包含 实体子集的模型,然后为每一个子集创建配置,这样一来,使用这个模型创建助理,当你需要添加存储时,可使用不同的配置指定对应的存储属性。当你创建配置的 时候,需要记住,不能创建跨存储的关系。
使用被管理对象模型
通常可以使用Xcode的建模工具来创建模型(请参考"Create a managed object with Xcode")。你也可以全部使用代码来创建(请参考"Core Data Utility Tutorial")。
编译数据模型
数据模型是一种部署资源。 在模型中,除了有实体和属性的详细信息外,用Xcode创建的模型还包含了一些额外的视图信息,包括布局、颜色等等。这些信息在运行时不是必须的。模型文 件在编译的过程中会删除这些额外信息以保证尽可能高效的加载。xcdatamodel“源”文件会被momc编译器编译为mom的目标文件。
"mom" 位于 /Library/Application Support/Apple/Developer Tools/Plug-ins/XDCoreDataModel.xdplugin/Contents/Resources/,如果你想把它用在自己的 build脚本中,格式是:mom source destination, source 就是Core Data Model文件,destination就是输出的mom文件。
加载数据模型
在一些情况下,你不需要写任何加载模型的代码。如果你使用基于文档的程序框架(Document-based application),NSPersistentDocument会管理诸如查找模型、加载模型的任务。 如果你创建了非Document-based application,而且里面又用到了Core Data,一般将获取模型的代码放在application delegate里。模型的存储名称——也就是文件名,
和运行时的名称是不相关的,一旦模型被加载,文件名就没有什么意义了。也就是说,对模型文件,你可以随意命名。
如果你想手动加载模型,有两种方式可用,它们各有各的好处:
你可以从指定的bundle集合里创建整合模型,使用如下的类方法:
mergeModelFromBundles:
也可以用指定的URL加载单个的模型,使用如下的实例方法:
initWithContentsOfURL: (这个方法相信大家都用过)
若不需要考虑分开加载模型,第一个类方法很适用。例如:在你的程序中和程序链接的framework里都有你想要加载的模型。这个类方法可以让你很轻松的加载所有的模型,而不需要考虑模型文件的名称,也不用特定的初始化方法来保证所有的模型都被找到。
但是当你有多个模型要加载,特别是这些模型都代表了一个schema的不同版本,这时,知道要加载哪个模型就很重要了(合并包含相同实体的模型可能导致命 名冲突和错误,我们之前“一锅端”的方法不太合适了)。在这种情况下,我们可以用第二个实例方法。 另外,有时我们也需要将模型存储在bundle之外,也需要用这个方法从指定的URL位置加载模型。
还有一点需要说明:我们还有一个类方法 modelByMergingModels:可以用。像mergedModelFromBundles:方法一样,它也能合并给定的若干个模型。这样,我 们就可以通过URL来逐一加载模型,然后在创建助理对象之前将它们整合为一个。
改变模型
由于模型描述了存储层数据的结构,任何改变模型的动作都将使其不在适配于之前创建的存储层。 如果你改变了模型的结构,就需要将当前存储层的数据迁移到新版本。(请参考"Core Data Model Versioning and Data Migration Programming Guide"文档)。例如:如果你添加了新的实体,或新的属性,你将无法打开旧的存储;如果你添加了验证的限制,或者为属性添加了新的缺省值,你就可以打 开旧的存储。
在运行时访问和适用被管理对象模型
在运行时,被管理对象模型就是一个简单的“对象图”(这个概念之前提到过),认识到这点很重要,尤其是当你需要用代码来访问模型的详细信息时。例如:修改 模型(你只能在runtime之前这样做,请参考 NSManagedObjectModel),取回信息(如本地化实体名,属性数据类型,或数据请求模板)。
在运行时访问模型有很多方法,通过持久栈最终从持久化存储助理得到模型,代码如下:
[[aManagedObjectContext persistentStoreCoordinator]managedObjectModel];
你也可以通过实体描述得到模型,因此给定一个被管理对象,你就可以得到它的实体描述,进而获得模型。代码如下:
[[aManagedObject entity] managedObjectModel];
某些情况下,你要维护模型的“直接”引用,也就是说,一个直接返回模型的方法。NSPersistentDocument提供了 managedObjectModel方法,可以返回一个模型,该模型和在文档的上下文中使用的持久化存储助理相关联。如果你使用Core Data Appplication的模板,application delegate将负责模型的引用。
通过代码创建获取数据请求模板(Fetch Request Templates)
你可以通过代码创建数据请求模板并将其和模型关联,方法是:setFetchRequestTemplate: forName:如Listing-1所示。 提醒一下:你只能在模型被助理(coordinator)使用之前修改它。
Listing 1 通过代码创建获取数据请求模板
NSManagedObjectModel *model = …;
NSFetchRequest * requestTemplate = [[NSFetchRequest alloc]init];
NSEntityDescription *publicationEntity =
[[model entitiesByName] objectForKey: @"Publication"];
[requestTemplate setEntity: publicationEntity];
NSPredicate *predicateTemplate = [NSPredicate predicateWithFormat:
@"(mainAuthor.firstName like[cd] $FIRST_NAME) AND \
(mainAuthor.lastName like[cd] $LAST_NAME) AND \
(publicationDate > $DATE)"];
[requestTemplate setPredicate: predicateTemplate];
[model setFetchRequestTemplate: requestTemplate
forName: @"PublicationForAuthorSinceDate"];
[requestTemplate release];
访问请求模板
你可以用"Accessing and Using a Managed Object Model at Runtime"里介绍的代码片段来获取并使用请求模板。替换字典必须包含和模板中定义的变量对应的键。如果你想测试null值,必须使用NSNull对 象——参考"Using Predicates"。(注:这里的替换字典很好理解,之前的模板中用到了诸如$FIRST_NAME, $LAST_NAME, $DATE这些东西,就相当于我们在模板中创建好的“变量”,我们需要把一个模板“具体化”,就用替换字典,将里面的变量对应一个值,这里看代码就明白 了。)
NSManagedObjectModel *model = …;
NSDictionary *substitutionDictionary = [NSDictionary dictionaryWithObjectsAndKeys:
@"Fiona", @"FIRST_NAME", @"Verde", @"LAST_NAME",
[NSDate dateWithTimeIntervalSinceNow: -31356000], @"DATE", nil]; //这里的FIRST_NAME, LAST_NAME, DATE和我们之前模板里的$FIRST_NAME, $LAST_NAME和$DATE对应
NSFetchRequest *fetchRequest =
[model fetchRequestFromTemplateWithName: @"PublicationForAuthorSinceDate"
substitutionVariables: substitutionDictionary]; //从之前的model中拿出请求模板,然后设定替换字典
NSArray *results =
[aManagedObjectContext executeFetchRequest: fetchRequest error: &error];
要是模板里不包含可替换的变量,你要么
1. 使用fetchRequestFromTemplateWithName: substitutionVariables: 方法,传递nil给第二个参数
或者:
2. 使用fetchRequestTemplateForName: 并将结果copy。这个方法不需要传递“替换变量”这个参数,但是如果你要用返回值本身,将会有异常抛出(无法在不可变的模型中修改命名的数据请 求"Can't modify named fetch request in an immutable model")。
本地化被管理对象模型
你可以对模型的大部分内容做本地化处理,包括实体和属性名,还有错误信息。要明白,“转成你自己的语言”也是本地化的一部分。 即使你不打算提供外语版本, 显示“自然语言”的出错提示信息也会有更好的用户体验。例如:“First Name is a required property”就比"firstName is a required property"更好。(后面的这个更像是开发者用的log,显示的是变量名,这里不太明显)。
要想对模型进行本地化处理,需要提供一个本地化字典,模式如下:
Table 1 针对被管理对象模型的本地化字典键值对应关系:
- Key Value Note
- "Entity/NonLocalizedEntityName" "LocalizedEntityName"
- "Property/NonLocalizedPropertyName/Entity/EntityName" "LocalizedPropertyName" 1
- "Property/NonLocalizedPropertyName" "LocalizedPropertyName"
- "ErrorString/NonLocalizedErrorString" "LocalizedErrorString"
备注:(1)在不同实体中的属性,拥有相同的原始名称,但需要不同的本地化名称,适用于该格式。
我们可以通过localizationDictionary方法来访问本地化字典。注意:在Mac OS X 10.4上,这个方法可能返回nil,除了Core Data为了某些特定目的(如报告本地化的错误描述)延迟加载本地化字典。
字符串文件
处理模型的本地化最简单的方法就是创建对应的字符串文件——字符串文件名和模型文件名一直,但是后缀名用.strings。(例如,模型文件名为 MyDocument.xcdatamodel,对应的字符串文件名就为MyDocumentModel.strings;如果模型文件已经包含了 Model后缀,你必须再附加一个Model,所以,如果模型文件名为JimsModel.xcdatamodel对应的字符串文件名为 JimsModelModel.strings)。字符串文件格式和标准字符串文件类似(请参考"Localizing String Resources"),但是对应的键值要遵循Table-1中的规则。
一个模型的字符串文件实例:
- "Entity/Emp" = "Employee";
- "Property/firstName" = "First Name";
- "Property/lastName" = "Last Name";
- "Property/salary" = "Salary";
更详细的示例请参考"NSPersistentDocument Core Data Tutorial"。
代码实现设置本地化字典
你可以在运行时设定本地化字典,适用NSManagedObjectModel的setLocalizationDictionary:方法即可。你必须 创建一个符合Table-1格式的字典,并把它和模型关联。必须保证在模型被使用(获取或创建被管理对象)之前做这些工作,因为再使用后模型就不可编辑 了。 Listing 3演示了创建包含本地化字典的被管理对象模型。实体名称叫“Run”,它有两个属性: "date"和"processID",分别是date和integer类型。process ID的值不能为负。
Listing 3 通过代码创建被管理对象模型
- NSManagedObjectModel *mom = [[NSManagedObjectModel alloc] init];
- NSEntityDescription *runEntity = [[NSEntityDescription alloc] init];
- [runEntity setName:@"Run"];
- [runEntity setManagedObjectClassName:@"Run"];
- [mom setEntities:[NSArray arrayWithObject:runEntity]];
- [runEntity release];
- NSMutableArray *runProperties = [NSMutableArray array];
- NSAttributeDescription *dateAttribute = [[NSAttributeDescription alloc] init];
- [runProperties addObject:dateAttribute];
- [dateAttribute release];
- [dateAttribute setName:@"date"];
- [dateAttribute setAttributeType:NSDateAttributeType];
- [dateAttribute setOptional:NO];
- NSAttributeDescription *idAttribute= [[NSAttributeDescription alloc] init];
- [runProperties addObject:idAttribute];
- [idAttribute release];
- [idAttribute setName:@"processID"];
- [idAttribute setAttributeType:NSInteger32AttributeType];
- [idAttribute setOptional:NO];
- [idAttribute setDefaultValue:[NSNumber numberWithInt:0]];
- NSPredicate *validationPredicate = [NSPredicate predicateWithFormat:@"SELF >= 0"];
- NSString *validationWarning = @"Process ID < 0";
- [idAttribute setValidationPredicates:[NSArray arrayWithObject:validationPredicate]
- withValidationWarnings:[NSArray arrayWithObject:validationWarning]];
- [runEntity setProperties:runProperties];
- NSMutableDictionary *localizationDictionary = [NSMutableDictionary dictionary];
- [localizationDictionary setObject:@"Process ID"
- forKey:@"Property/processID/Entity/Run"];
- [localizationDictionary setObject:@"Date"
- forKey:@"Property/date/Entity/Run"];
- [localizationDictionary setObject:@"Process ID must not be less than 0"
- forKey:@"ErrorString/Process ID < 0"];
- [mom setLocalizationDictionary:localizationDictionary];
这段代码写的比较多,这里不再解释了。本地化字典的代码在最后。创建一个符合格式的localizationDictionary,然后用model调用即可。
-----------------------------------------------------------------------------------
iOS -表视图控制器与CoreData
在接触到CoreData时,感觉就是苹果封装的一个ORM。CoreData负责在Model的实体和sqllite建立关联,数据模型的实体类就相当于Java中的JavaBean, 而CoreData的功能和JavaEE中的Hibernate的功能类似,最基本是两者都有通过对实体的操作来实现对数据库的CURD操作。CoreData中的上下文(managedObjectContext)就相当于Hibernate中的session对象, CoreData中的save操作就和Hibernate中的commit,还有一些相似之处,在这就不一一列举了。(上面是笔者自己为了更好的理解CoreData而做的简单类比,如果学过PHP的ThinkPHP框架的小伙伴们也可以和TP中的ORM类比)。
那么TableView为什么会爱上CoreData呢?下面会通个代码给出他们相爱的原因。就举一个IOS开发中的经典的demo:通讯录来说明问题。
1.在TableView没遇到CoreData的时候我们怎么通过动态表视图来显示我们的通讯录的内容呢?也就是说我们通讯录的数据结构该如何组织呢?
为了在TableView中显示我们的信息我们这样设计我们的数据结构:
1.整个TableView是一个可变的数组tableArray;
2.tableArray中的每个元素又是一个存放分组的字典sectionDictionary;
3.在sectionDictionary中我们存放着两个键值对 header和items, header中存放的时section中的名字,items中存放的时每个section中的用户信息
4.items中又是一个数组rowsArray, rowsArray中存放的又是一个字典userInfoDictionary, 在userInfoDictionary中存放着我们要显示的信息
千字不如一图,看到上面对我们要设计的数据结构的描述会有点迷糊,下面来张图吧:
2.数据结构我们设计好了,那么如何用代码生成我们的测试数据(数据的组织形式如上图所示),下面的代码就是生成我们要在tableView中显示的数据,生成的数组存储在tableArray中,代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
|
/* *手动创建我们在动态表视图上显示的数据格式 *整个数据存储在一个数组中 *数组中每一个元素是一个自动,字典的key是sectionHeader的值,value是该section中以数组形式存的数据 *section中的每一行对应着一个数组元素,数组元素中又存储着一个字典,字典中存储着用户的具体数据。 */ //为我们的数组分配存储空间, 代表着有20个section self.telBook = [NSMutableArray arrayWithCapacity:26]; //为我们的section设置不同的header char header = 'A' ; //计数 static int number = 0; for ( int i = 0; i < 26; i ++) { //新建字典来存储我们每个section中的数据, 假设每个section中有1个数组 NSMutableDictionary *sectionDic = [NSMutableDictionary dictionaryWithCapacity:1]; //创建字典中的数组,数组中以键值对的形式来储存用户的信息 NSMutableArray *rowArray = [NSMutableArray arrayWithCapacity:3]; for ( int j = 0; j < 3; j ++) { //创建存储用户信息的字典 NSMutableDictionary *user = [NSMutableDictionary dictionaryWithCapacity:2]; //生成测试数据 NSString *name = [NSString stringWithFormat:@ "User%03d" , number]; NSString *tel = [NSString stringWithFormat:@ "12345%03d" , number++]; //加入字典中 [user setObject:name forKey:@ "name" ]; [user setObject:tel forKey:@ "tel" ]; //把字典加入数组 [rowArray addObject:user]; } //把rowArray添加到section字典中 NSString *key = [NSString stringWithFormat:@ "%c" ,(header+i)]; [sectionDic setObject:key forKey:@ "header" ]; [sectionDic setObject:rowArray forKey:@ "items" ]; //把section添加到总的数组中 [self.telBook addObject:sectionDic]; } |
3.把我们用代码创建的模拟数据在我们的TableView中进行显示,在相应的函数中根据我们生成的数据返回相应的值显示在TableView中,显示代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
|
#pragma mark - Table view data source //返回Section的个数,即我们telBook数组元素的个数 - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return self.telBook.count; } //返回每个section中的行数,即section中的数组元素的个数 - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { NSArray *rowArray = self.telBook[section][@ "items" ]; return rowArray.count; } //给每个分组设置header -(NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section { //获取每个section中的header NSString *title = self.telBook[section][@ "header" ]; return title; } //获取cell并添加完数据发挥 - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@ "Cell" forIndexPath:indexPath]; //获取secion中的数据数组 NSArray *items = self.telBook[indexPath.section][@ "items" ]; //获取数组中的每一项的一个字典 NSString *name = items[indexPath.row][@ "name" ]; NSString *tel = items[indexPath.row][@ "tel" ]; //给sel设置值 cell.textLabel.text = name; cell.detailTextLabel.text = tel; return cell; } |
4.上面给出的时关键代码,至于怎么配置TableView的Cell模板或者如何把TableViewController和Storyboard中的ViewController绑定,在前面的博客中都有介绍,在这小编就不做赘述。运行结果和上面的图片是一样的。
上面的东西只是这篇博文的引子,为了显示上面的数据结构我们这样做是不是太麻烦了,而且上面的数据是不能被持久化存储的。如果给我们的数据都要转换成上面的数据组织形式,想必由于所给数据结构的不确定,所以转换起来是相当的复杂的。TableView之所以会爱上CoreData,是因为我们的CoreData会简化我们对数据的操作,并且会持久化到sqlite中。CoreData相当于TableView和sqllite的纽带,说的专业一些就是映射,那么我们CoreData如何使用才会简化我们的操作呢?下面将要介绍的才是这篇博客中的重点:我们如何使用CoreData才会让TableView爱上它呢?
1.新建一个Empty Application, 在新建工程的时候,不要忘了把Use Core Data给选中,选中Use Core Data会自动引入Core Data框架库和在AppDelegate.h和AppDelegate.m中进行相应的配置,并且同时还自动生成一个以本应用名命名的Data Model文件,我们可以在Data Model文件中添加我们的数据模型, 添加好的数据模型我们会在生成数据实体类时使用(和JavaBean类似)
(1)AppDelegata.m中多出的部分代码如下,从多出的部分代码就可以看出,CoreData会把我们的数据实体和sqllite建立起一一对应的关系:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
// Returns the managed object model for the application. // If the model doesn't already exist, it is created from the application's model. - (NSManagedObjectModel *)managedObjectModel { if (_managedObjectModel != nil) { return _managedObjectModel; } NSURL *modelURL = [[NSBundle mainBundle] URLForResource:@ "Demo083101" withExtension:@ "momd" ]; _managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL]; return _managedObjectModel; } // Returns the persistent store coordinator for the application. // If the coordinator doesn't already exist, it is created and the application's store added to it. - (NSPersistentStoreCoordinator *)persistentStoreCoordinator { if (_persistentStoreCoordinator != nil) { return _persistentStoreCoordinator; } NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:@ "Demo083101.sqlite" ]; NSError *error = nil; _persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]]; if (![_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:&error]) { NSLog(@ "Unresolved error %@, %@" , error, [error userInfo]); abort (); } return _persistentStoreCoordinator; } |
(2)我们可以通过 projectName.xcdatamodeld中创建我们的数据实体模型,如下图所示
(3)通过创建好的数据实体模型来创建我们的实体类(和JavaBean类似的东西)创建过程如下图,点击下一步以后,选中创建的实体模型即可:
2.CoreData准备的差不多啦,该我们的TableView出场啦,在Empty Application中默认的时没有storyboard, 如果你又想通过storyboard来简化你的操作,得给应用创建一个storybaord才对,创建过程如下:
(1)第一步创建一个storyboard文件,命名为Main,如下图所示
(2)第二步:设置从storyboard来启动, 在Main InterFace中选中我们创建的storyboard即可
(3) 第三步修改AppDelegate.m中的函数如下所示,把初始化的工作交给我们创建的storyboard进行:
1
2
3
4
|
- ( BOOL )application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { return YES; } |
3.配置工作完成接下来就是TableView和CoreData相爱的过程啦,如何在storyboard中对TableView的cell进行配置在这儿就不赘述了,下面给出我们要通过TableView和CoreData来实现什么功能。
(1)我们要实现对通讯录的增删改查,主要需求入下图所示:
(2)实现添加功能,点击右上角的添加按钮时会跳转到添加页面,在添加页面中有两个TextField来接受用户的输入,点击添加按钮进行数据添加。AddViewController.m中的主要代码如下。
a.需要用到的属性如下, 用NSManagedObejectContext的对象来操作CoreData中的数据,和Hibernate中的session的对象相似
1
2
3
4
5
|
@property (strong, nonatomic) IBOutlet UITextField *nameTextField; @property (strong, nonatomic) IBOutlet UITextField *numberTextField; //声明CoreData的上下文 @property (strong, nonatomic) NSManagedObjectContext *managedObjectContext; |
b.获取UIApplication的单例application, 然后再通过application获取delegate, 最后通过delegate来获取上下文,代码如下:
1
2
3
4
|
//通过application对象的代理对象获取上下文 UIApplication *application = [UIApplication sharedApplication]; id delegate = application.delegate; self.managedObjectContext = [delegate managedObjectContext]; |
c.编辑点击button要回调的方法,在点击添加按钮时首先得通过上下文获取我们的实体对象,获取完实体对象后再给实体对象的属性赋上相应的值,最后调用上下文的save方法来存储一下我们的实体对象。添加完以后还要通过navigationController来返回到上一层视图,代码如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
- (IBAction)tapAdd:(id)sender { //获取Person的实体对象 Person *person = [NSEntityDescription insertNewObjectForEntityForName:NSStringFromClass([Person class ]) inManagedObjectContext:self.managedObjectContext]; //给person赋值 person.name = self.nameTextField.text; person.number = self.numberTextField.text; person.firstN = [NSString stringWithFormat:@ "%c" , pinyinFirstLetter([person.name characterAtIndex:0])-32]; //通过上下文存储实体对象 NSError *error; if (![self.managedObjectContext save:&error]) { NSLog(@ "%@" , [error localizedDescription]); } //返回上一层的view [self.navigationController popToRootViewControllerAnimated:YES]; } |
(3)实现上面的代码只是通过CoreData往sqlite中添加数据,要想在我们的TableView中显示还需要通过CoreData把我们的存储在sqlite中的数据来查询出来,再用CoreData给我们提供的方法把查询结果做一个转换,转换成适合TableView显示的数据,下面给出相应的获取数据的代码。
a.在TableViewController我们需要声明如下两个属性,一个用于获取上下文,一个用于存储返回结果
1
2
3
4
|
//声明通过CoreData读取数据要用到的变量 @property (strong, nonatomic) NSManagedObjectContext *managedObjectContext; //用来存储查询并适合TableView来显示的数据 @property (strong, nonatomic) NSFetchedResultsController *fetchedResultsController; |
b.在viewDidLoad中获取上下文
1
2
3
4
|
//通过application对象的代理对象获取上下文 UIApplication *application = [UIApplication sharedApplication]; id delegate = application.delegate; self.managedObjectContext = [delegate managedObjectContext]; |
c.在viewDidLoad中通过上下文来查询数据,并存储在fetchedResultsController中, 在获取数据的过程中我们需要定义UIFetchRequest 和排序规则,代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
/********* 通过CoreData获取sqlite中的数据 *********/ //通过实体名获取请求 NSFetchRequest *request = [[NSFetchRequest alloc] initWithEntityName:NSStringFromClass([Person class ])]; //定义分组和排序规则 NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@ "firstN" ascending:YES]; //把排序和分组规则添加到请求中 [request setSortDescriptors:@[sortDescriptor]]; //把请求的结果转换成适合tableView显示的数据 self.fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:request managedObjectContext:self.managedObjectContext sectionNameKeyPath:@ "firstN" cacheName:nil]; //执行fetchedResultsController NSError *error; if ([self.fetchedResultsController performFetch:&error]) { NSLog(@ "%@" , [error localizedDescription]); } |
d.把查询到的数据显示在TableView中代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
|
#pragma mark - Table view data source - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { //我们的数据中有多少个section, fetchedResultsController中的sections方法可以以数组的形式返回所有的section //sections数组中存的是每个section的数据信息 NSArray *sections = [self.fetchedResultsController sections]; return sections.count; } //通过获取section中的信息来获取header和每个secion中有多少数据 -(NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section { NSArray *sections = [self.fetchedResultsController sections]; //获取对应section的sectionInfo id<NSFetchedResultsSectionInfo> sectionInfo = sections[section]; //返回header return [sectionInfo name]; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { NSArray *sections = [self.fetchedResultsController sections]; id<NSFetchedResultsSectionInfo> sectionInfo = sections[section]; //返回每个section中的元素个数 return [sectionInfo numberOfObjects]; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@ "Cell" forIndexPath:indexPath]; //获取实体对象 Person *person = [self.fetchedResultsController objectAtIndexPath:indexPath]; cell.textLabel.text = person.name; cell.detailTextLabel.text = person.number; // Configure the cell... return cell; }
|
(4) 经上面的代码,我们就可以通过CoreData查询sqlite, 然后把查询测数据结果显示到TableView中,可是上面的代码有个问题,就是当通过CoreData来修改或着添加数据时,TableView上的内容是不跟着CoreData的变化而变化的,接下来要做的就是要绑定TableView和CoreData的关系。即通过CoreData修改数据的同时TableView也会跟着改变。
a.要想实现TableView和CoreData的同步,我们需要让TableView对应的Controller实现协议NSFetchedResultsControllerDelegate, 然后再ViewDidLoad中进行注册,在添加上相应的回调代码即可。实现协议的代码如下:
1
2
3
4
5
|
#import <UIKit/UIKit.h> @interface MyTableViewController : UITableViewController<NSFetchedResultsControllerDelegate> @end |
b.进行委托回调的注册,在viewDidLoad中添加
1
2
|
//注册回调,使同步生效 self.fetchedResultsController.delegate = self; |
c.添加相应的委托回调的方法,我们可以到Help中的API中去复制, 查询NSFetchedResultsControllerDelegate,找到相应的回调代码复制过来然后再做简单的修改即可, 实现回调的方法代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
|
/* Assume self has a property 'tableView' -- as is the case for an instance of a UITableViewController subclass -- and a method configureCell:atIndexPath: which updates the contents of a given cell with information from a managed object at the given index path in the fetched results controller. */ //当CoreData的数据正在发生改变是,FRC产生的回调 - ( void )controllerWillChangeContent:(NSFetchedResultsController *)controller { [self.tableView beginUpdates]; } //分区改变状况 - ( void )controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type { switch (type) { case NSFetchedResultsChangeInsert: [self.tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade]; break ; case NSFetchedResultsChangeDelete: [self.tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade]; break ; } } //数据改变状况 - ( void )controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath { UITableView *tableView = self.tableView; switch (type) { case NSFetchedResultsChangeInsert: //让tableView在newIndexPath位置插入一个cell [tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade]; break ; case NSFetchedResultsChangeDelete: [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade]; break ; case NSFetchedResultsChangeUpdate: //让tableView刷新indexPath位置上的cell [tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade]; break ; case NSFetchedResultsChangeMove: [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade]; [tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade]; break ; } } //当CoreData的数据完成改变是,FRC产生的回调 - ( void )controllerDidChangeContent:(NSFetchedResultsController *)controller { [self.tableView endUpdates]; } |
(5)经过上面的代码就可以实现CoreData和TableView的同步啦,到此会感觉到TableView结合着CoreData是如此的顺手,虽然配置起来较为麻烦,但还是比较中规中矩的,只要按部就班的来,是不难实现的。因此TableView深爱着CoreData. 上面我们完成了通过CoreData来对数据的插入和查询并同步到TableView中,下面将会介绍到如何对我们的Cell进行删除。
a.想通过TableView来删除数据的话得开启我们的TableView的编辑功能
1
2
3
4
5
6
7
|
//开启编辑 // Override to support conditional editing of the table view. - ( BOOL )tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath { // Return NO if you do not want the specified item to be editable. return YES; } |
b.开启编辑功能以后我们就可以在tableView的对应的方法中来实现删除功能啦,当点击删除时,我们需呀获取cell对应的索引在CoreData中的实体对象,然后通过上下文进行删除,在save一下即可。因为CoreData和TableView已经进行了同步,所以删除后TableView会自动更新,删除代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
// Override to support editing the table view. - ( void )tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath { if (editingStyle == UITableViewCellEditingStyleDelete) { //通过coreData删除对象 //通过indexPath获取我们要删除的实体 Person * person = [self.fetchedResultsController objectAtIndexPath:indexPath]; //通过上下文移除实体 [self.managedObjectContext deleteObject:person]; //保存 NSError *error; if ([self.managedObjectContext save:&error]) { NSLog(@ "%@" , [error localizedDescription]); } } } |
c.默认的删除按钮上显示的是Delete, 可以通过下面的方法进行修改,代码如下:
1
2
3
4
5
6
|
//设置删除的名字 -(NSString *)tableView:(UITableView *)tableView titleForDeleteConfirmationButtonForRowAtIndexPath:(NSIndexPath *)indexPath { return @ "删除" ; } |
(6)到这一步删除功能算是完成了,还有最后一个功能点,就是更新我们的数据。更新数据通过点击相应的cell,把cell上的数据传到UpdateView的页面上,然后进行更新即可。
a.下面的代码是获取数据我们选中的数据并通过KVC把参数传到目的视图中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
#pragma mark - Navigation //把对应的cell上的值传到修改的页面上 // In a storyboard-based application, you will often want to do a little preparation before navigation - ( void )prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { //参数sender是点击的对应的cell //判断sender是否为TableViewCell的对象 if ([sender isKindOfClass:[UITableViewCell class ]]) { //做一个类型的转换 UITableViewCell *cell = (UITableViewCell *)sender; //通过tableView获取cell对应的索引,然后通过索引获取实体对象 NSIndexPath *indexPath = [self.tableView indexPathForCell:cell]; //用frc通过indexPath来获取Person Person *person = [self.fetchedResultsController objectAtIndexPath:indexPath]; //通过segue来获取我们目的视图控制器 UIViewController *nextView = [segue destinationViewController]; //通过KVC把参数传入目的控制器 [nextView setValue:person forKey:@ "person" ]; } } |
b.在UpdateViewController中把传过来的实体对象进行更新,再保存。更新部分的代码和添加部分的代码差不多,在这就不往上贴啦。
经过上面的艰苦的历程后我们的tableView就会深深的爱上CoreData, 可能上面的内容有些多,有疑问的可以留言交流。
上面所做的功能里我们的真正的通讯录还有些差距,看过上面的代码的小伙伴会有个疑问:添加的页面和更新的页面能不能使用同一个呢? 当然啦,为了遵循Don`t Repeat Yourself的原则,下面我们就把两个相似的页面合并在一起,同时给我们每条记录加上头像和给整个tableView加上索引。
1.把更新页面删掉,做如下修改,点击添加和修改都跳转到我们的编辑页面,同时添加一个自定义Button,点击Button时,我们会调用ImagePickerController来从手机相册获取图片:
2.为了把头像持久化存储,我们还得修改数据模型,从新生成Person类,添加一个存储image的选项,是通过二进制的形式存储的
3.在之前保存的ViewController中如果Person为空,说明是执行的添加记录的方法我们就生成一个新的person, 如果Person不为空则不新建Person对象,直接更新完保存。
(1)为了获取图片,我们需要添加ImagePickerController对象,并在viewDidLoad中做相应的配置,代码如下
1
2
|
//声明ImagePicker @property (strong, nonatomic) UIImagePickerController *picker; |
进行相关配置
1
2
3
4
5
6
|
//初始化并配置ImagePicker self.picker = [[UIImagePickerController alloc] init]; //picker是否可以编辑 self.picker.allowsEditing = YES; //注册回调 self.picker.delegate = self; |
(2)点头像会跳转到我们定义好的ImagePickerController中,我们就可在图片库中选取相应的照片啦。
1
2
3
4
5
6
7
|
//点击图片按钮设置图片 - (IBAction)tapImageButton:(id)sender { //跳转到ImagePickerView来获取按钮 [self presentViewController:self.picker animated:YES completion:^{}]; } |
(3)在ImagePickerController中点击取消按钮触发的事件,跳转到原来编辑的界面
1
2
3
4
5
6
|
//回调图片选择取消 -( void )imagePickerControllerDidCancel:(UIImagePickerController *)picker { //在ImagePickerView中点击取消时回到原来的界面 [self dismissViewControllerAnimated:YES completion:^{}]; } |
(4)选完图片把头像设置成用户选中的按钮,并dismiss到原来界面
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
//实现图片回调方法,从相册获取图片 -( void ) imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info { //获取到编辑好的图片 UIImage * image = info[UIImagePickerControllerEditedImage]; //把获取的图片设置成用户的头像 [self.imageButton setImage:image forState:UIControlStateNormal]; //返回到原来View [self dismissViewControllerAnimated:YES completion:^{}]; } |
(5)把我们点击保存按钮回调的方法作如下修改,如果person为空,我们会新建一个新的person.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
- (IBAction)tapSave:(id)sender { //如果person为空则新建,如果已经存在则更新 if (self.person == nil) { self.person = [NSEntityDescription insertNewObjectForEntityForName:NSStringFromClass([Person class ]) inManagedObjectContext:self.managedObjectContext]; } //赋值 self.person.name = self.nameTextField.text; self.person.tel = self.telTextField.text; self.person.firstN = [NSString stringWithFormat:@ "%c" , pinyinFirstLetter([self.person.name characterAtIndex:0])-32]; //把button上的图片存入对象 UIImage *buttonImage = [self.imageButton imageView].image; self.person.imageData = UIImagePNGRepresentation(buttonImage); //保存 NSError *error; if (![self.managedObjectContext save:&error]) { NSLog(@ "%@" , [error localizedDescription]); } //保存成功后POP到表视图 [self.navigationController popToRootViewControllerAnimated:YES]; } |
(6)因为是何更新页面公用的所以我们要在viewDidLoad对TextField和Button的背景进行初始化,如果person中的imageData有值我们有用传过来的图片,否则用默认的图片,添加数据初始化代码如下:
1
2
3
4
5
6
7
8
9
|
self.nameTextField.text = self.person.name; self.telTextField.text = self.person.tel; if (self.person.imageData != nil) { UIImage *image = [UIImage imageWithData:self.person.imageData]; [self.imageButton setImage:image forState:UIControlStateNormal]; } |
4.上面的代码就可以插入头像了,我们需要在tableView中进行显示即可,在tableView中从person对象中获取相应的头像,然后显示即可,下面我们要加上索引。
(1)在cell中显示头像的代码如下:
1
2
3
4
|
if (person.imageData != nil) { UIImage *image = [UIImage imageWithData:person.imageData]; cell.imageView.image = image; } |
(2)实现添加索引回调的方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
//给我们的通讯录加上索引,下面的方法返回的时一个数组 -(NSArray *) sectionIndexTitlesForTableView:(UITableView *)tableView { //通过fetchedResultsController来获取section数组 NSArray *sectionArray = [self.fetchedResultsController sections]; //新建可变数组来返回索引数组,大小为sectionArray中元素的多少 NSMutableArray *index = [NSMutableArray arrayWithCapacity:sectionArray.count]; //通过循环获取每个section的header,存入addObject中 for ( int i = 0; i < sectionArray.count; i ++) { id <NSFetchedResultsSectionInfo> info = sectionArray[i]; [index addObject:[info name]]; } //返回索引数组 return index; } |
经过上面的步骤,我们之前俩个页面可以共用,而且加上了头像和索引,运行效果如下:
上面的内容挺多的啦吧,别着急,我们的这个通讯录还没完呢,通讯录中的查询功能是少不了的,因为当存的用户多了,为了方便用户查询我们还需要添加一个控件。接下来是我们Search Bar and Search 出场的时候了。UISearchDisplayController自己有一个TableView用于显示查询出来的结果,需要在通讯录中添加一些代码我们的Seach Bar就可以使用了。
1.在storyboard中添加Search Bar and Search,然后把属性拖入我们对应的TableViewController中即可,新添加属性如下:
//添加Search Display Controller属性 @property (strong, nonatomic) IBOutlet UISearchDisplayController *displayC;
2.编辑SearchBar内容改变后调用的方法,我们会通过用户输入的内容进行一个模糊查询,把查询的内容添加到我们之前的fetchResultController中
1 //当search中的文本变化时就执行下面的方法 2 - (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText 3 { 4 //新建查询语句 5 NSFetchRequest * request = [[NSFetchRequest alloc]initWithEntityName:NSStringFromClass([Person class])]; 6 7 //排序规则 8 NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"firstN" ascending:YES]; 9 [request setSortDescriptors:@[sortDescriptor]]; 10 11 //添加谓词 12 NSPredicate * predicate = [NSPredicate predicateWithFormat:@"name contains %@",searchText]; 13 [request setPredicate:predicate]; 14 15 //把查询结果存入fetchedResultsController中 16 self.fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:request managedObjectContext:self.managedObjectContext sectionNameKeyPath:@"firstN" cacheName:nil]; 17 18 NSError *error; 19 if (![self.fetchedResultsController performFetch:&error]) { 20 NSLog(@"%@", [error localizedDescription]); 21 } 22 }
3.因为UISearchDisplayController里的TableView和我们之前的tableView用的是一个FetchedReaultsController,所以在UISearchDisplayController取消的时候要重载一下我们之前的TableView,或去通讯录中的FetchedResultsController, 代码如下:
//当在searchView中点击取消按钮时我们重新刷新一下通讯录 -(void)searchBarCancelButtonClicked:(UISearchBar *)searchBar { [self viewDidLoad]; }
4.因为通过search查询的结果集会显示在UISearchDisplayController自己的tableView中,所以加载cell时要进行相应的选择,search中的cell是我们自定义的cell, 选择代码如下:
1 //根据不同的tableView来设置不同的cell模板 2 if ([tableView isEqual:self.tableView]) 3 { 4 5 cell = [tableView dequeueReusableCellWithIdentifier:@"Cell" forIndexPath:indexPath]; 6 7 } 8 else 9 { 10 cell = [tableView dequeueReusableCellWithIdentifier:@"SearchCell" forIndexPath:indexPath]; 11 12 }
5.在我们的查询后的列表中,如果还想点击cell以后跳转到编辑页面,我们该如何做呢? 添加下面的回调方法,用代码进行跳转,代码如下:
1 -(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath 2 { 3 if ([tableView isEqual:self.displayC.searchResultsTableView]) 4 { 5 Person *person = [self.fetchedResultsController objectAtIndexPath:indexPath]; 6 UIStoryboard * s = [UIStoryboard storyboardWithName:@"Main" bundle:[NSBundle mainBundle]]; 7 8 //获取要目标视图 9 UIViewController *destination = [s instantiateViewControllerWithIdentifier:@"EditViewController"]; 10 11 //键值编码传值 12 [destination setValue:person forKeyPath:@"person"]; 13 14 [self.navigationController pushViewController:destination animated:YES]; 15 } 16 }
经过上面的步骤,我们的查询功能就写好了,下面是最终的运行结果:
经上面这么曲折的过程,我们的通讯录的基本功能就差不多了