IOS開發入門(13)-Core Data介紹

IOS開發入門(13)-Core Data介紹

前言

對於很多程序,外觀對於其價值只是一個關鍵部分。沒有數據,就沒有什麼可以顯示的。而對於許多程序來說,數據必須持久話。在CarValet應用程序中,到目前爲止數據都是臨時的,重新運行,數據就會消失。我們需要一種方式來在應用程序多次運行之間保存數據

iOS提供的許多方法可以實現這一點:可以將對象原封不動保存到一個文件裏,將對象及其關係翻譯爲XML並且存儲到另一個文件中,甚至創建自己的SQLite數據庫。無論選擇哪兒種方式,都需要編寫大量的子程序,包括保存一輛汽車、讀取一輛汽車、查找一輛汽車、刪除一輛汽車,以及更多。使用數據庫,一些基本操作將被處理,但仍然有大量工作要做:我們需要定義SQL數據模型,設計並編寫SQL查詢,甚至更多。

所以這裏我們開始介紹Core Data,他爲我們做了大量的工作,方便了不少

介紹Core Data

Core Data是蘋果公司用於將應用程序模型進行持久化的一項技術。它具有多層關係數據庫的全部能力,還添加了對iOS和Mac OS的用戶體驗的集成環境。儘管Core Data威力強大,但開始利用它只需要做相對很少的工作。(本章只是簡單的使用了一下Core Data,想真正利用Core Data還請自行查閱資料,如果我有資料的話會發在我的電子資源存放地,當然裏面還有其他的東西,想要的話可以免費下載

在更加詳細地介紹Core Data之前,理解如何在典型的應用程序中運用它是非常重要的。對於大多數應用程序,蘋果公司推薦一種整體架構,名爲Model-View-Controller(MVC)。

實現類被拆分爲三個功能區域。在MVC中,模型完全是關於應用程序數據的:創建、變更、刪除和修改。所有這些行爲被封裝到獨立於用戶界面的類中。做得好的話,任何與模型相關的操作都可以在命令行或圖形視圖中進行。外觀取決於視圖層。

視圖層是應用程序的可視化用戶體驗。它包含所有有訪問和修改數據相關的東西,以及任何其他的可視化應用程序元素,例如偏好設置。其關注點是外觀和感覺。需要顯示的信息來自於模型。視圖層解決信息如何展示的問題。

控制器位於視圖和模型之間。它解決從協調應用程序行爲到控制動作流等各種事情,充當模型和視圖的中間人。控制器通常是應用程序中最複雜的部分。除了位於模型和用戶體驗之間的控制器,還可能是其他控制器,用於與操作系統交互、處理通信,以及完成很多其他事情。如果應用程序針對打印或文本有不同的視圖層,那麼還會有針對那些特殊視圖的其他控制器。

Core Data專注於MVC的模型層,還有一些部分可提供對用戶界面控制器的支持。和在大多數數據庫中一樣,Core Data可以被分解爲三個主要的層次:數據存放在哪裏,數據的格式,以及數據訪問環境。存儲區是保存的數據被放置的地方,並且可能存在不止一個存儲區。數據格式有managed object model(託管對象模型)定義。數據通過managed object context(託管對象上下文)來訪問,並且同時可以有不止一個context處於活躍狀態。

以下是相關的類:

  • NSpersistentStoreCoordinator協調用於存儲數據的所有存儲區。對於iOS,設備上通常只有一個存儲區。
  • NSManagedObjectModel描述應用程序中使用的所有類型的數據對象。
  • NSManagedObjectContext是一組數據對象的管理器。這個上下文包含一些規則,用戶存儲區中找到的實際數據元素,一些已找到的數據元素以及機器的當前狀態。多個上下文同時處於活躍狀態是可能的,小童的對象在每個上下文中有不同的狀態

將CarValet應用程序遷移到Core Data

理解Core Data的最好方法就是使用。

首先向應用程序中添加Core Data框架。執行以下步驟

  • 打開CarValet應用程序,在Xcode Navigator的頂部選擇這個項目,打開CarValet應用程序的目標編輯區域,查找Editor區域(在當前這種情況下,是Project Editpr)的Target,並選擇CarValet應用程序target(Targets列表中應當只有一個元素
    這裏寫圖片描述
  • 保持這個目標被選中,確保General標籤在Project Editor的Project Details區域的頂部顯示。現在向下滾動Project Details區域,直到Linked Frameworks and Liaries,並且使用+按鈕添加Core Data框架。
    這裏寫圖片描述

添加CDCar模型

下一步,我們需要創建Core Data使用的文件以創建實體及其其他配置。同樣的描述文件和編輯器可以用於從CarValet應用程序這樣簡單的單實體配置文件,到包含多個實體、實體屬性、默認值以及關係非常複雜的模型的任何項目

添加汽車模型只需要完成以下幾個步驟:

  • 使用Core Data分類的Data Mode模板向項目中添加一個新文件。將此文件稱左CarValet並將其剛好添加到Car.h的上面
    這裏寫圖片描述
  • 打開新的CarValet.xdatamodeld文件,如果他還沒有顯示的話。應當能看到與下圖類似的編輯器
    這裏寫圖片描述
  • 通過單機左下方的Add Entity按鈕添加名爲CDCar的實體
    這裏寫圖片描述
  • 爲Car對象已有的每個屬性都添加一個特性(attribute),除了carInfo。可以在Assistant編輯器中打開Car.h文件,並確保得到所有屬性。當添加每個屬性時,按照當前模型設置器類型

類型不明顯的屬性只有year。可以將其定義爲16位整數,因爲年份在很長時間內不可能比32767還大,而不管選用的是哪種日曆格式。當完成之後,這些屬性如下圖所示
這裏寫圖片描述
原始Car數據對象可以在init中設置一些默認值。Core Data可讓我們完成同樣的事情,儘管在某些方面不太靈活。使用Data Model檢查器將1908設置爲年份的默認值。如下圖所示
這裏寫圖片描述
既然CDCar實體已經被定義,就可以創建NSManagedObject的一個子類來使用點表示法訪問這些屬性。唯一需要注意的地方是,屬性默認爲NSObject子類。這意味着任何整數、浮點數、布爾值等都是NSNumber。儘管即將執行的一步就能讓我們使用標量,如NSInteger來表示原始類型,但還是有一些不想要的結果。在當前這種情況,Core Data將NSData轉換爲NSTimeInterval

保持Model編輯器仍打開,選擇Editor|Create NSManagedObject Subclass,並且如果有提示的話,選擇CDCar,然後將這個文件與其他所有源代碼文件一起保存。
這裏要注意的有兩個地方:
這裏寫圖片描述
這裏寫圖片描述

添加Core Data樣板代碼

在能夠使用任何數據之前,需要爲應用程序初始化Core Data。需要爲下圖所示的所有三個類創建對象。

我們使用的是基於Xcode的Master-Detail Application模板。唯一修改就是添加對CoreData.h的導入,以及兩個#define
這裏寫圖片描述
步驟如下:(解釋在代碼中)

  • 修改AppDelegate.h文件:
#import <UIKit/UIKit.h>

#import <CoreData/CoreData.h>

#define MyModelURLFile          @"CarValet"//這是我們剛纔創建的CoreData模型文件的名稱
#define MySQLDataFileName       @"CarValet.sqlite"//這是由持久化存儲區域協調器管理的存儲區的名稱,並且是個SQLite數據庫


@interface AppDelegate : UIResponder <UIApplicationDelegate>

@property (strong, nonatomic) UIWindow *window;

@property (readonly, strong, nonatomic) NSManagedObjectContext
*managedObjectContext;
@property (readonly, strong, nonatomic) NSManagedObjectModel
*managedObjectModel;
@property (readonly, strong, nonatomic) NSPersistentStoreCoordinator
*persistentStoreCoordinator;


- (void)saveContext;
- (NSURL *)applicationDocumentsDirectory;


@end
  • 修改AppDelegate.m文件:
#import "AppDelegate.h"
#import "AboutViewController.h"

@interface AppDelegate ()

@end

@implementation AppDelegate
//3條@synthesize爲.h文件中的只讀屬性提供讀/寫權限
@synthesize managedObjectContext = _managedObjectContext;
@synthesize managedObjectModel = _managedObjectModel;
@synthesize persistentStoreCoordinator = _persistentStoreCoordinator;

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // Override point for customization after application launch.
    UIColor *Mocha = [UIColor colorWithDisplayP3Red:128.0/255 green:64.0/255.0 blue:0/255.0 alpha:1.0];//1

    [[UIButton appearance]setTitleColor:Mocha forState:UIControlStateNormal];//2
    [[UIBarButtonItem appearance] setTintColor:Mocha];

    UITabBarController *tabBarController = (UITabBarController*)self.window.rootViewController;//1
    //2
    AboutViewController *aboutViewController = [[AboutViewController alloc]initWithNibName:@"AboutViewController"
                                                                                    bundle:[NSBundle mainBundle]];
    UITabBarItem *aboutItem = [[UITabBarItem alloc]initWithTitle:@"About" //3
                                                           image:[UIImage imageNamed:@"tag"]
                                                             tag:0];
    [aboutViewController setTabBarItem:aboutItem];//4
    NSMutableArray *currentItems = [NSMutableArray arrayWithArray:tabBarController.viewControllers];//5
    [currentItems addObject:aboutViewController];//6
    [tabBarController setViewControllers:currentItems animated:NO];//7
    return YES;
}



- (void)applicationDidEnterBackground:(UIApplication *)application {
    // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
    // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
}


- (void)applicationWillEnterForeground:(UIApplication *)application {
    // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
}


- (void)applicationDidBecomeActive:(UIApplication *)application {
    // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
}


- (void)applicationWillResignActive:(UIApplication *)application
{
    // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
    // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game.
    [self saveContext];
}

- (void)applicationWillTerminate:(UIApplication *)application
{
    // Saves changes in the application's managed object context before the application terminates.
    [self saveContext];
}

- (void)saveContext
{
    NSError *error = nil;
    NSManagedObjectContext *managedObjectContext = self.managedObjectContext;
    if (managedObjectContext != nil) {
        if ([managedObjectContext hasChanges] && ![managedObjectContext save:&error]) {
            // Replace this implementation with code to handle the error appropriately.
            // abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
            NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
            abort();
        }
    }
}

#pragma mark - Core Data stack

// Returns the managed object context for the application.
// If the context doesn't already exist, it is created and bound to the persistent store coordinator for the application.
- (NSManagedObjectContext *)managedObjectContext
{
    if (_managedObjectContext != nil) {
        return _managedObjectContext;
    }

    NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
    if (coordinator != nil) {
        _managedObjectContext = [[NSManagedObjectContext alloc] init];
        [_managedObjectContext setPersistentStoreCoordinator:coordinator];
    }
    return _managedObjectContext;
}

// Returns the managed object model for the application.
// If the model doesn't already exist, it is created from the application's model.
- (NSManagedObjectModel *)managedObjectModel
{
    if (_managedObjectModel != nil) {
        return _managedObjectModel;
    }
    NSURL *modelURL = [[NSBundle mainBundle] URLForResource:MyModelURLFile withExtension:@"momd"];
    _managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];
    return _managedObjectModel;
}

