iOS應用開發視頻教程筆記(十四)Core Data Demo


這節課的主要內容是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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM