上一篇文章我們從一個小例子講解了Core Data的使用方式,其實盡管概念很多,但實際應用的時候大多數情況下我們不需要接觸太多的類和方法,只要拿到context並且操作即可。
這次我們看看iOS自帶的一個小例子,原文參見:Core Data Tutorial for iOS。由於上一次我們已經講述了基本概念和實現方法,這次我的描述會簡練一些,不再一步一步詳細講解。
本次目標是創建一個應用程序,可以記錄每次你保存的經緯度坐標,並且可以對這些坐標(我們保存為一個Event實體)進行編輯。
我的演示環境是XCode 4.6.3,所以有些地方和原文是不同的,這裡都已我自己的環境為准。
建立工程
步驟
- 創建一個Empty Application,起名叫Locations,選擇Devices為iPhone,並且使用ARC;
- 添加CoreLocation.framework;
- 添加一個Storyboard文件,並在工程屬性中選擇Main Storyboard為這個文件;
- 至此,操作步驟完成。
對工程的理解
以上步驟完成後,我們的工程中多了很多文件,我們一一講解如下:
- 應用的代理類AppDelegate(.h和.m文件);
- 一個.xib界面描述文件;
- 一個Core Data的模型文件.xcdatamodelId;
- 這個工程還引用了CoreLocation.framework;
我們還能看到對於AppDelegate.h,會自動生成相關Core Data的處理:
[cpp] view plaincopy
- @property (readonly, strong, nonatomic) NSManagedObjectContext *managedObjectContext;
- @property (readonly, strong, nonatomic) NSManagedObjectModel *managedObjectModel;
- @property (readonly, strong, nonatomic) NSPersistentStoreCoordinator *persistentStoreCoordinator;
- - (void)saveContext;
- - (NSURL *)applicationDocumentsDirectory;
就像方法暗指的那樣,applicationDocumentsDirectory方法返回的是app的文檔路徑,數據將被保存在那裡。
Core Data Stack
這裡的stack指的是Core Data framework中一系列對象的集合,這些對象協同工作,將模型從文件中取出,或者寫入到文件中。從類比的角度說,Core Data可以被看為一個數據庫,保存了記錄和表(Core Data中的數據有可能真的被保存在數據庫中,例如SQLite,但是這個不是一定的)。一個最簡單和最頻繁使用的Core Data Stack結構如下(可以和上一篇文章印證):
其中你最常用的是Managed Object Context和其包含的Managed Object對象。
Managed Object和Managed Object Context
Managed Object是一個NSManagedObject或者他的子類的對象。他代表了在Store中保存的一個對象的記錄,所以他是一個由Core Data管理的模型。Managed Object對象管理應用中使用的數據,例如:一個人力資源管理系統中的部門和雇員信息;一個繪圖程序中的形狀、文本和分組信息;一個音樂程序中的音軌信息。一個Managed Object始終和一個Managed Object Context相關聯。
Managed Object Context是一個NSManagedObjectContext類的實例。一個context代表了一個應用中與數據有關的內存區域。context的主要職責是用來管理managed object對象。Context對象是你的應用的控制中樞,負責生命周期管理到驗證、Managed Object間的關系維護、重做和取消等功能。
當你新建一個Managed Object的時候,首先要將其放入Context中。你將對象取回(fetch)的時候,同樣將其存入context對象。所有的操作都在內存中,一直到你通過commit提交你的修改為止。
下面的圖演示了記錄(record)和對象(object)的關系,注意有個Nigel對象沒有保存,所以Store中的salary仍然是50000,另外有兩個對象未納入context管理。
Managed Object Model
Managed Object Model是一個NSManagedObjectModel類的實例,他描述了數據庫的Schema,所以你的數據能夠保存到Store中。一個模型(model)是一個實體對象模型(NSEntityDescription)的集合。一個實體描述根據實體的名字描述一個實體,類名,及其屬性。
下圖描述了模型的實體描述、數據庫中表和Managed Object中單個對象的關系:
每個Managed Object都有對應的實體描述。
Core Data使用模型來在應用的managed object和數據庫的記錄中描述映射關系。所以一定要對改變模型(描述)小心處理。Core Data無法使用你之前的模型來使用現在的數據。(實際上需要通過版本管理來處理這種變更,而不是完全不能處理)
Persistent Store Coordinator
PSC扮演的角色是告訴Core Data如何管理數據。大多數時候你完全不用關心這個對象。
一個PSC是NSPersistentStoreCoordinator類的一個實例,他管理了一系列的persistent object store。一個persistent object stores代表一個需要序列化的外部文件或數據庫。實際上Core Data支持多種持久化數據類型,甚至你可以聲明自己的數據文件類型。
在iOS中,一般你僅有一個store,但在復雜的應用(例如OSX)中你可能有多個保存實體的store,所以PSC需要來管理這些store,當你需要取回數據時,PSC將取回所有結果。
下圖模擬了coordinator的工作角色,實際情況通常不會這麼復雜:
Table View Controller
這一小節主要實現Table View Controller相關功能。下面我們一步一步來執行以下步驟,:
- 在Storyboard中創建一個Table View,並將其嵌入到Navigation Controller中;
- 添加兩個按鈕到Navigation Bar上面,並且將左邊按鈕的Identifier改為Edit,右邊Identifier修改為Add;
- 添加一個UITableViewController的子類,名字為LocationsListViewController;
- 添加對位置信息的支持,所以LocationsListViewController的頭文件變為:
[cpp] view plaincopy
- #import <UIKit/UIKit.h>
- #import <CoreLocation/CoreLocation.h>
- @interface LocationsListViewController : UITableViewController <CLLocationManagerDelegate>
- @property (nonatomic, retain) NSMutableArray *eventsArray;
- @property (nonatomic, retain) NSManagedObjectContext *managedObjectContext;
- @property (nonatomic, retain) CLLocationManager *locationManager;
- @property (nonatomic, retain) UIBarButtonItem *addButton;
- @end
- 實現文件中對於location獲取相關代碼實現如下:
[cpp] view plaincopy
- @implementation LocationsListViewController
- @synthesize eventsArray;
- @synthesize locationManager;
- @synthesize managedObjectContext;
- @synthesize addButton;
- - (CLLocationManager *)locationManager {
- if (locationManager != nil) {
- return locationManager;
- }
- locationManager = [[CLLocationManager alloc] init];
- locationManager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters;
- locationManager.delegate = self;
- return locationManager;
- }
- - (void)locationManager:(CLLocationManager *)manager
- didUpdateToLocation:(CLLocation *)newLocation
- fromLocation:(CLLocation *)oldLocation {
- addButton.enabled = YES;
- }
- - (void)locationManager:(CLLocationManager *)manager
- didFailWithError:(NSError *)error {
- addButton.enabled = NO;
- }
- - (id)initWithStyle:(UITableViewStyle)style
- {
- self = [super initWithStyle:style];
- if (self) {
- // Custom initialization
- }
- return self;
- }
- - (void)viewDidLoad
- {
- [super viewDidLoad];
- // Set the title.
- self.title = @"Locations";
- // Set up the buttons.
- self.navigationItem.leftBarButtonItem = self.editButtonItem;
- addButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAdd
- target:self action:@selector(addEvent)];
- addButton.enabled = NO;
- self.navigationItem.rightBarButtonItem = addButton;
- // Start the location manager.
- [[self locationManager] startUpdatingLocation];
- }
這裡剩下addEvent未實現。
- 接著我們去AppDelegate類,將didFinishLaunchingWithOptions修改為:
[cpp] view plaincopy
- - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
- {
- return YES;
- }
- 至此,本階段工作完成,運行程序顯示如下界面:
Managed Object and Model
這一部分比較簡單,步驟如下:
- 打開Locations.xcdatamodeld文件,Add Entity,名字叫Event;
- 分別添加屬性如下圖:
- 創建自定義的Managed Object類。原則上你可以直接使用NSManagedObject,但實際上大部分情況下都會使用他的子類,好處如下:
- 從開發工具獲得更好的支持。例如屬性存取方法的自動完成、編譯時類型和符號的檢測等;
- 支持實體的自定義方法。大多數情況下你希望實體提供特殊的邏輯,例如自定義的驗證、或者衍生的屬性等。例如,一個Person實體的hasDriversLicense屬性在age屬性不到17時不可能是true(所以會有一個校驗),或者一個fullName方法返回的是firstName和lastName的一個合適的拼接。
- 我們選擇Event這個Entity,然後新建一個文件,選擇Core Data分類中的NSManagedObject subclass。
- 我們可以看到自動生成的代碼中包含了Event這個實體映射的域模型了。
Adding Events
這一小節的目標是添加事件並且將其顯示在ListView中。
- 在story board中將我們的list view和我們之前創建的LocationsListViewController相關聯;
- 在LocationsListViewController中添加方法:
[cpp] view plaincopy
- - (void)addEvent {
- // Get the Current Location
- CLLocation *location = [locationManager location];
- if (!location) {
- return;
- }
- // Create and configure a new instance of the Event entity.
- Event *event = (Event *)[NSEntityDescription insertNewObjectForEntityForName:@"Event" inManagedObjectContext:managedObjectContext];
- CLLocationCoordinate2D coordinate = [location coordinate];
- [event setLatitude:[NSNumber numberWithDouble:coordinate.latitude]];
- [event setLongitude:[NSNumber numberWithDouble:coordinate.longitude]];
- [event setCreationDate:[NSDate date]];
- // Save the New Event
- NSError *error = nil;
- if (![managedObjectContext save:&error]) {
- // Handle the error.
- NSLog(@"Save Event failed.");
- return ;
- }
- // Update the Events Array and the Table View
- [eventsArray insertObject:event atIndex:0];
- NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:0];
- [self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:indexPath]
- withRowAnimation:UITableViewRowAnimationFade];
- [self.tableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0] atScrollPosition:UITableViewScrollPositionTop animated:YES];
- }
- 其余涉及到Table View的顯示代碼如下(注意設置Table View的Cell的Identifier為Cell,區分大小寫;同時將Cell的Style設置為Right Detail):
[cpp] view plaincopy
- - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
- {
- // Return the number of sections.
- return 1;
- }
- - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
- {
- return [eventsArray count];
- }
- - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
- {
- // A date formatter for the time stamp.
- static NSDateFormatter *dateFormatter = nil;
- if (dateFormatter == nil) {
- dateFormatter = [[NSDateFormatter alloc] init];
- [dateFormatter setTimeStyle:NSDateFormatterMediumStyle];
- [dateFormatter setDateStyle:NSDateFormatterMediumStyle];
- }
- // A number formatter for the latitude and longitude.
- static NSNumberFormatter *numberFormatter = nil;
- if (numberFormatter == nil) {
- numberFormatter = [[NSNumberFormatter alloc] init];
- [numberFormatter setNumberStyle:NSNumberFormatterDecimalStyle];
- [numberFormatter setMaximumFractionDigits:3];
- }
- static NSString *CellIdentifier = @"Cell";
- // Dequeue or create a new cell.
- UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
- if (cell == nil) {
- cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier];
- }
- Event *event = (Event *)[eventsArray objectAtIndex:indexPath.row];
- cell.textLabel.text = [dateFormatter stringFromDate:[event creationDate]];
- NSString *string = [NSString stringWithFormat:@"%@, %@",
- [numberFormatter stringFromNumber:[event latitude]],
- [numberFormatter stringFromNumber:[event longitude]]];
- cell.detailTextLabel.text = string;
- return cell;
- }
- 還要實現一個getManagedObjectContext的方法:
[cpp] view plaincopy
- - (NSManagedObjectContext *)getManagedObjectContext {
- NSManagedObjectContext *context = nil;
- id delegate = [[UIApplication sharedApplication] delegate];
- if ([delegate performSelector:@selector(managedObjectContext)]) {
- context = [delegate managedObjectContext];
- }
- return context;
- }
- 最後在viewDidLoad方法最末尾加上eventArray和context的初始化代碼:
[cpp] view plaincopy
- eventsArray = [[NSMutableArray alloc] init];
- managedObjectContext = self.getManagedObjectContext;
- 運行程序,添加event看看效果。
Fetching Events
獲取數據的步驟比較簡單:
- 創建請求;
- 設置排序條件(可選);
- 執行查詢;
- 將數據放到我們准備好的(可變)數組中;
具體代碼如下,我們需要創建一個loadData方法,然後在viewDidLoad中執行loadData:
[cpp] view plaincopy
- -(void)loadData
- {
- // Create the Request
- NSFetchRequest *request = [[NSFetchRequest alloc] init];
- NSEntityDescription *entity = [NSEntityDescription entityForName:@"Event" inManagedObjectContext:managedObjectContext];
- [request setEntity:entity];
- // Set the Sort Descriptor
- NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"creationDate" ascending:NO];
- NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor, nil];
- [request setSortDescriptors:sortDescriptors];
- // Execute the Request
- NSError *error = nil;
- NSMutableArray *mutableFetchResults = [[managedObjectContext executeFetchRequest:request error:&error] mutableCopy];
- if (mutableFetchResults == nil) {
- // Handle the error.
- NSLog(@"error loading data");
- }
- // Finish Up
- [self setEventsArray:mutableFetchResults];
- }
Deleting Events
刪除操作主要是覆蓋commitEditingStyle方法,刪除Object、刪除界面中的顯示,代碼如下:
[cpp] view plaincopy
- - (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
- {
- if (editingStyle == UITableViewCellEditingStyleDelete) {
- // Delete the managed object at the given index path.
- NSManagedObject *eventToDelete = [eventsArray objectAtIndex:indexPath.row];
- [managedObjectContext deleteObject:eventToDelete];
- // Update the array and table view.
- [eventsArray removeObjectAtIndex:indexPath.row];
- [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:YES];
- // Commit the change.
- NSError *error = nil;
- if (![managedObjectContext save:&error]) {
- // Handle the error.
- }
- }
- 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
- }
- }
至此,這個例子完成,運行即可查看效果。