// Returns the persistent store coordinator for the application.
// If the coordinator doesn't already exist, it is created and the application's store added to it.
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator
{
    if (_persistentStoreCoordinator != nil) {
        return _persistentStoreCoordinator;
    }

    NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:MySQLDataFileName];

    NSError *error = nil;
    _persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
    if (![_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:&error]) {
        /*
         Replace this implementation with code to handle the error appropriately.

         abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.

         Typical reasons for an error here include:
         * The persistent store is not accessible;
         * The schema for the persistent store is incompatible with current managed object model.
         Check the error message to determine what the actual problem was.


         If the persistent store is not accessible, there is typically something wrong with the file path. Often, a file URL is pointing into the application's resources directory instead of a writeable directory.

         If you encounter schema incompatibility errors during development, you can reduce their frequency by:
         * Simply deleting the existing store:
         [[NSFileManager defaultManager] removeItemAtURL:storeURL error:nil]

         * Performing automatic lightweight migration by passing the following dictionary as the options parameter:
         @{NSMigratePersistentStoresAutomaticallyOption:@YES, NSInferMappingModelAutomaticallyOption:@YES}

         Lightweight migration will only work for a limited set of schema changes; consult "Core Data Model Versioning and Data Migration Programming Guide" for details.

         */
        NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
        abort();
    }

    return _persistentStoreCoordinator;
}

