iPhone Core Data: Your First Steps

来源:互联网 发布:java中排序函数 编辑:程序博客网 时间:2024/05/17 03:29

转自:http://mobile.tutsplus.com/tutorials/iphone/iphone-core-data/


Core Data is a framework Apple provides to developers that is described as a “schema-driven object graph management and persistence framework.” What does that actually mean? The framework manages where data is stored, how it is stored, data caching, and memory management. It was ported to the iPhone from Mac OS X with the 3.0 iPhone SDK release.

In this tutorial I am going to guide you through the process of creating a project with Core Data and show you how to use it with a simple UITableViewController. It will be a simple one table database that will store Lap Times and display the times and their split differences in a UITableView.

Why use Core Data?

Before we get started, it is important to understand why you might want to use Core Data with SQLite for storage over property lists, a custom XML format, or direct SQLite database access. The Core Data API allows developers to create and use a relational database, perform record validation, and perform queries using SQL-less conditions. It essentially allows you to interact with SQLite in Objective-C and not have to worry about connections or managing the database schema, and most of these features are familiar to people who have used object-relational mapping (ORM) technologies like those implemented in Ruby on Rails, CakePHP, LINQ or other libraries and frameworks that abstract access to the database. The main benefit to this approach is that it eliminates the development time otherwise necessary to write complex SQL queries and manually handle the SQL and output of those operations.

Creating a New iPhone project with Core Data

The example application we will be building today is a simple lap timer. It will create a new record in the Core Data store for each lap we make. The UITableView will then show the difference between the current and last laps.

core data lap timer final preview

To get started we will open XCode and create a new project. Name it what you wish, I have named it “LapTimer”. We will be creating a “Window-based application” from the New Project template selection. Make sure “Use Core Data for storage” is checked.

new core data project screen

The project should be familiar to what you may have seen before if you have developed iPhone applications previously.

Setting up The Core Data Structure

There is not a lot of setting up to do as the “Use Core Data for storage” option in the “Window-based Application” template has automatically set some important variables and created files in the project for us.

The LapTimer.xcdatamodel file is where we will define the schema for our SQLite database. The Core Data framework was also added to the project to include the files for API access. The other changes are made within the default application files. Notably, the application delegate files have the methods to setup the Core Data store in our application and to reference it in child UIViewControllers.

What we are interested in at the moment is the LapTimer.xcdatamodel file under “Resources.” This file gives you a visual map of your schema showing entities and attributes.

There are some different terms and phrases used in Core Data that can be loosely related to regular database names, but they are not identical.

An “entity”, also known as a “managed object”, is similar to a table. It is a the definition of an object that will contain a collection of data. An entity object contains “attributes.” These can be loosely associate with columns, but these attributes are not just limited to data storage. Attributes can define a relationship between two entities, a dynamically fetched property, or define a property for data storage.

iphone core data diagram

From the diagram above, you can get a feel for how flexible objects derived from Core Data entities are. For this example application we will need a really simple entity. We will call the entity “Event” which will store records of our laps.

iphone core data model

What we will do to create this entity is click the [+] button in the first (from the left) upper column. This will create a new entity in the list and visually on the schema map below the columns.

iphone core data managed object

This is a nice visual editor for your model. Core Data really does the heavy lifting when it comes to the “M” (Model) part of MVC. The visual editor shows the relationships, properties, and the entities of the store while the schema is dynamically created and managed all for you. This is similar to Interface Builder as it takes care of allocating, managing and placing objects for you on a UIView without a single line of code.

With the new Event entity in place, we want to create a “property.” Since this property will be storing data we will set it as an “attribute.” So this new attribute will just store the current date for when the record was created. In our example application we will use this to reference our lap times.

The next column to the right is where we define our properties (make sure the Event entity is selected). So, create a new property using the [+] button in the column, select “Add Attribute”.

iphone core data attributes

We will call the attribute “timeStamp” and set its Type to “Date”. In the type select drop-down there are similar column data types to those available in relational database systems like PostgreSQL or MySQL, including data types like integers, floats, strings, booleans, dates, and binary data (blobs).

For this attribute, we don’t need any other options selected or changed.

That is it for the xcdatamodel file, and we can move onto integrating it into our app. Now is a good time to save your work.

Creating our model files

If you have used an MVC framework that has database model definitions defining a table’s structure or behaviors, then this will be a familiar task.

