转自:http://www.cnblogs.com/geory/archive/2013/03/14/2958629.html
这节课的主要内容是Core Data、NSNotificationCenter和Objective-C Categories。
Core Data
它是一个完全面向对象的API,负责在数据库中存储数据,底层也是由类似于SQL的技术来实现的。
在高级语言这一层,如何使用Core Data?在xcode中,有个工具可以建立对象之间的映射,这些对象会存储在你的数据库里,它们是NSObject的子类,实际上是NSManagedObject的子类,然后Core Data负责管理这些对象之间的关系。一旦在xcode中建立了visual map,你就可以新建对象,并存到数据库里或在数据库里删除、查询,实际起作用的是底层的SQL。可以用property访问数据库中对象内部的数据。Core Data负责管理底层的通信。
如何建立visual map?打开New File界面,在左边找到Core Data,这里选择Data Model,然后点Next,这样就建立了一个数据库的图形化model。通常会给visual map一个和应用相同的名字。
map的内部结构是怎样的?由3个不同的部分组成:一是entities,它们将映射到class;还有attributes,它映射到properties上;然后是relationship,这个属性用来指向数据库中的其他对象。
新建两个entity,分别是photo和photographer,它们之间会有一个明显的relationship。在代码中,entity实际上是一个NSManagedObject。
接下来要做的是如何创建NSManagedObject的子类,有了这些子类就可以调用数据库中的entity了。即使创建了子类,管理这些对象的底层机制仍然是NSManagedObject。
记住,所有的attribute都是对象,Core Data只知道在数据库中如何读写对象,所有的attribute都是各种不同类型的对象。有几种方法可以获取这些对象的值,一种方法是可以用NSKeyValueCoding协议,valueForKey和setValueForKey是这个协议的一部分,所有对象都可以使用它们,用valueForKey和setValueForKey设置property;另一种访问attribute的方法是新建一个NSManagedObject的子类,数据库的所有对象在代码中都是NSManagedObject。
不仅可以以表格的形式查看entity和attribute,还可以用图的方式。点击右下角的Editor Style,看到的内容与刚才一样,但是是以图的方式。可以在entities之间按住control拖动,来建立它们之间的relationship。一旦建立了关系,可以双击它,然后在inspector里改变它的名字,有个开关叫To-Many Relationship,就是设置两者间一对多的关系,注意其中的Delete Rule,意思是如果删除其中一个,那么会对这个relationship指向的东西有什么影响?其实就是把指针设为空。relationship的property类型:whoTook这个property的类型是NSManagedObject *;photos的类型是NSSet,它是一个内部数据类型为NSManagedObject *的NSSet。NSSet就是一堆对象的集合,它是无序的。
怎么在代码中使用visual map的数据呢?要获得数据,最重要的一点是,需要使用一个NSManagedObjectContext的东西,这是一个类,需要实例化。可以给这个实例发消息,比如查询之类。
怎么得到NSManagedObjectContext呢?需要它来往数据库里添加数据或进行查询操作,有两种基本方法可以获得NSManagedObjectContext:其一是创建UIManagedDocument,它有个属性叫managedObjectContext,获取它并使用就好了;第二种方法是在你新建一个工程的时候,有个复选框Use Core Data,选中它,就会在AppDelegate中生成一些代码,添加一个managedObjectContext的property。
UIManagedDocument
UIManagedDocument类继承自UIDocument,UIDocument有一套机制来管理一个或一组与磁盘相关的文件。UIManagedDocument实际上是一个装载Core Data数据库的容器,而且这个容器提供一些功能,比如写入、打开数据库。
怎么创建UIManagedDocument呢?它只有一个intializer,叫做initWithFileURL:
UIManagedDocument *document = [[UIManagedDocument alloc] initWithFileURL:(URL *)url];
这个url几乎总是在文档目录下。现在还不可以用,还需要打开它,或者是创建,来使用。alloc init之后,它实际上并没有在磁盘上打开或创建。怎么打开或创建document?要调用以下方法来打开它:
- (void)openWithCompletionHandler:(void (^)(BOOL success))completionHandler;
CompletionHandler就是一个简单的block,这是一个没有返回值的block,它只处理一个表明是否成功打开文件的布尔值。如果文件不存在,不得不检查一下,必须调用fileExistsAtPath来检查这个文件是否存在:
[[NSFileManager defaultManager] fileExistsAtPath:[url path]]
如果这个文件存在,就可以用openWithCompletionHandler。但是如果不存在,需要创建它,需要调用UIManagedDocument里的这个方法来创建:
- (void)saveToURL:(NSURL *)url forSaveOperation:(UIDocumentSaveOperation)operation competionHandler:(void (^)(BOOL success))completionHandler;
创建完之后,如果要保存需要调用UIDocumentSaveForCreating。这边也有一个CompletionHandler。
为什么会有一个CompletionHandler呢?open和save方法是异步的,这些操作要花费一些时间,它们会立刻返回,但文件此时还没打开或创建好,只有在之后CompletionHandler被调用的时候,才能用这个document。异步的意思是这些操作需要花费一些时间,当这些操作完成之后调用你的block。
这是一个典型的例子:
self.document = [[UIManagedDocument alloc] initWithFileURL:(URL *)url]; if ([[NSFileManager defaultManager] fileExistsAtPath:[url path]]) { [document openWithCompletionHandler:^(BOOL success) { if (success) [self documentIsReady]; if (!success) NSLog(@“couldn’t open document at %@”, url); }]; } else { [document saveToURL:url forSaveOperation:UIDocumentSaveForCreating completionHandler:^(BOOL success) { if (success) [self documentIsReady]; if (!success) NSLog(@“couldn’t create document at %@”, url); }]; }
在这还不能对文档进行任何操作,因为这两个调用是异步的,必须等block被调用后,并激活一些条件才可以。
如果document打开了或创建好了,documentIsReady被调用了,你就可以使用它了:
- (void)documentIsReady { if (self.document.documentState == UIDocumentStateNormal) { NSManagedObjectContext *context = self.document.managedObjectContext; // do something with the Core Data context } }
document中有一个documentState的东西,通常在使用它之前都会检查这个documentState,最重要的状态就是UIDocumentStateNormal,意思就是已经打开好了,可以用了。如果状态是normal的话,我要做的是获得document context,然后就可以做Core Data的操作了,创建对象,查询,或从数据库在读取一些东西等等。
其他一些状态:UIDocumentStateClosed,这是document开始时的状态,当alloc initWithFileURL时,它的状态就是closed的;UIDocumentStateSavingError,这是指当保存文件时调用CompletionHandler出现了success等于NO,就会出现这种状态;UIDocumentStateEditingDisabled,这个状态是一个瞬时的状态,或许document正在重置,重置回以前保存的状态,或者保存操作正在进行,不能进行编辑;UIDocumentStateInConflict,这是处理iCloud时可能遇到的情况。
documentState的状态通常处于observed(监听)中,这是指,在ios中有一种方法,当documentState改变时,就告诉我,或者当有一个冲突出现了,马上告诉我,我好立刻解决问题。这个observed怎么用,它由NSNotification这个机制来管理。
NSNotification
有一种通信方式是广播站模式的,这种模式有点像广播,其他人可以接进这个广播站来并收听消息,这就是NSNotification。有一种办法可以让一个对象注册成为radio station,然后其他对象收听这个radio station。
需要一个NSNotificationCenter,就像交换中心似的,也可以把它想象成一个广播站注册机构。最简单的方式是调用[NSNotificationCenter defaultCenter],然后给NSNotification传递一个方法:
- (void)addObserver:(id)observer // you (the object to get notified) selector:(SEL)methodToSendIfSomethingHappens name:(NSString *)name // what you’re observing (a constant somewhere) object:(id)sender; // whose changes you’re interested in (nil is anyone’s)
addObserver就是你自己,你把自己设置为observer。selector是指当广播站广播时,会被调用的方法。name是指radio station的名字,是一个常量字符串,几乎总是常量类型的,一些类会告诉你它们广播站的名字,好让你注册。object是指你想收听的对象,你可以注册收听广播站上的任何广播,或者只收听某个特定的广播,如果是nil,就是收听所有的广播。
必须指定selector的名字,它的参数总是NSNotification *。NSNotification有三个property,一个是name,就是radio station的name,和上面一样;object,就是给你发送通知的那个对象,和上面一样。然后是userInfo,它就是个ID,可以是任何东西,由广播员负责告诉你现在正在播放什么内容,通常它会像一个词典或者某种容器来保存数据。
- (void)methodToSendIfSomethingHappens:(NSNotification *)notification { notification.name // the name passed above notification.object // the object sending you the notification notification.userInfo// notification-specific information about what happened }
下面来看一个例子,是关于documentState的:
NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; [center addObserver:self selector:@selector(documentChanged:) name:UIDocumentStateChangedNotification object:self.document];
把自己添加成observer。这边要注册的广播站是UIDocumentStateChangedNotification,这是在UIManagedDocument中定义的一个NSString,其实是在UIDocument.h中。object是我想收听的对象,所以在这写self.document。只要把这个消息传递给center,只要documentState有变化,我就会得到一个documentChanged的消息,这个消息会有一个NSNotification *参数。
当你不再需要监听广播时,要删除自己的observer身份。原因是,NSNotification不会维护一个指向你的weak指针,它维护一个unsafe或者是unretained的指针。这并不安全,如果被指向的对象消失,unsafe或者unretained指针会指向堆上的一块无用的内存,必须要确保在对象消失之前解除你的observer身份。
[center removeObserver:self]; or [center removeObserver:self name:UIDocumentStateChangedNotification object:self.document];
很有可能,会在viewDidAppear或者viewWillDisappear中传递add或者remove消息,这边有一个例子:
- (void)viewDidAppear:(BOOL)animated{ [super viewDidAppear:animated]; [center addObserver:self selector:@selector(contextChanged:) name:NSManagedObjectContextObjectsDidChangeNotification object:self.document.managedObjectContext]; } - (void)viewWillDisappear:(BOOL)animated{ [center removeObserver:self name:NSManagedObjectContextObjectsDidChangeNotification object:self.document.managedObjectContext]; [super viewWillDisappear:animated]; }
这里监听Core Data数据库是否有变化。记住,可以由多个不同的managedObjectContext改变数据库,这样会造成混淆,如果多线程就容易解决。广播者是managedObjectContext,如果数据库中添加,删除,或者有一些更改,它就会向你广播。广播站叫NSManagedObjectContextObjectsDidChangeNotification。
contextChanged是这个样子的:
- (void)contextChanged:(NSNotification *)notification { The notification.userInfo object is an NSDictionary with the following keys: NSInsertedObjectsKey // an array of objects which were inserted NSUpdatedObjectsKey //anarrayofobjectswhoseattributeschanged NSDeletedObjectsKey //anarrayofobjectswhichweredeleted }
userInfo是一个词典,这个词典有三个键,这些键是否存在取决于NSManagedObjectContext中出现了什么变化,这些键的值是NSArray,它的内部数据类型为一个有过更改的NSManagedObject,你可以获得context中所发生的更改的完整描述。
UIManagedDocument
打开或者创建document,获取它的context,对数据库做了很多更改,怎么保存这些更改呢?UIManagedDocument是自动保存的,但不会依赖这种自动保存机制,可以用以下这个方法来保存数据:
[self.document saveToURL:self.document.fileURL forSaveOperation:UIDocumentSaveForOverwriting completionHandler:^(BOOL success) { if (!success) NSLog(@“failed to save document %@”, self.document.localizedName); }];
关闭document同样是异步的,什么时候需要关闭document呢?在完成更改后都需要关闭它,同时撤销所有指向UIManagedDocument的strong指针。如果没有strong指针指向UIManagedDocument时,它会自动关闭。
[self.document closeWithCompletionHandler:^(BOOL success) { if (!success) NSLog(@“failed to close document %@”, self.document.localizedName); }];
它是异步的,得等到block执行了,它才会关闭。
可以有UIManagedDocument的多个实例指向磁盘上的同一个document吗?完全可以,但要小心,这些实例是没有关系的。
Core Data
现在从document中获得了一个NSManagedObjectContext,就可以进行插入和删除操作,可以进行查询。
通过调用NSEntityDescription中的方法来插入数据,这是一个类方法:
NSManagedObject *photo = [NSEntityDescription insertNewObjectForEntityForName:@“Photo”
inManagedObjectContext:(NSManagedObjectContext *)context];
数据库中的所有对象都是由NSManagedObject表示的,NSEntityDescription insert的返回值是一个NSManagedObject *,它返回一个指向新创建对象的指针。
现在有这个对象了,需要设置它的attribute,怎么访问这些attribute呢?可以用NSKeyValueObserving协议,注意NSKeyValueObserving协议中的Observing,可以观察任何支持这个协议的对象的setting和geting这两个property,你希望观察这些property,这看起来和NSNotificationCenter很相似,可以说添加一个观察者,来观察这个对象的某个property,只要这个对象为这个property实现了这个协议。
- (id)valueForKey:(NSString *)key; - (void)setValue:(id)value forKey:(NSString *)key;
如果使用valueForKeyPath:/setValue:forKeyPath:方法,它就会跟踪那个relationship。key是attribute的名字,而value是所存的内容。
对UIManagedDocument做的所有修改都是在内存中进行的,直到做了save操作。
但是调用valueForKey:/setValueForKey:会使代码变得很乱,这么做没有任何的类型检查,所以通常不用这种方法。用property,但是如何给NSManagedObject添加一个property,并且它的类型是Photographer *,而不是NSManagedObject *,而且是在NSManagedObject不认识这些东西的情况下。方法是创建NSManagedObject的子类,比如创建一个名为Photo的NSManagedObject的子类来表示photo entity,它在头文件里生成的就是@property,这个@property对应着所有的attribute,在实现文件中采用的不是@synthesize,因为@synthesize是给它生成一个实例变量,但这些property并不是以实例变量存储的,它是存储在SQL数据库里的。
怎么生成NSManagedObject的子类呢?只需到xcode中的model file,选中它们,然后到Editor菜单,点击下面的Create NSManagedObject subclasses。生成后可以看到Photographer.h和.m文件,还有Photo.h和.m文件。
它创建了一个category,可以用来设置NSSet中的值。怎么往photos relationship中添加图片呢?有两种方法:一种是可以用它自动生成的add;另一种是用photos这个set,调用mutableCopy,这样就有一个mutable set了,然后往里面加东西,然后把photos设置回来就行了,通过调用这个property的setter。
在Photo.h中可以看到whoTook,它的类型是NSManagedObject *,应该是Photographer *才对。这是xcode的问题,在xcode生成代码时,它先生成Photo,然后生成Photographer。怎么修改这个错误呢?回到xcode,再生成一下就行了。
再看.m文件,很简洁,它所做的就是在所有property前面加上@dynamic,@dynamic的作用是告诉编译器我清楚我不需要对这个property进行@synthesize,请不要发出警告。如果这些子类不实现这些property,会有什么后果?这就不确定了。NSManagedObject的做法是,如果你传递一个property,它就会查找自己是否有个相同名字的属性,如果有,它就调用valueForKey:,或者setValueForKey:。如果添加一些额外的property,会出现错误。
有了Photographer.h、Photographer.m文件、Photo.h和Photo.m文件,那如何访问property呢?用“.”的方式调用就可以。
Photo *photo = [NSEntityDescription insertNewObjectForEntityForName:@“Photo” inManagedObj...]; NSString *myThumbnail = photo.thumbnailURL; photo.thumbnailData = [FlickrFetcher urlForPhoto:photoDictionary format:FlickrPhotoFormat...]; photo.whoTook = ...; // a Photographer object we created or got by querying photo.whoTook.name = @“CS193p Instructor”; // yes, multiple dots will follow relationships
如果更改schema,得重新生成子类。要是往其中加入一些代码呢,这么做就得修改Photo.m,那下次改变schema并在xcode中重新生成时,代码就没了。怎么解决这个问题呢?用一个Objective-C语言的一个新特性,叫category。
Categories
Categories可以让你在不使用子类的情况下往一个类中添加方法或者属性,语法是这样的:
@interface Photo (AddOn) - (UIImage *)image; @property (readonly) BOOL isOld; @end
这就是@interface,它会在Photo+AddOn.h中。不仅需要声明这些方法,还要实现它们,这里是一个.m文件可能的写法:
@implementation Photo (AddOn) -(UIImage*)image //imageisnotanattributeinthedatabase,butphotoURLis { NSData *imageData = [NSData dataWithContentsOfURL:self.photoURL]; return [UIImage imageWithData:imageData]; } -(BOOL)isOld //whetherthisphotowasuploadedmorethanadayago { return [self.uploadDate timeIntervalSinceNow] < -24*60*60; } @end
把它们加入到Photo类,isOld是只读的,只添加isOld的getter方法,self就是Photo。
使用category有一个很大的限制就是,它自己是不能添加实例变量的。所以在实现一个category时,内部是不能有@synthesize。
向NSManagedObject的子类,添加的最常用的category是Create:
@implementation Photo (Create) + (Photo *)photoWithFlickrData:(NSDictionary *)flickrData inManagedObjectContext:(NSManagedObjectContext *)context { Photo *photo = ...; // see if a Photo for that Flickr data is already in the database if (!photo) { photo = [NSEntityDescription insertNewObjectForEntityForName:@“Photo” inManagedObjectContext:context]; // initialize the photo from the Flickr data // perhaps even create other database objects (like the Photographer) } return photo; } @end
要使用这个方法,只需import Photo+Create.h。
Core Data
如何在数据库上删除对象,只要调用以下方法:
[self.document.managedObjectContext deleteObject:photo];
必须要保证如果删除数据库中的某个对象时,数据要维持在一个稳定的状态。
有一个prepareForDeletion方法,而且可以在category中实现它,这个方法必须由一个NSManagedObject的子类来实现,才可以调用。在将要进行删除操作的时候,就会调用它。就是说,如果有谁调用了deletePhoto,这个过程的前期就是调用这个prepareForDeletion。
@implementation Photo (Deletion) - (void)prepareForDeletion { // we don’t need to set our whoTook to nil or anything here (that will happen automatically) // but if Photographer had, for example, a “number of photos taken” attribute, // we might adjust it down by one here (e.g. self.whoTook.photoCount--). } @end
在对象删除后,就不要保留strong指针了。
怎么查询呢?通过创建、执行NSFetchRequest对象来完成。首先要创建,然后请求NSManagedObjectContext替你执行这个fetch。
在建立NSFetchRequest时,有四点很重要:
首先,要指明想获取的那个entity;
还有,NSPredicate,这个指明你想从哪些entities中获取数据,就是查询条件;
再有,NSSortDescriptors,因为fetch会返回一个array,就是一个有序列表,所以要指明排序规则;
最后,可以控制每次查询的返回值的数量,或者每个batch有多少。
这是查找和建立一个fetch请求,大概的写法:
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@“Photo”]; request.fetchBatchSize = 20; request.fetchLimit = 100; request.sortDescriptors = [NSArray arrayWithObject:sortDescriptor]; request.predicate = ...;
首先是指明entity,当你查询Core Data时,只返回一类entity,从数据库角度讲,只能在一个表上查询,每次只能从一个表中获取数据。NSSortDescriptor,它指明了你在执行这个查询后返回的array的排列顺序,通过以下方法来创建sortDescriptor:
NSSortDescriptor *sortDescriptor =
[NSSortDescriptor sortDescriptorWithKey:@“thumbnailURL”
ascending:YES
selector:@selector(localizedCaseInsensitiveCompare:)];
key就是排序时要参照的那个属性,ascending用来指定是升序还是降序,然后是selector,它并非一定得是Objective-C selector。排序是在数据库中进行的,也就是SQL做排序的工作,然后返回排列好的数据。fetch request的sortDescriptor不是只能有一个,可以是一个sortDescriptor的组合。
predicate用来表明你想得到什么样的对象,它看起来就像一个NSString:
NSString *serverName = @“flickr-5”; NSPredicate *predicate = [NSPredicate predicateWithFormat:@“thumbnailURL contains %@”, serverName];
还有一些例子:
@“uniqueId = %@”, [flickrInfo objectForKey:@“id”] // unique a photo in the database @“name contains[c] %@”, (NSString *) // matches name case insensitively @“viewed > %@”, (NSDate *) // viewed is a Date attribute in the data mapping @“whoTook.name = %@”, (NSString *) // Photo search (by photographer’s name) @“any photos.title contains %@”, (NSString *) // Photographer search (not a Photo search)
contain的意思就是是否有子字符串,注意这个[c],意思是区分大小写。
这还有一个例子,如果想查询所有Photographer,查询会在Photographer表上进行:
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@“Photographer”]; ... who have taken a photo in the last 24 hours ... NSDate *yesterday = [NSDate dateWithTimeIntervalSinceNow:-24*60*60]; request.predicate = [NSPredicate predicateWithFormat:@“any photos.uploadDate > %@”, yesterday]; ... sorted by the Photographer’s name ... NSSortDescriptor *sortByName = [NSSortDescriptor sortDescriptorWithKey:@“name” ascending:YES]; request.sortDescriptors = [NSArray arrayWithObject:sortByName];
这个请求建好了,接下来是如何执行这个查询?我向managedObjectContext发送一个消息,这个managedObjectContext是从document中获取的,消息的名字叫executeFetchRequest,发送请求的时候,还跟了一个error指针,这样也能接收到error消息。
NSManagedObjectContext *moc = self.document.managedObjectContext; NSError *error; NSArray *photographers = [moc executeFetchRequest:request error:&error];
如果返回值是nil,表示出错了,要查看一下这个error。如果返回的array是空的,是指没有查询到符合条件的对象。
所有的数据并不会一次返回,它会有选择的存储你想要的对象。
这节课的主要内容是Core Data的线程安全、Core DataTable View,以及大Demo。
Core Data Thread Safety
NSManagedObjectContext不是线程安全的,只能在创建NSManagedObjectContext的那个线程里访问它。一个数据库有多个UIManagedDocument和context,它们可以在不同的线程里创建,只要能管理好它们之间的关系就没问题。
线程安全的意思是,程序可能会崩溃,如果多路访问同一个NSManagedObjectContext,或在非创建实例的线程里访问实例,app就会崩溃。对此要怎么做呢?NSManagedObjectContext有个方法叫performBlock可以解决这个问题:
[context performBlock:^{ //or performBlockAndWait: // do stuff with context }];
它会自动确保block里的东西都在正确的context线程里执行,但这不一定就意味着使用了多线程。事实上,如果在主线程下创建的context,那么这个block会回到主线程来,而不是在其他线程里运行,这个performBlock只是确保block运行在正确的线程里。
NSManagedObjectContext,包括所有使用SQL的Core Data,都有一个parentContext,这就像是另一个NSManagedObjectContext,在真正写入数据库之前要写入到这里。可以获取到parentContext,可以让parentContext调用performBlock来做一些事,这总是在另一个线程里进行。parentContext和创建的NSManagedObjectContext不在一个线程里运行,可以通过performBlock在那个线程里执行你要做的事。记住,如果改变了parentContext,必须保存,然后重新获取child context。如果你想在非主线程载入很多内容,那么就全部放入数据库,然后在主线程去获取,这个效率非常快。
Core Data and Table View
ios里有一个非常重要的类NSFetchedResultsController,它不是一个viewController,而是控制fetchedResults与tableView通信方式的controller,所以这个NSFetchedResultsController是一个NSObject。
它的作用就是用来连接NSFetchedResultsController和UITableViewController的,而且连接的方法很强大。首先,它可以回答所有UITableView DataSource、protocol的问题,唯一不能回答的是cellForRowAtIndexPath。这是个NSFetchedResultsController的例子:
- (NSUInteger)numberOfSectionsInTableView:(UITableView *)sender{ return [[self.fetchedResultsController sections] count]; } - (NSUInteger)tableView:(UITableView *)sender numberOfRowsInSection:(NSUInteger)section{ return [[[self.fetchedResultsController sections] objectAtIndex:section] numberOfObjects]; }
tableView有个property是NSFetchedResultsController,就是这个self.fetchedResultsController。
cellForRowAtIndexPath也是很容易实现的,因为NSFetchedResultsController有objectAtIndexPath这个方法,你给出一个index,它会返回给你该row所显示的NSManagedObject:
- (NSManagedObject *)objectAtIndexPath:(NSIndexPath *)indexPath;
如果要实现cellForRowAtIndexPath,可以像下面这样做:
- (UITableViewCell *)tableView:(UITableView *)sender cellForRowAtIndexPath:(NSIndexPath *)indexPath { UITableViewCell *cell = ...; NSManagedObject *managedObject = [self.fetchedResultsController objectAtIndexPath:indexPath]; // load up the cell based on the properties of the managedObject // of course, if you had a custom subclass, you’d be using dot notation to get them return cell; }
有了managedObject,可以用valueForKey或者子类它,用.号来获取它的property。在一个Core Data驱动的tableView里,每一行都是通过数据库里的对象来表示的,一对一的关系。
如何创建这个NSFetchedResultsController呢?这是个如何alloc/init的例子:
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@“Photo”]; NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@“title” ...]; request.sortDescriptors = [NSArray arrayWithObject:sortDescriptor]; request.predicate = [NSPredicate predicateWithFormat:@“whoTook.name = %@”, photogName]; NSFetchedResultsController *frc = [[NSFetchedResultsController alloc] initWithFetchRequest:(NSFetchRequest *)request managedObjectContext:(NSManagedObjectContext *)context sectionNameKeyPath:(NSString *)keyThatSaysWhichSectionEachManagedObjectIsIn cacheName:@“MyPhotoCache”; // careful!
如果用这个fetch request来创建NSFetchedResultsController,那么table的每一行的照片的photographer名字都会对应这个字符串。cacheName就是缓存名,缓存速度很快,但有一点要小心:如果改变了fetch request的参数,比如改了predicate或者sortDescriptors,缓存会被破坏。如果真的要通过NSFetchedResultsController改fetch request,那么得删除缓存。NSFetchedResultsController里有个工厂方法可以删除缓存,或者可以就设为null,这样就不会有缓存,对于小型数据库,可以不用缓存。对于一个非常大型的数据库,如果要改request,得先删除缓存。另一个参数sectionNameKeyPath表示每个managedObject所在的section,这就是objects的一个attribute,这个例子里就是photo的一个attribute,如果问这个attribute关于photo的事,它会告诉我这个section的title,所以这里就是section的title。
如果context改变了,NSFetchedResultsController会自动更新table,只要把两个东西连接起来就行。唯一的前提是,这些改变都要发生在创建NSFetchedResultsController的那个context里。原理是,NSFetchedResultsController有个delegate,它会发很多消息,例如,添加了对象或者对象的一个属性改了等等,然后tableView就会去做相应的调整,事实上还是要在tableView里写些代码的。
Demo
这个demo会从Flickr上得到几百张照片,但table里显示的不是照片而是摄影师,点击摄影师就会显示他拍的所有照片的一个列表,点击photo才会显示图片。我们要实现从Flickr获取数据并存到Core Data数据库,然后把CoreDataTableView和fetch request连接起来。
在xcode新建project,从Single View Application开始,就叫它Photomania,在storyboard中从library拖出两个tableViewController,并将其中一个导入到导航控制器中,然后将开始小箭头移到导航控制器前面。新建两个UITableViewController的子类,分别为PhotographersTableViewController和PhotosByPhotographerTableViewController,到storyboard中将两个tableViewController的类分别设为这两个。
Photo+Flickr.h文件代码:
#import "Photo.h" @interface Photo (Flickr) + (Photo *)photoWithFlickrInfo:(NSDictionary *)flickrInfo inManagedObjectContext:(NSManagedObjectContext *)context; @end
Photo+Flickr.m文件代码:
#import "Photo+Flickr.h" #import "FlickrFetcher.h" #import "Photographer+Create.h" @implementation Photo (Flickr) // 9. Query the database to see if this Flickr dictionary's unique id is already there // 10. If error, handle it, else if not in database insert it, else just return the photo we found // 11. Create a category to Photographer to add a factory method and use it to set whoTook // (then back to PhotographersTableViewController) + (Photo *)photoWithFlickrInfo:(NSDictionary *)flickrInfo inManagedObjectContext:(NSManagedObjectContext *)context { Photo *photo = nil; NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Photo"]; request.predicate = [NSPredicate predicateWithFormat:@"unique = %@", [flickrInfo objectForKey:FLICKR_PHOTO_ID]]; NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"title" ascending:YES]; request.sortDescriptors = [NSArray arrayWithObject:sortDescriptor]; NSError *error = nil; NSArray *matches = [context executeFetchRequest:request error:&error]; if (!matches || ([matches count] > 1)) { // handle error } else if ([matches count] == 0) { photo = [NSEntityDescription insertNewObjectForEntityForName:@"Photo" inManagedObjectContext:context]; photo.unique = [flickrInfo objectForKey:FLICKR_PHOTO_ID]; photo.title = [flickrInfo objectForKey:FLICKR_PHOTO_TITLE]; photo.subtitle = [flickrInfo valueForKeyPath:FLICKR_PHOTO_DESCRIPTION]; photo.imageURL = [[FlickrFetcher urlForPhoto:flickrInfo format:FlickrPhotoFormatLarge] absoluteString]; photo.whoTook = [Photographer photographerWithName:[flickrInfo objectForKey:FLICKR_PHOTO_OWNER] inManagedObjectContext:context]; } else { photo = [matches lastObject]; } return photo; } @end
Photographer+Create.h文件代码:
#import "Photographer.h" @interface Photographer (Create) + (Photographer *)photographerWithName:(NSString *)name inManagedObjectContext:(NSManagedObjectContext *)context; @end
Photographer+Create.m文件代码:
#import "Photographer+Create.h" @implementation Photographer (Create) + (Photographer *)photographerWithName:(NSString *)name inManagedObjectContext:(NSManagedObjectContext *)context { Photographer *photographer = nil; NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Photographer"]; request.predicate = [NSPredicate predicateWithFormat:@"name = %@", name]; NSSortDescriptor *sortDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"name" ascending:YES]; request.sortDescriptors = [NSArray arrayWithObject:sortDescriptor]; NSError *error = nil; NSArray *photographers = [context executeFetchRequest:request error:&error]; if (!photographers || ([photographers count] > 1)) { // handle error } else if (![photographers count]) { photographer = [NSEntityDescription insertNewObjectForEntityForName:@"Photographer" inManagedObjectContext:context]; photographer.name = name; } else { photographer = [photographers lastObject]; } return photographer; } @end
PhotographersTableViewController.h文件代码:
#import <UIKit/UIKit.h> #import "CoreDataTableViewController.h" // inherits from CoreDataTableViewController to get an NSFetchedResultsController @property // and to get all the copy/pasted code for the NSFetchedResultsController delegate from the documentation @interface PhotographersTableViewController : CoreDataTableViewController @property (nonatomic, strong) UIManagedDocument *photoDatabase; // Model is a Core Data database of photos @end
PhotographersTableViewController.m文件代码:
#import "PhotographersTableViewController.h" #import "FlickrFetcher.h" #import "Photographer.h" #import "Photo+Flickr.h" @implementation PhotographersTableViewController @synthesize photoDatabase = _photoDatabase; // 4. Stub this out (we didn't implement it at first) // 13. Create an NSFetchRequest to get all Photographers and hook it up to our table via an NSFetchedResultsController // (we inherited the code to integrate with NSFRC from CoreDataTableViewController) - (void)setupFetchedResultsController // attaches an NSFetchRequest to this UITableViewController { NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Photographer"]; request.sortDescriptors = [NSArray arrayWithObject:[NSSortDescriptor sortDescriptorWithKey:@"name" ascending:YES selector:@selector(localizedCaseInsensitiveCompare:)]]; // no predicate because we want ALL the Photographers self.fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:request managedObjectContext:self.photoDatabase.managedObjectContext sectionNameKeyPath:nil cacheName:nil]; } // 5. Create a Q to fetch Flickr photo information to seed the database // 6. Take a timeout from this and go create the database model (Photomania.xcdatamodeld) // 7. Create custom subclasses for Photo and Photographer // 8. Create a category on Photo (Photo+Flickr) to add a "factory" method to create a Photo // (go to Photo+Flickr for next step) // 12. Use the Photo+Flickr category method to add Photos to the database (table will auto update due to NSFRC) - (void)fetchFlickrDataIntoDocument:(UIManagedDocument *)document { dispatch_queue_t fetchQ = dispatch_queue_create("Flickr fetcher", NULL); dispatch_async(fetchQ, ^{ NSArray *photos = [FlickrFetcher recentGeoreferencedPhotos]; [document.managedObjectContext performBlock:^{ // perform in the NSMOC's safe thread (main thread) for (NSDictionary *flickrInfo in photos) { [Photo photoWithFlickrInfo:flickrInfo inManagedObjectContext:document.managedObjectContext]; // table will automatically update due to NSFetchedResultsController's observing of the NSMOC } // should probably saveToURL:forSaveOperation:(UIDocumentSaveForOverwriting)completionHandler: here! // we could decide to rely on UIManagedDocument's autosaving, but explicit saving would be better // because if we quit the app before autosave happens, then it'll come up blank next time we run // this is what it would look like (ADDED AFTER LECTURE) ... [document saveToURL:document.fileURL forSaveOperation:UIDocumentSaveForOverwriting completionHandler:NULL]; // note that we don't do anything in the completion handler this time }]; }); dispatch_release(fetchQ); } // 3. Open or create the document here and call setupFetchedResultsController - (void)useDocument { if (![[NSFileManager defaultManager] fileExistsAtPath:[self.photoDatabase.fileURL path]]) { // does not exist on disk, so create it [self.photoDatabase saveToURL:self.photoDatabase.fileURL forSaveOperation:UIDocumentSaveForCreating completionHandler:^(BOOL success) { [self setupFetchedResultsController]; [self fetchFlickrDataIntoDocument:self.photoDatabase]; }]; } else if (self.photoDatabase.documentState == UIDocumentStateClosed) { // exists on disk, but we need to open it [self.photoDatabase openWithCompletionHandler:^(BOOL success) { [self setupFetchedResultsController]; }]; } else if (self.photoDatabase.documentState == UIDocumentStateNormal) { // already open and ready to use [self setupFetchedResultsController]; } } // 2. Make the photoDatabase's setter start using it - (void)setPhotoDatabase:(UIManagedDocument *)photoDatabase { if (_photoDatabase != photoDatabase) { _photoDatabase = photoDatabase; [self useDocument]; } } // 0. Create full storyboard and drag in CDTVC.[mh], FlickrFetcher.[mh] and ImageViewController.[mh] // (0.5 would probably be "add a UIManagedDocument, photoDatabase, as this Controller's Model) // 1. Add code to viewWillAppear: to create a default document (for demo purposes) - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; if (!self.photoDatabase) { // for demo purposes, we'll create a default database if none is set NSURL *url = [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject]; url = [url URLByAppendingPathComponent:@"Default Photo Database"]; // url is now "<Documents Directory>/Default Photo Database" self.photoDatabase = [[UIManagedDocument alloc] initWithFileURL:url]; // setter will create this for us on disk } } // 14. Load up our cell using the NSManagedObject retrieved using NSFRC's objectAtIndexPath: // (go to PhotosByPhotographerViewController.h (header file) for next step) - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *CellIdentifier = @"Photographer Cell"; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if (cell == nil) { cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier]; } // ask NSFetchedResultsController for the NSMO at the row in question Photographer *photographer = [self.fetchedResultsController objectAtIndexPath:indexPath]; // Then configure the cell using it ... cell.textLabel.text = photographer.name; cell.detailTextLabel.text = [NSString stringWithFormat:@"%d photos", [photographer.photos count]]; return cell; } // 19. Support segueing from this table to any view controller that has a photographer @property. - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { NSIndexPath *indexPath = [self.tableView indexPathForCell:sender]; Photographer *photographer = [self.fetchedResultsController objectAtIndexPath:indexPath]; // be somewhat generic here (slightly advanced usage) // we'll segue to ANY view controller that has a photographer @property if ([segue.destinationViewController respondsToSelector:@selector(setPhotographer:)]) { // use performSelector:withObject: to send without compiler checking // (which is acceptable here because we used introspection to be sure this is okay) [segue.destinationViewController performSelector:@selector(setPhotographer:) withObject:photographer]; } } @end
PhotosByPhotographerTableViewController.h文件代码:
#import <UIKit/UIKit.h> #import "Photographer.h" #import "CoreDataTableViewController.h" // inherits from CoreDataTableViewController to get an NSFetchedResultsController @property // and to get all the copy/pasted code for the NSFetchedResultsController delegate from the documentation @interface PhotosByPhotographerTableViewController : CoreDataTableViewController // 15. Added public Model (the photographer whose photos we want to show) @property (nonatomic, strong) Photographer *photographer; @end
PhotosByPhotographerTableViewController.m文件代码:
#import "PhotosByPhotographerTableViewController.h" #import "Photo.h" #import "ImageViewController.h" @implementation PhotosByPhotographerTableViewController @synthesize photographer = _photographer; // 17. Create a fetch request that looks for Photographers with the given name and hook it up through NSFRC // (we inherited the code to integrate with NSFRC from CoreDataTableViewController) - (void)setupFetchedResultsController // attaches an NSFetchRequest to this UITableViewController { NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Photo"]; request.sortDescriptors = [NSArray arrayWithObject:[NSSortDescriptor sortDescriptorWithKey:@"title" ascending:YES selector:@selector(localizedCaseInsensitiveCompare:)]]; request.predicate = [NSPredicate predicateWithFormat:@"whoTook.name = %@", self.photographer.name]; self.fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:request managedObjectContext:self.photographer.managedObjectContext sectionNameKeyPath:nil cacheName:nil]; } // 16. Update our title and set up our NSFRC when our Model is set - (void)setPhotographer:(Photographer *)photographer { _photographer = photographer; self.title = photographer.name; [self setupFetchedResultsController]; } // 18. Load up our cell using the NSManagedObject retrieved using NSFRC's objectAtIndexPath: // (back to PhotographersTableViewController.m for next step, segueing) - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *CellIdentifier = @"Photo Cell"; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if (cell == nil) { cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier]; } // Configure the cell... Photo *photo = [self.fetchedResultsController objectAtIndexPath:indexPath]; // ask NSFRC for the NSMO at the row in question cell.textLabel.text = photo.title; cell.detailTextLabel.text = photo.subtitle; return cell; } // 20. Add segue to show the photo (ADDED AFTER LECTURE) - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender { NSIndexPath *indexPath = [self.tableView indexPathForCell:sender]; Photo *photo = [self.fetchedResultsController objectAtIndexPath:indexPath]; // ask NSFRC for the NSMO at the row in question if ([segue.identifier isEqualToString:@"Show Photo"]) { [segue.destinationViewController setImageURL:[NSURL URLWithString:photo.imageURL]]; [segue.destinationViewController setTitle:photo.title]; } } @end