#pragma mark - Application's Documents directory

// Returns the URL to the application's Documents directory.
- (NSURL *)applicationDocumentsDirectory
{
    return [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
}


@end

當現在運行程序的時候,它看上去沒有什麼不同。這是因爲我們所做的所有事情就是定義模型並且添加代碼以準備使用Core Data,還沒有任何用於創建和顯示CDCar的代碼

轉換CarTableViewController

使用Core Data可以改變數據訪問、創建、刪除和更新的方式。要想轉換應用程序的每個部分,我們需要識別這些區別並添加或修改相關代碼。下表顯示了這些區別以及需要對CarTableViewController所做的修改

修改 爲Core Data所做的修改 需要修改的代碼
1 設置託管對象上下文 在viewDidLoad中設置指向主要託管對象上下文的變量
2 通過託管對象上下文訪問數據對象 設置數據獲取請求,從託管對象上下文獲得恰當的實體數據
3 讓Core Data處理汽車的添加和刪除 使用Core Data調用來添加和刪除汽車。使用這些調用的結果更新arrayOfCars
4 使用新的託管數據對象執行數據訪問 使用CDCar替換Car
5 從原始類型改爲對象 將依賴interger和float數據類型的地方修改爲使用NSNumber

1. 修改汽車表視圖修改:添加託管對象上下文

第一處修改需要一個對託管對象上下文的引用。AppDelegate已經有了一個,因此汽車表視圖控制器蘇需要的只是一個引用。由於託管對象上下文在許多方法中使用,因此需要創建一個實例變量,儘管property並不是必須的。步驟如下:

  • 將AppDelegate.h和CDCar+CoreDataProperties.h導入到CarTableViewController.m中,並且刪除Car.h的導入語句
  • 將如下聲明添加到@implementation下面的花括號中的arrayOfCars的下邊,創建一個實例變量:
NSManagedObjectContext *managedContextObject;
  • 將以下代碼添加到viewDidLoad方法中,恰好在調用super的語句下面的位置,從而設置託管對象上下文:
AppDelegate *appDelegate = [[UIApplication shareApplication] delegate];

managedContextObject = appDelegatemanagedObjectContext;

2.汽車表視圖修改:使用託管對象上下文訪問數據

下一步,我們需要將數組的內容設置爲當前的CDCar、對象。這需要做一系列修改。首先,從託管對象上下文獲取數據,這需求在抓取請求中描述這些數據。可以是簡單地指定一種特定實體類型的所有對象,也可以是非常複雜的過濾和排序

抓取請求被用做executeFetchRequest:error:的一部分,這是個用於獲取數據對象數組的NSManagedObjectContext方法。該方法還可被用於當數據有變化時獲得更新過的數據對象集合,變化包括添加或刪除等。由於汽車數組在汽車表視圖控制器的多個方法中被更新,因此執行以下步驟:

  • 將arrayOfCars的聲明從NSMutableArray改爲NSArray。與此同時,在managedObjectContext:的下邊添加如下代碼來聲明抓取請求:
NSFetchRequest *fetchRequest;
  • 將viewDidLoad方法更新爲下面代碼,有加有刪
-- (void)viewDidLoad {
    [super viewDidLoad];
    AppDelegate *appDelegate = [[UIApplication sharedApplication]delegate];

    managedContextObject = appDelegate.managedObjectContext;
    NSError *error=nil;
    fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"CDCar"];//1 創建查找類CDCar的所有實體的抓取請求。其他方法和屬性可用於添加過濾、排序,甚至批量加載對象
    arrayOfCars = [managedContextObject executeFetchRequest:fetchRequest error:&error];//2將汽車數組設置爲上下文中所有符合抓取請求標準的託管對象——在此處爲所有汽車

    if (error != nil) {
        NSLog(@"Unresolved error %@, %@", error, [error userInfo]); //3 讀取對象時出錯。將日誌打印到文件對學習游泳。但更好的辦法是如果個能的話,盡力從錯誤中恢復;如果無法恢復的話,將當前情況通知用戶並告訴用戶可以嘗試的解決辦法
        abort();//4對abourt()的調用來自系統提供的模板。它所做的所有事情就是創建崩潰日誌並且終止程序
    }

    self.navigationItem.leftBarButtonItem = self.editButton;

//    arrayOfCars = [NSMutableArray new];
//    
//    [self newCar:nil];
//    self.navigationItem.leftBarButtonItem = self.editButton;

}

