iOS应用开发视频教程笔记(十三)Core Data

转自: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


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值