这是接着上一次《iOS教程:Core Data数据持久性存储基础教程》的后续教程,程序也会使用上一次制作完成的。
再上一个教程中,我们只做了一个数据模型,之后我们使用这个数据模型中的数据创建了一个表视图,我们还学习了如何测试数据模型的可行性,今天,我们来看看如何在应用启动的时候,将已经存在的数据载入或者引用到我们的程序中去。
请注意我们在上一次的教程中学习到的是直接通过操作SQLite数据库来加载数据,你当然可以一直使用这种方法,但是这个教程教授的方法更加优雅,更加合理。
在下一部分的教程中,我们将会讨论如何使用NSFetchedResultsController来优化我们的应用的访问数据的方式。
至于如果你没有上一次做好的程序的话,你可以从这里下载。
预加载/引入数据
那么我们究竟怎样把数据存储进Core Data数据库呢?目前有两种比较好的选择。
- 在App启动的时候从外部文件引入数据,就是在程序开始运行的时候从外部的资源,比如SQLite数据库或者XML文件中,引入数据。
- 提供一个已经制作完成的SQLite数据库,首先制作一个像上次的教程说的那样的数据库模型,之后在这个模型中填充数据,填充数据的方式是使用一个utility app,这个utility app可以是一个使用Core Data API填充数据库的Mac或者iOS app,也可以是一些直接填充数据库的程序。一旦数据库被填充之后,你就可以在没有已存在的数据库的情况下设置这个数据库未使用的默认数据库。
在这个教程中,我们会通过第二种,为大家展示如何使用一个简单的utility app来预加载一个已经装在好的Core Data数据库,以便让你的app使用。
第一步
我们在iOS上使用Core Data的方法的基础和我们在Mac OS X上使用的是一致的,他们使用同样的模型和类。
这一为我们可以写一个MAC OS X上的简单的console程序,来从数据源引入数据,再把这个数据库的数据库拿来给我们的iOS程序来用,不错吧?
我们来试试,首先打开Xcode,在 Mac OSX类中的Application中使用Command Line Tool 的模板。
我们就用 “CoreDataTutorial2” 作为工程的名字吧,记得使用“Core Data” 和 “Use Automatic Reference Counting” 。
完成创建之后,选择 “CoreDataTutorial2.xcdatamodeld” 彻底删除之。
之后找到我们上次完成的哪些文件中的
- FailedBankCD.xcdatamodeld
- FailedBankInfo.h
- FailedBankInfo.m
- FailedBankDetails.h
- FailedBankDetails.m
将这些文件复制,或者直接拖到我们的新项目中:
确保“Copy items into destination group’s folder (if needed)” 没有选中
并且选中“Add to targets” 。
选择 main.m,你会注意到由于我们选择了使用Core Data,所以这里为我们准备了一些模板的方法,现在,我们来修改这些方法来让他们为我们的iOS程序生成数据数。
将 managedObjectModel()
方法从
NSString *path = [[[NSProcessInfo processInfo] arguments] objectAtIndex:0]; path = [path stringByDeletingPathExtension]; |
替换为
NSString *path = @"FailedBankCD"; |
这会把这个程序指向 FailedBankCD.xdatamodeld
而不是我们已经删除的CoreDataTutorial2.xdatamodeld
。
按下command+r进行编译和运行,应该看到没有错误。
但是如果你在这一步之前进行过编译的话,你到这时就会出现数据不符的错误,按照我们上次的教程所说的那样,删除之后重新编译运行就行。
如果你看到了下面的错误:
NSInvalidArgumentException‘, reason: ‘Cannot create an NSPersistentStoreCoordinator with a nil model‘
这是因为程序再找一个 ‘momd’ 文件, (上一个版本的Core Data模型),但是如果你的app使用的不是这个文件的话,那就会出这个错误,最快的修正方法就是把managedObjectModel()这个方法修改为下面的:
NSURL *modelURL = [NSURL fileURLWithPath:[path stringByAppendingPathExtension:@"mom"]]; |
现在应该就没问题了
引入数据
现在到了动真家伙的时候了,真的把我们的数据加载进去。
在我们这个例子里,我们要从一个JSON文件中引入数据,也许你会想从其他类型的文件中引入数据,不过原理都是一样的。
下面,我们新建一个文件iOS – Other – Empty
把这个文件命名为Banks.json。
将下面的代码输进去:
[{ "name": "Bank1", "city": "City1", "state": "State1", "zip": 11111, "closeDate": "1/1/11" }, { "name": "Bank2", "city": "City2", "state": "State2", "zip": 22222, "closeDate": "2/2/12" }, { "name": "Bank3", "city": "City3", "state": "State3", "zip": 33333, "closeDate": "3/3/13" }, { "name": "Bank4", "city": "City4", "state": "State4", "zip": 44444, "closeDate": "4/4/14" } ] |
这是一个一个数组中包含四个字典的JSON文件,每一个字典都有几个与FailedBankInfo/FailedBankDetails中的物体相对应的属性。
如果你不是很清楚JSON文件是如何组织数据的,你可以看一下这个教程: this tutorial.
接下来,我们告诉我们的应用当编译的时候将这个文件我们的产品目录,看图做,首先选择Project,之后选择CoreDataTutorial2目标,选择Build Phase选项卡,按下“Add Build Phase”,选择“Add Copy File”,选择目标位置为“Products Directory”,最后,把“Banks。json拖到Add Files的部分。
当一个应用启动时,要先使用FailedBank的数据模型和类初始化一个Core Data的数据库,之后用Banks.json文件中的数据来输进去。
现在,我们要:
- 载入 JSON 文件
- 解析 JSON 文件为一个 Objective C 数组
- 枚举这个数组中的数据,为每一个物体创建一个managed item。
- 将他们全都存入 Core Data
编程开始,首先打开 main.m 把下面的代码加入主函数:
NSError* err = nil; NSString* dataPath = [[NSBundle mainBundle] pathForResource:@"Banks" ofType:@"json"]; NSArray* Banks = [NSJSONSerialization JSONObjectWithData:[NSData dataWithContentsOfFile:dataPath] options:kNilOptions error:&err]; NSLog(@"Imported Banks: %@", Banks); |
之后你的主函数看起来应该是下面这个样子的:
int main(int argc, const char * argv[]) { @autoreleasepool { // Create the managed object context NSManagedObjectContext *context = managedObjectContext(); // Custom code here... // Save the managed object context NSError *error = nil; if (![context save:&error]) { NSLog(@"Error while saving %@", ([error localizedDescription] != nil) ? [error localizedDescription] : @"Unknown Error"); exit(1); } NSError* err = nil; NSString* dataPath = [[NSBundle mainBundle] pathForResource:@"Banks" ofType:@"json"]; NSArray* Banks = [NSJSONSerialization JSONObjectWithData:[NSData dataWithContentsOfFile:dataPath] options:kNilOptions error:&err]; NSLog(@"Imported Banks: %@", Banks); } return 0; } |
这是用了使用了内置的 NSJSONSerialization API 来简单地将JSON的文件数据导入Core Foundation的数据类型中区(如NSArray,NSDictionary等等),想了解更多的话,请看 this tutorial.
运行一下这个程序,你会看到下面的输出:
2012-04-14 22:01:34.995 CoreDataTutorial2[18388:403] Imported Banks: ( { city = City1; closeDate = "1/1/11"; name = Bank1; state = State1; zip = 11111; }, { city = City2; closeDate = "2/2/12"; name = Bank2; state = State2; zip = 22222; }, { city = City3; closeDate = "3/3/13"; name = Bank3; state = State3; zip = 33333; }, { city = City4; closeDate = "4/4/14"; name = Bank4; state = State4; zip = 44444; } ) |
现在我们已经能够把这些数据存储进了一个Objective – C的物体中,那么现在我们就可以像上次教程的末尾那样把这些数据输入进Core Data的数据库中。
首先在头部加上一下你需要的文件的引用语句:
#import "FailedBankInfo.h" #import "FailedBankDetails.h" |
之后把这些你之前加入主函数代码。
[Banks enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { FailedBankInfo *failedBankInfo = [NSEntityDescription insertNewObjectForEntityForName:@"FailedBankInfo" inManagedObjectContext:context]; failedBankInfo.name = [obj objectForKey:@"name"]; failedBankInfo.city = [obj objectForKey:@"city"]; failedBankInfo.state = [obj objectForKey:@"state"]; FailedBankDetails *failedBankDetails = [NSEntityDescription insertNewObjectForEntityForName:@"FailedBankDetails" inManagedObjectContext:context]; failedBankDetails.closeDate = [NSDate dateWithString:[obj objectForKey:@"closeDate"]]; failedBankDetails.updateDate = [NSDate date]; failedBankDetails.zip = [obj objectForKey:@"zip"]; failedBankDetails.info = failedBankInfo; failedBankInfo.details = failedBankDetails; NSError *error; if (![context save:&error]) { NSLog(@"Whoops, couldn‘t save: %@", [error localizedDescription]); } }]; // Test listing all FailedBankInfos from the store NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init]; NSEntityDescription *entity = [NSEntityDescription entityForName:@"FailedBankInfo" inManagedObjectContext:context]; [fetchRequest setEntity:entity]; NSArray *fetchedObjects = [context executeFetchRequest:fetchRequest error:&error]; for (FailedBankInfo *info in fetchedObjects) { NSLog(@"Name: %@", info.name); FailedBankDetails *details = info.details; NSLog(@"Zip: %@", details.zip); } |
这些代码本质上就是我们上一次使用的代码,除了我们这次使用了enumerateObjectsUsingBlock: 的方法老枚举这个数组的内容之后进行插入,之后我们使用一个Fetch命令来输出数据。
现在运行一下,你会看到输出了之前的数组。
2012-04-14 22:15:44.149 CoreDataTutorial2[18484:403] Name: Bank1 2012-04-14 22:15:44.150 CoreDataTutorial2[18484:403] Zip: 11111 2012-04-14 22:15:44.150 CoreDataTutorial2[18484:403] Name: Bank2 2012-04-14 22:15:44.151 CoreDataTutorial2[18484:403] Zip: 22222 2012-04-14 22:15:44.152 CoreDataTutorial2[18484:403] Name: Bank3 2012-04-14 22:15:44.152 CoreDataTutorial2[18484:403] Zip: 33333 2012-04-14 22:15:44.153 CoreDataTutorial2[18484:403] Name: Bank4 2012-04-14 22:15:44.153 CoreDataTutorial2[18484:403] Zip: 44444 |
Ok,这些就是你在Core Data中的数据了。除了这种简单的JSON文件之外,你也可以使用更加复杂的JSON文件,XML文件,甚至是普通的表格文件,只要你存成了csv的格式,也可以是来自互联网的pipe,可以使用的文件种类数也数不清,我们以后也会详细介绍。
Xcode犯病了?
下面我们做一个脑部移植手术,将我们使用Mac OS X上的命令行程序的数据库转移到iPhone app中去。最简单的找到数据库文件的方法就是右键(ctrl+) CoreDataTutorial2
产品揽之后按 “Show in Finder”。
这会打开一个新的Finder窗口,在这里面会有这些文件:
- Banks.json – 这是数据的原始文件,记得吗?
- CoreDataTutorial2 – 这个是应用本身。
- FailedBankCD.momd (或者 .mom) – 这是编译好的Core Data数据模型。
- CoreDataTutorial2.sqlite – 这就是我们在找的sqlite数据库文件,它是由程序生成的,Core Data应该可以通用的。你可以自己找一个SQLite数据库的查看软件,也可以下载 这个
确定 “CoreDataTutorial2.sqlite” 就是我么所需要的文件,下面我们把这个文件拷贝到我们上一个教程的源码工程文件之中,之后打开:
从Finder中拖拽 “CoreDataTutorial2.sqlite” 文件到Xcode的工程之中,确保 “Copy items into destination group’s folder (if needed)” 这个选项没有被选中,另一个是选中的。
最后,打开 “FBCDAppDelegate.m”,找到 persistentStoreCoordinator
方法,在 NSURL *storeURL = [[self app...
这一行的下面加入以下的代码:
if (![[NSFileManager defaultManager] fileExistsAtPath:[storeURL path]]) { NSURL *preloadURL = [NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:@"CoreDataTutorial2" ofType:@"sqlite"]]; NSError* err = nil; if (![[NSFileManager defaultManager] copyItemAtURL:preloadURL toURL:storeURL error:&err]) { NSLog(@"Oops, could copy preloaded data"); } } |
这一段的代码是为了检测sqlite数据库是否已经存在与这个app之中,如果不存在,就会找到我们预加载的数据库,之后把这个数据库复制到正常的路径,超级简单,来,让我们试试!
看到了原本在JSON文件中的四个Banks,之后还有一个我们在第一个教程中加入的Test Bank,如果你没有看到的话,八成是数据库已经存在了,山茶模拟器中的App之后重新运行。
之后看些什么?
这是我制作完成的例子程序源码,欢迎下载。
这是原作者的样板程序:here (direct download)
欢迎关注我的围脖: @Oratis
在知乎和豆瓣上,我的名字也是Oratis
我会把之后发表的教程分享到这些社交网络中。
如果你有任何问题,欢迎在底下留言,也欢迎写信给我,我的邮箱地址是: [email protected]