3.汽車表示圖修改:使用Car Data添加和刪除汽車

當前的汽車添加方式簡單分配了一個新的Car對象並且將其插入到可變數組中。Car對象負責設置任何必須的初始值。刪除僅僅是從數組中刪除對Car對象的調用。使用可變數組假設內存從不耗盡,錯誤從不發生,並且回話之間的狀態保存由別的代碼實現。

使用Core Data帶來了持久化、錯誤檢查,以及更好的內存管理。此外還有一些額外的工作,但主要是錯誤檢查。要想對添加汽車的代碼做出修改,將newCar:方法代碼替換爲下面的代碼:

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {

    return [arrayOfCars count];
}

- (IBAction)newCar:(id)sender {
    NSLog(@"here");
    CDCar *newCar=[NSEntityDescription insertNewObjectForEntityForName:@"CDCar"
                                                inManagedObjectContext:managedContextObject];//1 在當前的託管對象上下文中創建新的汽車對象

    newCar.dateCreated = [NSDate date];//2初始化汽車的創建日期

    NSError *error;
    arrayOfCars = [managedContextObject executeFetchRequest:fetchRequest
                                                      error:&error];//3重新生成當前的汽車數組,已包含新的汽車對象

    if(error != nil) {
        NSLog(@"Unresolved error %@, %@",error,[error userInfo]);
        abort();
    }



//    Car *newCar = [Car new];
//    
//    [arrayOfCars insertObject:newCar atIndex:0];//1 將汽車插入到數組的前邊
    NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:0];//2 創建一個NSindexPath對象來指定新單元格的位置——他的section和row

    [self.tableView insertRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];//3 讓表視圖在新的索引路徑上插入一個對象。這回事表視圖調用數據源來查找第0表格段第0行的數據——也就是數組的第一個元素。因爲數組已經被更新,所以新的單元格會被返回