We need to start by creating a definition of the entity. We do this by creating a NSManagedObject class of the entity and define the variables the store has.

This is a simple process. Select the Event entity in the LapTimer.xcdatamodel file and go File > New File. You will see that there is a new file template in the “Cocoa Touch Class” section called “Managed Object Class”.

new managed object class

Select the template and hit “Next.” The next screen is just defining where we save the file and the target to include it with, this is all correct by default so hit “Next” again. The next screen is where you define what entities you want to create NSManagedObject classes for. If it isn’t selected, select Event and make sure the “Generate Accessors” and “Generate Obj-C 2.0 properties” checkboxes are selected, we will not need validations at this time. Hit Finish.

managed object class generation

So now we have 2 new files in our application. Event.m and Event.h. They define the NSManagedObject class for the Event entity we created in our Core Data store. They define the timeStamp field so when we want to use the Event class we can access the attribute.

Event.h

view plaincopy to clipboardprint?
  1. #import <CoreData/CoreData.h>   
  2.   
  3. @interface Event :  NSManagedObject    
  4. {   
  5.   
  6. }   
  7.   
  8. @property (nonatomic, retain) NSDate * timeStamp;   
  9.   
  10. @end  

Event.m

view plaincopy to clipboardprint?
  1. #import "Event.h"   
  2.   
  3. @implementation Event    
  4.   
  5. @dynamic timeStamp;   
  6.   
  7. @end  

Like model definitions in other frameworks and languages you can add custom methods for all records in the Event entity. You will notice the timeStamp attribute has been added and assigned as an NSDate object.

Core Data and KVC

With Core Data setup it is now time to work on some of the controller logic in the application. We will use a UITableViewController as the main interface of the app to show the lap times as well as logging a new time.

So, we will create the new UITableViewController with File > New File. Then under the iPhone section select “UIViewController subclass” and check “UITableViewController subclass” but not checking boxes that relate to using XIB’s or for targeting for an iPad. We will not be using a XIB for this controller. Call the new file “TimeTableController” and finish the file wizard.

In this controller we will need 2 properties, a reference to the NSManagedObjectContext and an array to store the records in for the UITableView. As well as defining those properties, we will import the Event.h file so we can use the class.

TimeTableController.h

view plaincopy to clipboardprint?
  1. #import <UIKit/UIKit.h>   
  2.   
  3. #import "Event.h"   
  4.   
  5. @interface TimeTableController : UITableViewController {   
  6.   
  7.     NSManagedObjectContext *managedObjectContext;  
  8.     NSMutableArray *eventArray;   
  9.   
  10. }   
  11.   
  12. @property (nonatomic, retain) NSManagedObjectContext *managedObjectContext;  
  13. @property (nonatomic, retain) NSMutableArray *eventArray;   
  14.   
  15. - (void) fetchRecords;  
  16. - (void) addTime:(id)sender;   
  17.   
  18. @end  

What is an NSManagedObjectContext? It’s referred to as the “scratch pad” for Core Data in the application for managing fetching, updating, and creating records in the store. It also manages a few fundamental features in Core Data including validations and undo/redo management for the records.

The managed object context is the connection between your code and the data store. All of the operations you will execute for Core Data do so against the managed object context. When a request is performed, the managed object context will then talk to the persistent store coordinator which is responsible for mapping the objects to data for the data store. This allows the Core Data to be flexible between different data store formats. Here’s a diagram of how this looks.

iphone core data diagram 2

With the header file defined we need to propagate the allocated properties and methods in the implementation file.

TimeTableController.m

