Core Data 包含了如何让UITableView,UICo
包含组件
最底层File System -> SQLite -> NSPersistent Store(可有多个) -> NSPersistent StoreCoordinator -> NSManagedObjectContext(可有多个,每个可包含多个NSManagedObject)
设置堆栈
范例:https://github.com/objcio/issue-4-full-core-data-application
- (void)setupManagedObjectContext
{
//使用initWithConcurrencyType:来明确使用的是基于队列的并发模型
self.managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
self.managedObjectContext.persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:self.managedObjectModel];
NSError* error;
[self.managedObjectContext.persistentStoreCoordinator
addPersistentStoreWithType:NSSQLiteStoreType
configuration:nil
URL:self.storeURL
options:nil
error:&error];
if (error) {
NSLog(@"error: %@", error);
}
self.managedObjectContext.undoManager = [[NSUndoManager alloc] init];
}
创建模型
在xcode新建的Core Data选项中选择Data Model template,模型文件会被编译成.momd文件。模型创建完毕就可以创建与之对应的NSManagedObject子类。从菜单选择Editor > NSManagedObject subclass。
模型的属性
- 默认/可选:建议不使用带默认值的可选属性
- Transient:方便撤销操作和故障处理,建议使用transient属性
- 索引:提高读取速度
- 标量类型:默认NSNumber,也可以使用int64_t,float_t或BOOL。
创建Store类
存储类除了managed object context还有rootItem方法,程序启动时会查找这root item然后传给root view controller。
- (Item*)rootItem
{
NSFetchRequest* request = [NSFetchRequest fetchRequestWithEntityName:@"Item"];
request.predicate = [NSPredicate predicateWithFormat:@"parent = %@", nil];
NSArray* objects = [self.managedObjectContext executeFetchRequest:request error:NULL];
Item* rootItem = [objects lastObject];
if (rootItem == nil) {
rootItem = [Item insertItemWithTitle:nil
parent:nil
inManagedObjectContext:self.managedObjectContext];
}
return rootItem;
}
//增加一个item
+ (instancetype)insertItemWithTitle:(NSString*)title
parent:(Item*)parent
inManagedObjectContext:(NSManagedObjectContext *)managedObjectContext
{
NSUInteger order = parent.numberOfChildren;
Item* item = [NSEntityDescription insertNewObjectForEntityForName:self.entityName
inManagedObjectContext:managedObjectContext];
item.title = title;
item.parent = parent;
item.order = @(order);
return item;
}
//获得子节点数量
- (NSUInteger)numberOfChildren
{
return self.children.count;
}
//创建一个fetched results controller的方法方便自动更新table view
- (NSFetchedResultsController*)childrenFetchedResultsController
{
NSFetchRequest* request = [NSFetchRequest fetchRequestWithEntityName:[self.class entityName]];
request.predicate = [NSPredicate predicateWithFormat:@"parent = %@", self];
request.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"order" ascending:YES]];
return [[NSFetchedResultsController alloc] initWithFetchRequest:request
managedObjectContext:self.managedObjectContext
sectionNameKeyPath:nil
cacheName:nil];
}
和Table View无缝结合
创建一个NSFetchedResultsController作为table view的data source
- (id)initWithTableView:(UITableView*)tableView
{
self = [super init];
if (self) {
self.tableView = tableView;
self.tableView.dataSource = self;
}
return self;
}
- (void)setFetchedResultsController:(NSFetchedResultsController*)fetchedResultsController
{
_fetchedResultsController = fetchedResultsController;
fetchedResultsController.delegate = self;
[fetchedResultsController performFetch:NULL];
}
- (NSInteger)numberOfSectionsInTableView:(UITableView*)tableView
{
return self.fetchedResultsController.sections.count;
}
- (NSInteger)tableView:(UITableView*)tableView numberOfRowsInSection:(NSInteger)sectionIndex
{
id<NSFetchedResultsSectionInfo> section = self.fetchedResultsController.sections[sectionIndex];
return section.numberOfObjects;
}
- (UITableViewCell*)tableView:(UITableView*)tableView
cellForRowAtIndexPath:(NSIndexPath*)indexPath
{
id object = [self.fetchedResultsController objectAtIndexPath:indexPath];
id cell = [tableView dequeueReusableCellWithIdentifier:self.reuseIdentifier
forIndexPath:indexPath];
[self.delegate configureCell:cell withObject:object];
return cell;
}
创建Table View Controller
在新建的Table view的viewDidLoad里写:
fetchedResultsControllerDataSource = [[FetchedResultsControllerDataSource alloc] initWithTableView:self.tableView];
self.fetchedResultsControllerDataSource.fetchedResultsController = self.parent.childrenFetchedResultsController;
fetchedResultsControllerDataSource.delegate = self;
fetchedResultsControllerDataSource.reuseIdentifier = @"Cell";
实现delegate
- (void)configureCell:(id)theCell withObject:(id)object
{
UITableViewCell* cell = theCell;
Item* item = object;
cell.textLabel.text = item.title;
}
添加
在textFieldShouldReturn:里
[Item insertItemWithTitle:title
parent:self.parent
inManagedObjectContext:self.parent.managedObjectContext];
textField.text = @"";
[textField resignFirstResponder];
增删改后table view也会更改显示
- (void)controller:(NSFetchedResultsController*)controller
didChangeObject:(id)anObject
atIndexPath:(NSIndexPath*)indexPath
forChangeType:(NSFetchedResultsChangeType)type
newIndexPath:(NSIndexPath*)newIndexPath
{
if (type == NSFetchedResultsChangeInsert) {
[self.tableView insertRowsAtIndexPaths:@[newIndexPath]
withRowAnimation:UITableViewRowAnimationAutomatic];
}
}
- (void)controllerWillChangeContent:(NSFetchedResultsController*)controller
{
[self.tableView beginUpdates];
}
- (void)controllerDidChangeContent:(NSFetchedResultsController*)controller
{
[self.tableView endUpdates];
}
和Collection View的结合
范例:https://github.com/AshFurrow/UICollectionView-NSFetchedResultsController collection view没有beginUpdates和endUpdates方法,所以只能用performBatchUpdate方法收集所有更新,然后在controllerDidChangeContent中用block执行所有更新。
如何传递Table view里的Model对象到新的view controller中
- (void)prepareForSegue:(UIStoryboardSegue*)segue sender:(id)sender
{
[super prepareForSegue:segue sender:sender];
if ([segue.identifier isEqualToString:selectItemSegue]) {
[self presentSubItemViewController:segue.destinationViewController];
}
}
- (void)presentSubItemViewController:(ItemViewController*)subItemViewController
{
Item* item = [self.fetchedResultsControllerDataSource selectedItem];
subItemViewController.parent = item;
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
self.fetchedResultsControllerDataSource.paused = NO;
}
- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
self.fetchedResultsControllerDataSource.paused = YES;
}
- (void)setPaused:(BOOL)paused
{
_paused = paused;
if (paused) {
self.fetchedResultsController.delegate = nil;
} else {
self.fetchedResultsController.delegate = self;
[self.fetchedResultsController performFetch:NULL];
[self.tableView reloadData];
}
}
删除
//让table view支持滑动删除
- (BOOL)tableView:(UITableView*)tableView
canEditRowAtIndexPath:(NSIndexPath*)indexPath
{
return YES;
}
- (void)tableView:(UITableView *)tableView
commitEditingStyle:(UITableViewCellEditingStyle)editingStyle
forRowAtIndexPath:(NSIndexPath *)indexPath {
if (editingStyle == UITableViewCellEditingStyleDelete) {
id object = [self.fetchedResultsController objectAtIndexPath:indexPath];
[self.delegate deleteObject:object];
}
}
//强制order变化,可以重写prepareForDeletion方法
- (void)prepareForDeletion
{
NSSet* siblings = self.parent.children;
NSPredicate* predicate = [NSPredicate predicateWithFormat:@"order > %@", self.order];
NSSet* siblingsAfterSelf = [siblings filteredSetUsingPredicate:predicate];
[siblingsAfterSelf enumerateObjectsUsingBlock:^(Item* sibling, BOOL* stop)
{
sibling.order = @(sibling.order.integerValue - 1);
}];
}
增加删除的动画效果
...
else if (type == NSFetchedResultsChangeDelete) {
[self.tableView deleteRowsAtIndexPaths:@[indexPath]
withRowAnimation:UITableViewRowAnimationAutomatic];
}
增加晃动撤销功能
//第一步告诉application支持这个
application.applicationSupportsShakeToEdit = YES;
//重写UIResponder类中的两个方法
- (BOOL)canBecomeFirstResponder {
return YES;
}
- (NSUndoManager*)undoManager
{
return self.managedObjectContext.undoManager;
}
//在持续化stack中设置一个undo manager
self.managedObjectContext.undoManager = [[NSUndoManager alloc] init];
//实现上面几步后晃动时会得到两个按钮的提醒框,可以给让用户体验更加友好些
NSString* title = textField.text;
NSString* actionName = [NSString stringWithFormat:NSLocalizedString(@"add item \"%@\"", @"Undo action name of add item"), title];
[self.undoManager setActionName:actionName];
[self.store addItem:title parent:nil];
排序
保存
- 保存是在managed object context中调用save。可以在applicationWillTerminate:中执行,也可能在applicationDIdEnterBackground:中。
- 异步存储Core Data 网络应用实例:(中文)http://objccn.io/issue-10-5/ (英文)http://www.objc.io/issue-10/networked-core-data-application.html
Fetch获取对象
基础
范例
request.result = [NSPredicate predicateWithFormat:
@"(%@ <= longitude) AND (longitude <= %@)"
@"AND (%@ <= latitude) AND (latitude <= %@)",
@(minLongitude), @(maxLongitude), @(minLatitude), @(maxLatitude)];
//取消将值放到row cache中。
request.returnsObjectsAsFaults = NO;
request.fetchLimit = 200;
//执行fetch
NSError *error = nil;
NSArray *stops = [moc executeFetchRequest:request error:&error];
NSAssert(stops != nil, @"Failed to execute %@: %@", request, error);
//二次遍历
NSPredicate *exactPredicate = [self exactLatitudeAndLongitudePredicateForCoordinate:self.location.coordinate];
stops = [stops filteredArrayUsingPredicate:exactPredicate];
- (NSPredicate *)exactLatitudeAndLongitudePredicateForCoordinate:(CLLocationCoordinate2D)pointOfInterest;
{
return [NSPredicate predicateWithBlock:^BOOL(Stop *evaluatedStop, NSDictionary *bindings) {
CLLocation *evaluatedLocation = [[CLLocation alloc] initWithLatitude:evaluatedStop.latitude longitude:evaluatedStop.longitude];
CLLocationDistance distance = [self.location distanceFromLocation:evaluatedLocation];
return (distance < self.distance);
}];
}
//子查询
NSPredicate *timePredicate = [NSPredicate predicateWithFormat:@"(%@ <= departureTime) && (departureTime <= %@)”, startDate, endDate];
NSPredicate *predicate = [NSPredicate predicateWithFormat:
@"(SUBQUERY(stopTimes, $x, (%@ <= $x.departureTime) && ($x.departureTime <= %@)).@count != 0)”, startDate, endDate];
//文本搜索
NSString *searchString = @"U Görli";
predicate = [NSPredicate predicateWithFormat:@"name BEGINSWITH %@", searchString];
导入大量数据
导入应用Bundle里的SQLite文件
NSFileManager* fileManager = [NSFileManager defaultManager];
NSError *error;
if([fileManager fileExistsAtPath:self.storeURL.path]) {
NSURL *storeDirectory = [self.storeURL URLByDeletingLastPathComponent];
NSDirectoryEnumerator *enumerator = [fileManager enumeratorAtURL:storeDirectory
includingPropertiesForKeys:nil
options:0
errorHandler:NULL];
NSString *storeName = [self.storeURL.lastPathComponent stringByDeletingPathExtension];
//遍历目录下是否有重复
for (NSURL *url in enumerator) {
if (![url.lastPathComponent hasPrefix:storeName]) continue;
[fileManager removeItemAtURL:url error:&error];
}
// 处理错误
}
NSString* bundleDbPath = [[NSBundle mainBundle] pathForResource:@"seed" ofType:@"sqlite"];
[fileManager copyItemAtPath:bundleDbPath toPath:self.storeURL.path error:&error];
//真机删除会失效,所以使用版本号来进行区分新旧
NSString* bundleVersion = [infoDictionary objectForKey:(NSString *)kCFBundleVersionKey];
NSString *seedVersion = [[NSUserDefaults standardUserDefaults] objectForKey@"SeedVersion"];
if (![seedVersion isEqualToString:bundleVersion]) {
// 复制源数据库
}
// ... 导入成功后
NSDictionary *infoDictionary = [NSBundle mainBundle].infoDictionary;
[[NSUserDefaults standardUserDefaults] setObject:bundleVersion forKey:@"SeedVersion"];
导入范例
https://github.com/objcio/issue-4-importing-and-fetching
版本迁移
Mapping Models
NSMigrationManager能够推断两个版本模型的映射关系,但是如果版本跨度大了就力不从心了。
Progressive Migrations渐进式迁移
实现原理是两个版本之间确保正常,升级时按照一个版本一个版本渐进式的升级方式,比如最新的版本是第四版,如果用户使用的是第二版的,那么升级是就是先从第二版升级到第三版,然后再从第三版升级到第四版。完整范例:https://github.com/objcio/issue-4-core-data-migration 主要代码来自Marcus Zarrahttps://twitter.com/mzarra ,他的书关于Core Data的值得一看,http://pragprog.com/book/mzcd2/core-data
迁移策略
NSEntityMigrationPolicy这个类不光能够修改Entity的属性和关系,还能够自定义一些操作完成每个Entity的迁移。例如在Entity Mapping的Custom Polity里写上自定义的polity的方法
NSNumber *modelVersion = [mapping.userInfo valueForKey:@"modelVersion"];
if (modelVersion.integerValue == 2) {
NSMutableArray *sourceKeys = [sourceInstance.entity.attributesByName.allKeys mutableCopy];
NSDictionary *sourceValues = [sourceInstance dictionaryWithValuesForKeys:sourceKeys];
NSManagedObject *destinationInstance = [NSEntityDescription insertNewObjectForEntityForName:mapping.destinationEntityName
inManagedObjectContext:manager.destinationContext];
NSArray *destinationKeys = destinationInstance.entity.attributesByName.allKeys;
for (NSString *key in destinationKeys) {
id value = [sourceValues valueForKey:key];
// 避免value为空
if (value && ![value isEqual:[NSNull null]]) {
[destinationInstance setValue:value forKey:key];
}
}
}
NSMutableDictionary *authorLookup = [manager lookupWithKey:@"authors"];
// 检查该作者是否已经被创建了
NSString *authorName = [sourceInstance valueForKey:@"author"];
NSManagedObject *author = [authorLookup valueForKey:authorName];
if (!author) {
// 创建作者
// ...
// 更新避免重复
[authorLookup setValue:author forKey:authorName];
}
[destinationInstance performSelector:@selector(addAuthorsObject:) withObject:author];
//源存储和目的存储之间的关系
[manager associateSourceInstance:sourceInstance
withDestinationInstance:destinationInstance
forEntityMapping:mapping];
return YES;
NSmigrationManager的category方法
@implementation NSMigrationManager (Lookup)
- (NSMutableDictionary *)lookupWithKey:(NSString *)lookupKey
{
NSMutableDictionary *userInfo = (NSMutableDictionary *)self.userInfo;
// 这里检查一下是否已经建立了 userInfo 的字典
if (!userInfo) {
userInfo = [@{} mutableCopy];
self.userInfo = userInfo;
}
NSMutableDictionary *lookup = [userInfo valueForKey:lookupKey];
if (!lookup) {
lookup = [@{} mutableCopy];
[userInfo setValue:lookup forKey:lookupKey];
}
return lookup;
}
@end
更复杂的迁移
NSArray *users = [sourceInstance valueForKey:@"users"];
for (NSManagedObject *user in users) {
NSManagedObject *file = [NSEntityDescription insertNewObjectForEntityForName:@"File"
inManagedObjectContext:manager.destinationContext];
[file setValue:[sourceInstance valueForKey:@"fileURL"] forKey:@"fileURL"];
[file setValue:destinationInstance forKey:@"book"];
NSInteger userId = [[user valueForKey:@"userId"] integerValue];
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"User"];
request.predicate = [NSPredicate predicateWithFormat:@"userId = %d", userId];
NSManagedObject *user = [[manager.destinationContext executeFetchRequest:request error:nil] lastObject];
[file setValue:user forKey:@"user"];
}
数据量大时的迁移改造,利用CoreData提供的chunks数据块方式。官方文档https://developer.apple.com/library/ios/documentation/cocoa/Conceptual/CoreDataVersioning/Articles/vmCustomizing.html#//apple_ref/doc/uid/TP40004399-CH8-SW9
NSArray *mappingModels = @[mappingModel]; // 我们之前建立的那个模型
if ([self.delegate respondsToSelector:@selector(migrationManager:mappingModelsForSourceModel:)]) {
NSArray *explicitMappingModels = [self.delegate migrationManager:self
mappingModelsForSourceModel:sourceModel];
if (0 < explicitMappingModels.count) {
mappingModels = explicitMappingModels;
}
}
for (NSMappingModel *mappingModel in mappingModels) {
didMigrate = [manager migrateStoreFromURL:sourceStoreURL
type:type
options:nil
withMappingModel:mappingModel
toDestinationURL:destinationStoreURL
destinationType:type
destinationOptions:nil
error:error];
}
- (NSArray *)migrationManager:(MHWMigrationManager *)migrationManager
mappingModelsForSourceModel:(NSManagedObjectModel *)sourceModel
{
NSMutableArray *mappingModels = [@[] mutableCopy];
NSString *modelName = [sourceModel mhw_modelName];
if ([modelName isEqual:@"Model2"]) {
// 把该映射模型加入数组
}
return mappingModels;
}
- (NSString *)mhw_modelName
{
NSString *modelName = nil;
NSArray *modelPaths = // get paths to all the mom files in the bundle
for (NSString *modelPath in modelPaths) {
NSURL *modelURL = [NSURL fileURLWithPath:modelPath];
NSManagedObjectModel *model = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];
if ([model isEqual:self]) {
modelName = modelURL.lastPathComponent.stringByDeletingPathExtension;
break;
}
}
return modelName;
}
建立单元测试,
- (void)setUpCoreDataStackMigratingFromStoreWithName:(NSString *)name
{
NSURL *storeURL = [self temporaryRandomURL];
[self copyStoreWithName:name toURL:storeURL];
NSURL *momURL = [[NSBundle mainBundle] URLForResource:@"Model" withExtension:@"momd"];
self.managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:momURL];
NSString *storeType = NSSQLiteStoreType;
MHWMigrationManager *migrationManager = [MHWMigrationManager new];
[migrationManager progressivelyMigrateURL:storeURL
ofType:storeType
toModel:self.managedObjectModel
error:nil];
self.persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:self.managedObjectModel];
[self.persistentStoreCoordinator addPersistentStoreWithType:storeType
configuration:nil
URL:storeURL
options:nil
error:nil];
self.managedObjectContext = [[NSManagedObjectContext alloc] init];
self.managedObjectContext.persistentStoreCoordinator = self.persistentStoreCoordinator;
}
- (NSURL *)temporaryRandomURL
{
NSString *uniqueName = [NSProcessInfo processInfo].globallyUniqueString;
return [NSURL fileURLWithPath:[NSTemporaryDirectory() stringByAppendingString:uniqueName]];
}
- (void)copyStoreWithName:(NSString *)name toURL:(NSURL *)url
{
// 每次创建一个唯一的url以保证测试正常运行
NSBundle *bundle = [NSBundle bundleForClass:[self class]];
NSFileManager *fileManager = [NSFileManager new];
NSString *path = [bundle pathForResource:[name stringByDeletingPathExtension] ofType:name.pathExtension];
[fileManager copyItemAtPath:path
toPath:url.path error:nil];
}
//在测试类中复用
- (void)setUp
{
[super setUp];
[self setUpCoreDataStackMigratingFromStoreWithName:@"Model1.sqlite"];
}
调试迁移一个有用的启动参数是-com.apple.CoreData.MigrationDebug,设置1就会在console收到迁移数据时会出现的特殊的情况的信息。如果设置-com.apple.CoreData.SQLDebug 为 1还能够在console看到实际操作的SQL语句。
Core Data的并行处理
- 官方文档有基本规则:https://developer.apple.com/library/mac/#documentation/cocoa/conceptual/CoreData/Articles/cdConcurrency.html
- 处理步骤,先创建一个操作,创建一个managed object context,和main managed object context使用同样的persistent store coordinator,当context保存后就通知main managed object context,后再合并变化。可以参考这个范例https://github.com/objcio/issue-2-background-core-data
性能测试
- 加上-com.apple.CoreData.SQLDebug1 作为启动参数传递给应用程序可以得到的输出
sql: SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZIDENTIFIER, t0.ZLATITUDE, t0.ZLONGITUDE, t0.ZNAME FROM ZSTOP t0 WHERE (? <= t0.ZLONGITUDE AND t0.ZLONGITUDE <= ? AND ? <= t0.ZLATITUDE AND t0.ZLATITUDE <= ?) LIMIT 100
annotation: sql connection fetch time: 0.0008s
annotation: total fetch execution time: 0.0013s for 15 rows.
实际生成的SQL是:
SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZIDENTIFIER, t0.ZLATITUDE, t0.ZLONGITUDE, t0.ZNAME FROM ZSTOP t0
WHERE (? <= t0.ZLONGITUDE AND t0.ZLONGITUDE <= ? AND ? <= t0.ZLATITUDE AND t0.ZLATITUDE <= ?)
LIMIT 200
- 使用SQL 的EXPLAIN命令https://www.sqlite.org/eqp.html对性能调查研究
% cd TrafficSearch
% sqlite3 transit-data.sqlite
SQLite version 3.7.13 2012-07-17 17:46:21
Enter ".help" for instructions
Enter SQL statements terminated with a ";"
sqlite> EXPLAIN QUERY PLAN SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZIDENTIFIER, t0.ZLATITUDE, t0.ZLONGITUDE, t0.ZNAME FROM ZSTOP t0
...> WHERE (13.30845219672199 <= t0.ZLONGITUDE AND t0.ZLONGITUDE <= 13.33441458422844 AND 52.42769566863058 <= t0.ZLATITUDE AND t0.ZLATITUDE <= 52.44352370653525)
...> LIMIT 100;
0|0|0|SEARCH TABLE ZSTOP AS t0 USING INDEX ZSTOP_ZLONGITUDE_INDEX (ZLONGITUDE>? AND ZLONGITUDE<?) (~6944 rows)
输出
0|0|0|SEARCH TABLE ZSTOP AS t0 USING INDEX ZSTOP_ZLONGITUDE_ZLATITUDE (ZLONGITUDE>? AND ZLONGITUDE<?) (~6944 rows)