//    //[self.tableView reloadData];
}

刪除對象只需要對託管對象上下文的方法調用,那樣就會將那個對象標記爲已刪除,儘管直到保存上下文時纔會從存儲區刪除它

使用下面代碼修改tableView:commitEditingSyle:forRowAtIndexPath:中的刪除條件分支

- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {

    if (editingStyle == UITableViewCellEditingStyleDelete) {
        // Delete the row from the data source
        [managedContextObject deleteObject:arrayOfCars[indexPath.row]];//1讓託管對象上下文來刪除CDCar對象
        NSError *error = nil;
        arrayOfCars = [managedContextObject executeFetchRequest:fetchRequest error:&error];

        if(error != nil) {
            NSLog(@"Unresolved error %@, %@",error,[error userInfo]);
            abort();
        }
    } //else if (editingStyle == UITableViewCellEditingStyleInsert) {
        // Create a new instance of the appropriate class, insert it into the array, and add a new row to the table view
    //}
    [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade];
}

4.汽車表視圖修改:改爲CDCar並跟新汽車單元格

我們所需要做的全部事情,即使切換到新的CDCar對象類。有三個視圖控制器使用了Car對象:CarTableViewController、CarTableViewCell和ViewCarTableViewController

對於ViewCarTableViewController和CarTableViewCell:

  • 在.h文件中,在@class聲明中和myCar屬性中將Car修改爲CDCar
  • 在.m文件中,將Car.h的導入語句修改爲導入CDCar+CoreDataProperties.h