view plaincopy to clipboardprint?
  1. #import "TimeTableController.h"  
  2. @implementation TimeTableController  
  3. @synthesize managedObjectContext, eventArray;   
  4.   
  5. // ...  
  6. // ... default commented code from the file template  
  7. // ...   
  8.   
  9. - (void)viewDidLoad {   
  10.   
  11.     [super viewDidLoad];  
  12.     self.title = @"Lap Times";  
  13.     UIBarButtonItem *addButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAdd target:self action:@selector(addTime:)];  
  14.     self.navigationItem.rightBarButtonItem = addButton;  
  15.     [addButton release];  
  16.     [self fetchRecords];   
  17.   
  18. }   
  19.   
  20. - (void)addTime:(id)sender {   
  21.   
  22.     Event *event = (Event *)[NSEntityDescription insertNewObjectForEntityForName:@"Event" inManagedObjectContext:managedObjectContext];  
  23.     [event setTimeStamp: [NSDate date]];   
  24.   
  25.     NSError *error;  
  26.   
  27.     if(![managedObjectContext save:&error]){  
  28.   
  29.         //This is a serious error saying the record  
  30.         //could not be saved. Advise the user to  
  31.         //try again or restart the application.   
  32.   
  33.     }  
  34.   
  35.     [eventArray insertObject:event atIndex:0];  
  36.   
  37.     [self.tableView reloadData];  
  38.   
  39. }  
  40.   
  41. - (void)fetchRecords {   
  42.   
  43.     // Define our table/entity to use  
  44.     NSEntityDescription *entity = [NSEntityDescription entityForName:@"Event" inManagedObjectContext:managedObjectContext];   
  45.   
  46.     // Setup the fetch request  
  47.     NSFetchRequest *request = [[NSFetchRequest alloc] init];  
  48.     [request setEntity:entity];   
  49.   
  50.     // Define how we will sort the records  
  51.     NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"timeStamp" ascending:NO];  
  52.     NSArray *sortDescriptors = [NSArray arrayWithObject:sortDescriptor];  
  53.     [request setSortDescriptors:sortDescriptors];  
  54.     [sortDescriptor release];   
  55.   
  56.     // Fetch the records and handle an error  
  57.     NSError *error;  
  58.     NSMutableArray *mutableFetchResults = [[managedObjectContext executeFetchRequest:request error:&error] mutableCopy];   
  59.   
  60.     if (!mutableFetchResults) {  
  61.         // Handle the error.  
  62.         // This is a serious error and should advise the user to restart the application  
  63.     }   
  64.   
  65.     // Save our fetched data to an array  
  66.     [self setEventArray: mutableFetchResults];  
  67.     [mutableFetchResults release];  
  68.     [request release];  
  69. }   
  70.   
  71. // ...  
  72. // ... more template comments and default method definitions  
  73. // ...   
  74.   
  75. - (void)dealloc {  
  76.     [managedObjectContext release];  
  77.     [eventArray release];  
  78.     [super dealloc];  
  79. }   
  80.   
  81. @end   

Thats a fair bit of code so lets go through it by each method individually. There is code from when you create the file from the template, commented out methods like viewDidUnload, which I have just left out from the above.

viewDidLoad

We start off with the usual call to the super class. Then we define the UINavigationBar’s title. After that we need to define the button we will use to add a record to the Core Data store. When the button is pressed we tell it to call the selector addTime: which will then interact with Core Data. After the required releases we get to calling the fetchRecords function which is explained below.

addTime:(id)sender

This is fired when the UIBarButtonItem in the upper right of the UINavigationBar is pressed. We need to create a new Event record with the current NSDate and save it to the database.

We create a new Event record which is called an NSEntityDescription. This is your row in the database for the new record. To do this we define the entity name the record belongs to and provide the NSManagedObjectContext. The current date is then set against the timeStamp attribute.

The save operation is then performed, but there is a provision to handle an error if the insert fails. You would usually throw a UIAlertView saying the record did not create, and perhaps instruct the user to try again or close and reopen the application.

The record needs to be added to the array that the UITableView feeds from. Then the UITableView needs to be told to reload the data. You can do this more graphically with an animation but for the sake of the tutorial, let’s keep it simple.

fetchData

This method will get the data from the store and add it to the array we have in the controller. For further detail on getting records let’s have a close look at NSFetchRequest.

Fetching data from the data store

Core Data has a different approach to fetching data from its database. It is a NoSQL data store, meaning all the conditions of a query are based upon methods. This is great as the base store, which is SQLite, could be changed to any other database technology and the only thing to change would be the connection and drivers for the database.

So, to create a request we create an NSFetchRequest object. This is the base object that the query conditions will be set against. We can define conditions for matching a particular property based on how records are ordered. You can find out more about Core Data and its conditions for NSFetchRequests in their documentation.

Creating an new NSFetch request is simple. You just need to define the entity you want records from and the NSManagedObjectContext.