現在修改carToView:的返回值類型:

  • 在ViewCarProtocol.h中,將@class聲明改爲使用CDCar,並將返回值類型修改爲CDCar
  • 在CarTableViewController.m中,將carToView的返回值類型修改爲CDCar。這只需要修改方法的聲明行:
- (CDCar*)carToView{

5.汽車表視圖修改:更新爲使用NSNumber

我們用的iOS版本是10.1,所以這裏直接就能編譯過了,而且運行也是正確的。self.myCar.year本身就是整數,不需要再額外的[self.myCar.year integerValue]轉換了

現在可以運行程序了。我們一開始看起來不知道哪裏發生變化了。可以嘗試一下添加新的汽車,然後cmd+shift+H,返回主屏幕。然後重新運行一下程序,你會發現,原來的數據並沒有隨着程序的重啓而消失,依然保存下來。到這裏,說明我們現在已經成功的將CarValet應用程序遷移到了Core Data上了

更加簡單的表視圖:NSFetchedResultsController

到目前爲止,在將CarValet應用程序轉換爲使用Core Data後,我們已經使用一個修改了的汽車數組版本來管理表視圖。每一次數據有修改,一個新的數組就會產生。目前這之所以可行,是由於沒有很多的數據(我們之前是每次更新數據,就創建一個新的數組,即arrayOfCars每次都新創建)。然而,當汽車數量變得更大時這樣式不可行的。當有太多數據對象時,最好的情況是我們會遇到性能問題。更有可能會遇到內存問題

看起來應該寫更少的代碼。畢竟,如果託管對象上下文可以生成數據,那麼它必須擁有關於總共多少啓辰機器排列順序的信息。此時,我們擁有了計算表格段數據以及表格段中數、特定索引路徑中有哪兒些數據元素,以及基於數據的修改而更新表格的代碼。

其實這些代碼都不用寫,因爲系統提供了NSFetchResultsController及其關聯的協議。它們的設計目的就是通過如下步驟使得概覽基於Core Data的數據對象的表視圖變得易於管理:

  • 配置表格中的段數與行數
  • 得到索引路徑中的數據條目
  • 返回表格段頭標題
  • 跟蹤託管對象上下文中的修改,並且允許使用委託協議來執行表格更新
  • 批量獲取數據,並且可選擇將數據緩存到文件中來提升性能

第一部分:集成NSFetchedResultsController

我們需要一個實例變量來存放抓取結果控制器。將如下聲明添加到CarTableViewController.m頂部@implementation語句後面的fetchRequest聲明之下:

NSFetchedResultsController *fetchedResultsController;

現在需要初始化抓取結果控制器。這樣做需要創建 正確的抓取請求。這是由於MSFetchedResultsController將應用程序託管對象上下文抓取請求的結果映射到了索引路徑。

抓取結果控制器至少需要一個過濾器和一個排序。過濾器可以是nil,但是排序必須至少指定一個key。

抓取結果可以具有可選的批處理大小,儘管對於如此少量的數據沒有必要。對於更大的數據集,批處理大小限制了每次讀取多少數據元素,因此也限制了內存中的元素數量。

處理代碼如下:

- (void)viewDidLoad {
    [super viewDidLoad];

    AppDelegate *appDelegate = [[UIApplication sharedApplication] delegate];

    managedContextObject = appDelegate.managedObjectContext;
    fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"CDCar"];

    NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc]
                                        initWithKey:@"dateCreated"
                                        ascending:NO];//1在創建時創建一條簡單的排序規則,將最新的數據顯示在頂部

    [fetchRequest setSortDescriptors:@[sortDescriptor]];//2將抓取請求的愛須描述符設置爲這條新的排序規則。注意必須將排序描述符設置爲描述符數組,即使只有一個。這就是這條語句使用@[]數組字面構造器的原因
    fetchedResultsController = [[NSFetchedResultsController alloc]//3從應用程序委託中,初始化使用剛剛分配的抓取請求結果控制器以及託管對象上下文。只有一個表格段,因此不需要表格段名稱。也沒有緩存
                                initWithFetchRequest:fetchRequest
                                managedObjectContext:managedContextObject
                                sectionNameKeyPath:nil
                                cacheName:nil];

    NSError *error=nil;
    [fetchedResultsController performFetch:&error];//4讓控制器讀取初始數據集,並且處理仍可能發生的錯誤

    if (error!=nil) {
        NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
        abort();
    }
    self.navigationItem.leftBarButtonItem = self.editButton;
}

1更新基本表視圖數據源方法

UITableViewDataSource實現了用於返回表格段數目、返回給定表格段中的表格行數,以及返回給定索引路徑的單元格的三個核心方法。這些方法中的每一個現在都可以使用抓取結果控制器。執行以下步驟來更新這些方法。

  • numberOfSectionsInTableView:改爲如下代碼
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {

    //return 1;
    return [[fetchedResultsController sections]count];
}
  • tableView:numberOfRowsInSection:改爲如下代碼
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {

    //return [arrayOfCars count];
    id <NSFetchedResultsSectionInfo> sectionInfo;
    sectionInfo = [fetchedResultsController sections][section];
    return [sectionInfo numberOfObjects];
}
  • tableView:cellForRowAtIndexPath:中設置單元格中汽車對象的代碼行修改如下:
 cell.myCar = [fetchedResultsController objectAtIndexPath:indexPath];

2更新、刪除以及查看汽車

擋在模擬器中運行這個程序時,我們可以看到已經有汽車了。然而,當我們嘗試添加或者刪除汽車的時候,就會導致程序的崩潰。這是因爲支持這些行爲的方法仍然使用的是舊的基於數組的管理方法。所以我們需要更新一下代碼。按照以下步驟來使用抓取控制器

  • tableView:commitEditingStyle:forRowAtIndexPath:方法中:
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {

    if (editingStyle == UITableViewCellEditingStyleDelete) {
        [managedContextObject deleteObject:[fetchedResultsController objectAtIndexPath:indexPath]];
        NSError *error = nil;
        [fetchedResultsController performFetch:&error];
        if(error != nil) {
            NSLog(@"Unresolved error %@, %@",error,[error userInfo]);
            abort();
        }
    }
    [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade];
}
  • newCar:中,將生成arrayOfCars的代碼替換如下
[fetchedResultsController performFetch:&error];
  • carToView的返回值替換如下
return [fetchedResultsController objectAtIndexPath:currentViewCarPath];
  • 刪除arrayOfCars實例變量的聲明

現在程序能夠正常運行了

第2部分:實現NSFetchedResultsControllerDelegate

代碼能夠正常運行,但是還有一個潛在的問題。每一次通過添加、刪除或修改來更新託管對象上下文的數據時,都會想抓取結果控制器發送performFetch:消息。這個調用可能比僅僅更新修改了的數據需要更多的工作和時間。

抓取結果控制器有能力觀察託管對象上下文中的修改,並且當這些變化發生時調用一些方法。我們只需要支持NSFetchedResultsControllerDelegate協議

  • 打開CarTableVIewController.h,將下列代碼添加進去
#import <CoreData/CoreData.h>
  • 添加協議
<ViewCarProtocol,NSFetchedResultsControllerDelegate>

1.添加NSFetchedResultsControllerDelegate方法

  • 通過viewDidLoad中初始化fetchResultsController的代碼行的後面添加如下代碼,將當前的CarTableViewController實例設置爲抓取結果控制器委託:
fetchedResultsController.delegate = self;
  • 在carToView的上面添加如下代碼,這個#pragma被用於快速找到新的協議支持
#pragma mark - NSFetchedResultsControllerDelegate

- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
    [self.tableView beginUpdates];
}
  • 現在通知表視圖更新已完成並應當更新,在上面的代碼下方添加如下代碼:
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
    [self.tableView endUpdates];
}
  • 通過添加如下代碼來添加基於修改類型完成更新表格段這一工作的協議方法。