view plaincopy to clipboardprint?
  1. NSEntityDescription *entity = [NSEntityDescription entityForName:@"Event" inManagedObjectContext:managedObjectContext];   
  2.   
  3. NSFetchRequest *request = [[NSFetchRequest alloc] init];   
  4.   
  5. [request setEntity:entity];  

The entity is defined using an NSEntityDescription object which requires the entity name and the NSManagedObjectContext. The fetch request is then created passing the entity description. This would equate to the first part of a SQL statement:

  1. SELECT * FROM `events`  

In our example application we sort the data by the timeStamp in a descending manner. To do this, we use a NSSortDescriptor.

view plaincopy to clipboardprint?
  1. NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"timeStamp" ascending:NO];   
  2.   
  3. NSArray *sortDescriptors = [NSArray arrayWithObject:sortDescriptor];   
  4.   
  5. [request setSortDescriptors:sortDescriptors];   
  6.   
  7. [sortDescriptor release];  

The NSSortDescriptor is created and we define the attribute we wish to sort and wether it is ascending, in this case we want it to descend so it is set to NO. The fetch request can take many sort descriptors so it accepts an array when setting the sort descriptors. Since we only want one, we just need to create an array with one object in it. We set the descriptor array against the fetch request and that is that.

For defining a condition to match a records contents the NSPredicate class comes into play. It allows the fetch request match or define a range that the contents of a record must meet. This is the equivalent to your equals, greater and less than matches in SQL. It does have more than your basic SQL functions,which you can see here.

Setting a predicate can be very simple.

view plaincopy to clipboardprint?
  1. NSPredicate *predicate = [NSPredicate predicateWithFormat:@"(lastName like %@) AND (birthday > %@)", lastNameSearchString, birthdaySearchDate];  

Using NSPredicate predicateWithFormat: is a simple and familiar method that allows you to define the conditions of the query. For an in depth explanation on NSPredicates the Apple Documentation has some great guides.

When you have defined the conditions in your fetch request you can then execute it.

  1. NSMutableArray *mutableFetchResults = [[managedObjectContext executeFetchRequest:request error:&error] mutableCopy];  

That will return an array of entity objects, NSManagedObjects, to use in your data output.

Populating the UITableView from Core Data

With the data fetched from Core Data and stored in the eventArray we can now output those records into the UITableView.

First thing is to tell the table that we will only require 1 section and how many rows we have to use.

Excerpt From TimeTableController.m