- (void)controller:(NSFetchedResultsController *)controller
   didChangeObject:(id)anObject
       atIndexPath:(nullable NSIndexPath *)indexPath
     forChangeType:(NSFetchedResultsChangeType)type
      newIndexPath:(nullable NSIndexPath *)newIndexPath {

    UITableView *tableView = self.tableView;

    switch (type) {//1基於修改的類型,確定需要做哪一種更新
        case NSFetchedResultsChangeInsert://2檔新對象插入之後,在表格中的恰當位置插入一個單元格
            [tableView insertRowsAtIndexPaths:@[newIndexPath]
                             withRowAnimation:UITableViewRowAnimationFade];
            break;
        case NSFetchedResultsChangeDelete://3一個對象被刪除,因此刪除相應的單元格
            [tableView deleteRowsAtIndexPaths:@[indexPath]
                             withRowAnimation:UITableViewRowAnimationFade];
            break;
//        case NSFetchedResultsChangeUpdate://4這類修改在對象被修改或更新時發生。在此處,我們需要做刷新表示數據的單元格所需要做的一切事情
//            code to update the content of the cell at indexPath
//            break;
//        case NSFetchedResultsChangeMove://5最後一處修改是將數據單元格從表格中的一個地方一道另一個地方。通常這意味着刪除舊的單元格並插入一個新的
//            [tableView deleteRowsAtIndexPaths:@[indexPath]
//                             withRowAnimation:UITableViewRowAnimationFade];
//            [tableView insertRowsAtIndexPaths:@[newIndexPath]
//                             withRowAnimation:UITableViewRowAnimationFade];
//            break;

        default:
            break;
    }
}

2.啓用委託方法調用

如果現在運行CarValet應用程序,委託方法並沒有被調用。可以通過添加NSLog語句或者在controllerWillChangeContent中設置斷電,然後添加或刪除汽車來看到這一點。表格會被更新,但並不是由於委託方法。

觀察者僅在託管對象上下文被保存時纔會別通知。保存發生的唯一地方就是在應用程序委託中,當應用程序進入後臺或退出時。這些事件會調用自定義方法saveContext,它會向所有修改了的託管對象上下文發送save消息。

要觸發委託方法,需要在添加或刪除汽車時保存上下文。tableViewcommitEditingStyle:forRowAtIndexPath:刪除一輛汽車,而newCar添加一輛。在這些方法中將[fetchedResultsController performFetch:&error];替換成[managedContextObject save:&error];

save:消息會觸發委託消息流:初始的controllerWillChangeContent消息,所需數量的Controller:diddChangeObjectPath:forChangeType:newIndexPath:消息,然後是controllerDidChangeContent:消息。

現在運行程序,添加數據或刪除數據時崩潰跳出,在調試器輸出中發現錯誤原因如下:

Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid update: invalid number of rows in section 0.  The number of rows contained in an existing section after the update (8) must be equal to the number of rows contained in that section before the update (8), plus or minus the number of rows inserted or deleted from that section (1 inserted, 0 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out).
----來自谷歌翻譯----
由於未被捕獲的異常'NSInternalInconsistencyException'終止應用程序,原因:'無效更新:第0節中的行數無效。更新(8)後現有部分中包含的行數必須等於該列中包含的行數 更新前的部分(8),加或減從該部分插入或刪除的行數(1插入,0刪除),加或減移動到該部分的行數(0移動,0移動 出)。

這個異常告訴我們,表視圖期望的行數與實際的行數不匹配,者之所以說得通,是由於原始的代碼和新的委託方法都更新了表格。有兩處代碼都對桶以對象執行了添加和刪除操作,要修改這個錯誤,步驟如下:

  • 在tableViews:commitEditingStyle:forRowAtIndexPath:中刪除對deleteRowsAtIndexPaths:withRowAnimation:的調動,就是下面那段代碼
[tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade];
  • 從newCar:中刪除最後兩行:
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:0];
[self.tableView insertRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];

運行程序,ok,完美運行咯。

今天的介紹就到這裏咯

我的另一個博客站點:Arnold-你們好啊

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章