view plaincopy to clipboardprint?
  1. -(NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {  
  2.     return 1;  
  3. }   
  4.   
  5. -(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {  
  6.     return [eventArray count];  
  7. }   

If you have used a UITableViewController before, the next function should be straight forward.

view plaincopy to clipboardprint?
  1. -(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {   
  2.   
  3. static NSString *CellIdentifier = @"Cell";  
  4. static NSDateFormatter *dateFormatter = nil;   
  5.   
  6. if (dateFormatter == nil) {  
  7.     dateFormatter = [[NSDateFormatter alloc] init];  
  8.     [dateFormatter setDateFormat:@"h:mm.ss a"];  
  9. }  
  10.   
  11. UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];   
  12.   
  13. if (cell == nil) {  
  14.     cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:CellIdentifier] autorelease];  
  15. }   
  16.   
  17. Event *event = [eventArray objectAtIndex: [indexPath row]];  
  18. Event *previousEvent = nil;   
  19.   
  20. if ([eventArray count] > ([indexPath row] + 1)) {  
  21.     previousEvent = [eventArray objectAtIndex: ([indexPath row] + 1)];  
  22. }  
  23.     
  
  24. [cell.textLabel setText: [dateFormatter stringFromDate: [event timeStamp]]];   
  25.   
  26. if (previousEvent) {  
  27.     NSTimeInterval timeDifference = [[event timeStamp] timeIntervalSinceDate: [previousEvent timeStamp]];  
  28.     [cell.detailTextLabel setText: [NSString stringWithFormat:@"+%.02f sec", timeDifference]];  
  29. else {  
  30.     [cell.detailTextLabel setText: @"---"];  
  31. }   
  32.   
  33. return cell;   
  34.   
  35. }  

The cell will display 2 values, from using the UITableViewCellStyleValue1 style. The left will be the time of the lap and the right will be the difference in seconds from the previous record.

Since this method iterates we need to take special care with the load it can put the device under if it isn’t managed correctly. For that reason the NSDatFormatter is stored as a static variable so it can be reused in each iteration without allocating and releasing it every time.

Lazy Loading

Lazy Loading is a technique where you delay the request or allocation of a property as much as possible. This helps keep the memory down and during iterative functions this is paramount. Knowing when and how to allocate data is crucial to keeping a mobile app speedy. Deallocating the object is also as important, the earlier the better.

cellForRowAtIndexPath: is an iterative method and any data processed or allocated in this method especially needs to be kept at a minimum. This method is run whenever a cell comes into view so when a user is scrolling quickly this particular method, depending on the record set size, can be called very frequently in succession.

The next task is to get the event object associated with the table row that needs to be rendered. Since we need to get the previous record for the time comparison there is a simple check to see if there is a previous record and to store it into previousEvent. If the previousEvent exists then we calculate the split using [NSDate timeIntervalSinceDate: (NSDate)]. The textLabel and detailedTextLabel are then set with the values we have calculated.

Finishing the application

With the UITableViewController setup and the table data source working with the Core Data store all that is needed is to load the controller when the application is started.

In the Application controller a UINavigationController property needs to be defined. Then the applicationDidFinishLaunching method just needs to allocate the controller and we are done.

LapTimerAppDelegate.h

  1. @interface LapTimerAppDelegate : NSObject <UIApplicationDelegate> {  
  2.     NSManagedObjectModel *managedObjectModel;  
  3.     NSManagedObjectContext *managedObjectContext;      
  4.     NSPersistentStoreCoordinator *persistentStoreCoordinator;  
  5.     UIWindow *window;  
  6.     UINavigationController *navigationController;  
  7. }   
  8.   
  9. @property (nonatomic, retain, readonly) NSManagedObjectModel *managedObjectModel;  
  10. @property (nonatomic, retain, readonly) NSManagedObjectContext *managedObjectContext;  
  11. @property (nonatomic, retain, readonly) NSPersistentStoreCoordinator *persistentStoreCoordinator;  
  12. @property (nonatomic, retain) IBOutlet UIWindow *window;  
  13. @property (nonatomic, retain) UINavigationController *navigationController;   
  14.   
  15. - (NSString *)applicationDocumentsDirectory;   
  16.   
  17. @end   

Excerpt from LapTimerAppDelegate.m

view plaincopy to clipboardprint?
  1. #import "LapTimerAppDelegate.h"  
  2. #import "TimeTableController.h"   
  3.   
  4. @implementation LapTimerAppDelegate  
  5. @synthesize window, navigationController;   
  6.   
  7. - (void)applicationDidFinishLaunching:(UIApplication *)application {      
  8.     TimeTableController *tableController = [[TimeTableController alloc] initWithStyle:UITableViewStylePlain]; tableController.managedObjectContext = [self managedObjectContext];  
  9.     self.navigationController = [[UINavigationController alloc] initWithRootViewController:tableController];  
  10.     [tableController release];  
  11.     [window addSubview: [self.navigationController view]];  
  12.     [window makeKeyAndVisible];  
  13. }  
  14.   
  15. // ...  
  16. // ... other template methods  
  17. // ...   
  18.   
  19. - (void)dealloc {  
  20.     [managedObjectContext release];  
  21.     [managedObjectModel release];  
  22.     [persistentStoreCoordinator release];  
  23.     [window release];  
  24.     [navigationController release];  
  25.     [super dealloc];  
  26. }  

The TimeTableController.h file is included into the application delegate and then allocated with a UINavigationController.

That should be it. Build the application to check for errors. Some of the code examples have only been extracts, none of the code that is generated when creating a file has been removed, only filled in. If you run into errors you can’t crack, you can download the project file attached to this tutorial which you can then compile and compare.

Run the application. You will see the navigation controller and the add button. Press the add button and you will get a new time in the table.

Recap

In this tutorial we have created an example application to store simple data into a Core Data store. The application ran through the initial setup procedure when creating an application with Core Data, defining the data structure and fetching records from the data store.

Hopefully you have gotten an introduction to Core Data and can see how easy it is to use and how it can improve your applications performance and functionality.

If you want to find out more about Core Data or want a detailed look at the framework’s structure then the Apple Developer Documentation is the perfect place to go.

Resources

Apple Developer Documentation:

Introduction to Core Data Programming

Core Data Migration and Versioning

NSPredicate Programming Guide: