在XCODE中开发Iphone/Ipad 使用sqlite的Data Core框架.

来源:互联网 发布:debian centos 编辑:程序博客网 时间:2024/05/29 17:49
最近接触到IPAD上的应用开发. 所以就在思考是否在XCODE中也会有ORM关系型框架呢. 在国内本身开发APP应用的就很少了,再加上GM的问题就更难找到资料了,终于在国外找到一篇文章来分享给大家.大家可以下载那个DEMO看看.  大家别问我问题,因为我自己还搞懂是怎么个情况. 

 

The majority of Core Data sample code is for either single-window applications or single-document-window applications. There are, however, times when an application or document requires more than one window on its data. When starting out learning to write code with Core Data, I spent a great deal of time trying to find a tutorial that covered multi-window applications; aside from Apple’s CoreRecipes application (which I found rather frightening to disassemble), there is very little reference information ‘out there’.

In this article I aim to show the way I code multi-window Cocoa data apps. I am by no means trying to say how all such code should be written and I definitely don’t claim that what I am currently doing is absolutely the best way to do things. Hopefully the article may help someone else who is beginning to write a multi-window Core Data application. Please accept my apologies if it is written at a level below your own knowledge; on the other hand if it assumes too much or you don’t understand something, feel free to contact me and ask.

- Edit – The example project for this post is available to download from here:coredatamultiwin.zip.

Before we start with the theory, let’s have a look at what we’re going to create. This sample application is a very simple example of a company ‘people records’ database application.

Screenshot of the finished application

The main screen displays a list of departments alongside a list of the people in a selected department; you can also click a button to view the details of a selected person in a new window. Clearly this particular application would be perfectly fine as a single-window application and I have deliberately made it as simple as possible. It doesn’t take much imagination, though, to see how this could be extended, making multi-window display essential. I have spent a large amount of time with commercial Fundraising database products like The Raiser’s Edge, and in these it is often necessary to view multiple records at the same time. You might, for example, need to look at individual screens for a husband and wife to see how much money they have donated as a couple, or perhaps have two different company records on screen to compare their history.

When working with multiple windows and Core Data, the key is to understand the primary concepts of the framework. When I started trying to write a Core Data application, it was great to see the walk-through tutorials that involve hardly any coding but this did lead to a slight frustration that I hadn’t got quite the right understanding of the underlying foundation to expand the given examples in the way I wanted. Understanding exactly what a Managed Object Context is and how it deals with the data held in a Persistent Store is very important to using multiple windows with Core Data. I will try to give more information about the core concepts as we move through the article.

I’ll assume that if you’re reading this, you know at least the basics of creating Cocoa and specifically Core Data apps so I won’t provide too many screenshots of that sort of task! Using Xcode to generate a new Core Data Document-based application produces a project with, amongst other things, a MyDocument object inheriting fromNSPersistentDocument. NSPersistentDocument is similar toNSDocument in that it handles opening and saving files but adds built-in Core Data functionality for working with data in XML, Binary and SQLite formats. It is this document object (as with a traditionalNSDocument object) that keeps track of the windows specific to an open document, keeping them separate from any general application-related windows that are handled by the application object or its delegate. To work with multiple windows in one document, we need to change the standard document object to work with multiple window controllers.

A window controller instance (of NSWindowController) acts as the control code for a window that is displayed on screen. Typically, you subclassNSWindowController for each different type of window that you display (in our example application, these would be the window that displays the lists of departments and people and the window that displays details of a single person). You then need to create an actual instance of an NSWindowController or its subclass for every single window that is displayed on screen.

First Steps

If you haven’t already, go ahead and create a new ‘Core Data Document-based application.’ By default, itsNSPersistentDocument object called MyDocument is set to work with only one window that is taken from a nib file called MyDocument.nib. If you open the ‘MyDocument.m’ file, you will see that it uses the methodwindowNibName: to get the name of the nib file for the window and then opens the window contained within the nib, after which it callswindowControllerDidLoadNib:.

An NSDocument/NSPersistentDocument has two possible ways to open its document windows. When a new document object is created, if implementation code exists for thewindowNibName: method, that method will be called and a single window will be created for use as the document’s window. If this code is not implemented, the document object will instead try to call itsmakeWindowControllers: method.

A document object maintains its own array of NSWindowController instances — ie an array of singleNSWindowController or subclass instances for each window that is currently open for the document. When using themakeWindowControllers: method, opening a new window for a document requires that you allocate some memory to a window controller instance, initialize it as with any other object, and then add it to the document object’s array of window controllers. Each window can also be specified to be important enough to close the document object (and therefore any other open document windows) if it is itself closed. In our example application, the main document should be closed when the window that displays the list of people is closed but not when any individual windows of a person’s details are closed.

The Code

For now, we are going to change the MyDocument.m file to use makeWindowControllers: rather thanwindowNibName:.

Delete the windowNibName: method and replace it with this:

- (void)makeWindowControllers{NSWindowController *mainWindowController = [[NSWindowController alloc] initWithWindowNibName:@"MyDocument" owner:self][mainWindowController setShouldCloseDocument:YES];[self addWindowController:mainWindowController];}

The code is fairly self-explanatory. It creates a generic NSWindowController instance using the MyDocument.nib file, specifies that it should close the document when the window is closed, and then adds the controller instance to the document object’s window controller array.

Build and run the application and as the default behaviour is to open a new, untitled document, our standard document window will open in exactly the same way as if you’d left the originalwindowNibName: code in MyDocument.m as it was when created. Choosing ‘File -> New’ will show a new window with a title to match that of the new and unsaved document (‘Untitled 2′, for example).

The Model

To create the data model for our sample application, open up MyDocument.xcdatamodel in Xcode.

  • Make a new entity called Department and give it a string attribute calledname.
  • Make a new entity called Person and give it string attributes forfirstName and lastName.
  • In the Department entity, create a to-many relationship employees with destinationPerson.
  • In the Person entity, create a to-one relationship to the Department entity and call it department.
  • Make the department relationship the inverse of the employees relationship.

My data model looks like this:
The sample data model

The Interface

We’re going to set up the interface manually rather than use the ‘option-drag from model to window’ approach so go ahead and open the MyDocument.nib file in Interface Builder.

  • Create a new NSArrayController, specify its Mode as ‘Entity’ and set the Entity Name to ‘Department’. It’s helpful to name array controllers to avoid confusion when using Cocoa Bindings so use the Identity Inspector tab to set the name to ‘Departments’.
  • Create a second NSArrayController for the ‘Person’ entity and name it ‘People’.
  • For both array controllers make sure the ‘Prepares Content’ flag is ticked. This flag tells the array controller to fetch data when it is instantiated from the nib file at runtime rather than waiting until you tell it to do so in code somewhere.
  • In the ‘Bindings’ Inspector tab, bind the ‘Managed Object Context’ to the ‘File’s Owner’ with Model Key Path ‘managedObjectContext’ for both array controllers.
  • For the ‘People’ array controller, bind the ‘Content Set’ to the ‘Departments’ ‘selection’ with Model Key Path ‘employees’.

Open the Window and delete the existing content label. Add two NSTableViews (one with one column and one with two) and some buttons to look like the interface below:
The layout of the main application window

  • Bind the Value of the single column of the left-hand table view to the ‘Departments’ array controller’s ‘arrangedObjects’ ‘name’ Model Key Path.
  • Bind the Values of the two columns of the other table view to the ‘People’ array controller’s ‘arrangedObjects’ ‘firstName’ and ‘lastName’ Model Key Path.
  • Control-drag from the ‘+’ and ‘-’ buttons to the ‘Departments’ and ‘People’ array controllers’ ‘add:’ and ‘remove:’ received actions respectively.
  • Set the ‘Title’ of each table view column to match its content – ie ‘Department’, ‘First Name’ and ‘Last Name’.
  • Set the Selection mode of each table view to ‘Multiple’.

Go back to XCode and build and run the project. Everything should function as expected — you should be able to add and remove departments as expected, with employees being inserted into the currently selected department (and if no department is selected, the ‘+’ button under the employees table does nothing), and also Undo your actions.
The main application window in use

The interface might not be terribly pleasing but it is reasonable enough for this example application. If you wish, disable the ‘+’ and ‘-’ buttons when these actions are disabled by theirNSArrayController — easily accomplished by binding the ‘Enabled’ attribute on each button to the relevant array controller’s ‘canAdd’ and ‘canRemove’ keys.

Moving On

Now for a little theory. When you were making the array controllers in our sample application, you bound their Managed Object Context to the File’s Owner. It’s worth at this point saying a little about Managed Object Contexts before we continue with developing our application. If you are happy that you understand how Managed Object Contexts relate to Core Data, feel free to skip the rest of this section.

Managed Object Contexts

Apple documentation says you should think of a Managed Object Context as a ‘scratchpad’ for data. It also helps me to think of a Managed Object Context behaving like a cloud, hovering over the ‘persistent’ stored data. In a Core Data application, persistent data is stored in a ‘Persistent Store’. For this article it’s not necessary to go to much into Persistent Stores or their Co-ordinators but it is important to understand the keywordpersistent. In Core Data, the data that is ‘persistent’ is the actual data which is physically stored on disc — for example when you choose to save a document in our sample application, the entries you have made in the Departments and People tableviews are made persistent and stored to disc. What then, are you seeing on screen before you save to a persistent store? Well, the data you are editing is being held in a ‘temporary’ place — in our Managed Object Context (MOC for short from now on…).

When you open a document in our sample application, the NSPersistentDocument objectMyDocument automatically creates an empty NSManagedObjectContext object. As the main window is instantiated from the nib file, the DepartmentsNSArrayController asks its MOC to load any persistent data for the Entity that you have specified, in this case the ‘Department’ records. Core Data is all about Managed Objects (hence the Managed Object Context) and these ‘records’, or instances of the ‘Department’ entity, are instances of NSManagedObject (or a custom subclass ofNSManagedObject). If the current document is a new document, there are obviously no pre-existing persistent managed objects for the MOC to load.

Once the window appears on screen, you might add a department to the Departments array controller; the array controller tells its MOC to create a new managed object using the ‘Department’ entity specification, which you are then able to edit. It’s only when you Save the document that the MOC ‘rains down’ its data into the Persistent Store. You are therefore able to make multiple changes to managed objects, add new objects or remove existing ones, but nothing affects the data stored on disc until you actually tell the MOC to persist its data by telling the document to Save.

Let’s consider a case where you have previously saved a document with two Department records. When you open the document, the Departments array controller asks the MOC for any managed objects with the ‘Department’ entity specification. The MOC then asks the Persistent Store for these objects and retrieves them into its memory (in fact, Core Data uses a system of ‘faults’ to load data only when needed but I won’t go into this here). It remembers which objects in its memory relate to existing persistent objects such that when you tell the MOC to persist any changes, it updates the values in the persistent store. Imagine that you delete one of the two existing departments and add a new one — until you save, the data on screen at this point is that held only in the MOC. If you close the document without saving (or your application crashes…), the MOC will be deallocated from memory without updating the persistent store and your original document will remain unaffected.

The MOC is also what provides Undo support ‘for free’ in a Core Data application. When you choose ‘Edit -> Undo’, the MOC for that window is asked to step back through its recent activity. Again, nothing changes in the persistent store until you Save.

As a trick, go back and change the MyDocument makeWindowControllers: method to open a second window thus:

- (void)makeWindowControllers{NSWindowController *mainWindowController = [[NSWindowController alloc] initWithWindowNibName:@"MyDocument" owner:self][mainWindowController setShouldCloseDocument:TRUE];[self addWindowController:mainWindowController]NSWindowController *secondWindowController = [[NSWindowController alloc] initWithWindowNibName:@"MyDocument" owner:self];[self addWindowController:secondWindowController];}

When you run the application, two windows will appear. Both will have the same MOC so if you add a Department in one window, it will also appear in the list in the other and vice-versa. And, at the risk of being punched in the face for repetition, the persistent store isn’t affected until you Save the document.

Make sure you change the makeWindowControllers: method back to its original ‘one window’ implementation before proceeding.

A New Window Controller Subclass

Let’s move on now to create the window that will display the information for individual employees in our application. In the existing nib file, the File’s Owner is set to be theMyDocument object (by the use of [...initWithWindowNibName: owner:self] in themakeWindowControllers: method) and it is this that provides the array controllers with their Managed Object Context (remember anNSPersistentDocument object automatically creates one managedObjectContext for your use). But, for our new person window, in order to keep track of which person that window represents we’re going to have to subclassNSWindowController; the File’s Owner of this new window nib will no longer beMyDocument but our new subclass. In order to be able to provide a MOC to the objects in the new nib file, we will have to keep a reference to it in our subclass, and be able to specify this when the window controller instance is created.

The Code

  • In Xcode, create a new ‘Objective-C NSWindowController subclass’ file called ‘PersonWindowController.m’.
  • In the PersonWindowController.h interface, add an NSManagedObjectContext pointer called_moc.
  • Add KVC methods to access this attribute using managedObjectContext: andsetManagedObjectContext:, together with an initialization method.

Your code should look like this:

@interface PersonWindowController : NSWindowController { NSManagedObjectContext *_moc; } - (void)setManagedObjectContext:(NSManagedObjectContext *)value;- (NSManagedObjectContext *)managedObjectContext; - (PersonWindowController *)initWithManagedObjectContext:(NSManagedObjectContext *)inMoc; @end

The initialization method will be used to set up a PersonWindowController instance with the specified MOC.

In the PersonWindowController.m file, add implementations for the methods thus:

@implementation PersonWindowController - (PersonWindowController *)initWithManagedObjectContext:(NSManagedObjectContext *)inMoc{self = [super initWithWindowNibName:@"PersonWindow"][self setManagedObjectContext:inMoc]return self;} - (void)setManagedObjectContext:(NSManagedObjectContext *)value{// keep only weak ref_moc = value;} - (NSManagedObjectContext *)managedObjectContext{return _moc;} @end

Next we need to have code to open our new PersonWindows on demand:

  • In MyDocument.h, add declarations for an Interface Builder Action method:
    - (IBAction)openPersonWindow:(id)sender;
  • In MyDocument.m, be sure to import your new PersonWindowController.h header file.
  • Add a method implementation for openPersonWindow: thus:
    - (IBAction)openPersonWindow:(id)sender{PersonWindowController *newWindowController = [[PersonWindowController alloc] initWithManagedObjectContext:[self managedObjectContext]][newWindowController setShouldCloseDocument:NO];[self addWindowController:newWindowController];[newWindowController showWindow:sender];}

The Interface

Now that we have a suitable window controller for our new window, we need to create a nib file for it to use:

  • Create a new Interface Builder Window nib file called ‘PersonWindow.nib’ and open it in Interface Builder.
  • Select the ‘File’s Owner’ icon and in the Identity Inspector set its Class to ‘PersonWindowController’.
  • Right click on the File’s Owner icon and set it’s Window outlet to the Window in the nib file. (This is a step I have often forgotten and then spent several hours trying to understand why my window didn’t appear!)

Next we need to make a button to open the new window:

  • In MyDocument.nib, open the main window.
  • Add an NSButton to the window called ‘Open Employee Window’.
  • Set the button action to the newly created openPersonWindow: method of File’s Owner.

Build and run the application. Each time you click on the ‘Open Employee Window’ button, a new, blank window will open. This window might not be particularly impressive as it doesn’t display any data, but at least it’s a start! Just to reassure yourself that the new window is a document window, make sure that closing the main document window will also close any associated employee windows.

- Edit – Handling enabling and disabling of the ‘Open Employee Window’ button is a little more complicated than the ‘+’ and ‘-’ buttons. The hack way would be to bind its ‘Enabled’ attribute to the People array controller’s ‘canRemove’ key since canRemove is only true if a person is currently selected. The better way to do this, however, is described in a great post on this article at:http://www.passingcuriosity.com/.

Keeping track of the Person object

In order to display our employee data in the new window, we obviously need to be able to tell ourpersonWindowController which employee it should display.

So, in the PersonWindowController.h file, add an NSManagedObject pointer called_person to the interface. Then add KVC methods to access it called person and setPerson: and change the initialization method so your code looks like this:

@interface PersonWindowController : NSWindowController { NSManagedObjectContext *_moc;NSManagedObject *_person;} - (void)setManagedObjectContext:(NSManagedObjectContext *)value;- (NSManagedObjectContext *)managedObjectContext; - (void)setPerson:(NSManagedObject *)value;- (NSManagedObject *)person; - (PersonWindowController *)initWithPerson:(NSManagedObject *)inPerson inManagedObjectContext:(NSManagedObjectContext *)inMoc; @end

The initialization method has been modified to allow us to initialize with a specified person object. Change the method implementation in PersonWindowController.m and add the_person getter and setter:

- (PersonWindowController *)initWithPerson:(NSManagedObject *)inPerson inManagedObjectContext:(NSManagedObjectContext *)inMoc{self = [super initWithWindowNibName:@"PersonWindow"][self setManagedObjectContext:inMoc];[self setPerson:inPerson]return self;} - (void)setPerson:(NSManagedObject *)value{// keep only weak ref_person = value;} - (NSManagedObject *)person{return _person;}

To know which Person object we need to pass to the window we’re opening, we need to be able to ask the ‘People’ array controller which object is currently selected. So, in MyDocument.h, add to theMyDocument interface an IBOutlet for the NSArrayController:

@interface MyDocument : NSPersistentDocument { IBOutlet NSArrayController *peopleArrayController; }

Return to Interface Builder and connect this new outlet to the People array controller.

In MyDocument.m, change openPersonWindow: to the following:

- (IBAction)openPersonWindow:(id)sender{NSArray *selectedPeople = [peopleArrayController selectedObjects];NSManagedObject *selectedPerson; int i;for( i = 0; i < [selectedPeople count]; i++ ){selectedPerson = [[peopleArrayController selectedObjects] objectAtIndex:i]; PersonWindowController *newWindowController = [[PersonWindowController alloc] initWithPerson:selectedPerson inManagedObjectContext:[self managedObjectContext]][newWindowController setShouldCloseDocument:NO];[self addWindowController:newWindowController];[newWindowController showWindow:sender];}}

This code asks the peopleArrayController for the currently selected objects. It then moves through these opening a newPersonWindowController instance for each selected person.

In order to display the Person data from the NSManagedObject a particular window represents, we can use anNSObjectController that is tied to the window controller’s person key.

  • Open the PersonWindow.nib file and add an NSObjectController called ‘Person’.
  • Set its Mode to Entity with Entity Name ‘Person’.
  • Bind its Managed Object Context to the File’s Owner ‘managedObjectContext’, and its Controller Object to the File’s Owner ‘person’ model key path.
  • Add an NSArrayController called ‘Departments’, set its Mode to ‘Entity’ its Entity Name to ‘Department’, and specify that it Prepares Content.
  • Bind the Departments array controller Managed Object Context to the File’s Owner ‘managedObjectContext’.

Next add labels, text fields and a popup menu to the person window so it looks like this:
The layout of the employee window

To get the values to display in the window setup the following:

  • Bind the Value of the two text fields to the Person controller ‘selection’ ‘firstName’ and ‘selection’ ‘lastName’ respectively.
  • Bind the Content of the popup menu to the Departments controller ‘arrangedObjects’.
  • Bind the Content Values of the popup menu to the Departments ‘arrangedObjects’ ‘name’.
  • Bind the Selected Object of the popup menu to the Person ‘selection’ ‘department’.

After you’ve built and run the project, create a department and a person. Clicking the Open Employee Window button will now open a window displaying the current person with their details. When you change their first or last name (and either press Enter or tab out of the field to ‘End Editing’ on that field), notice that the entries in the main window change to match those in the person window. If you move a person into a different department, notice that they disappear from the list in the other window (assuming the old department is still selected, of course). Notice also that Undo automatically works for changes made in the employee window and if there are multiple people selected when you press the button, the right number of windows should open.

Final Touches

It would be nice if the person window that was displayed had a title relevant to the person it represents. When a window controller builds a window from a nib file, it calls thewindowTitleForDocumentDisplayName: method which by default simply returns the supplieddisplayName attribute. To change this behaviour, override the method by using the following declaration in PersonWindowController.m:

- (NSString *)windowTitleForDocumentDisplayName:(NSString *)displayName{if( [self person] )return [NSString stringWithFormat:@"Employee: %@ %@", [[self person] valueForKey:@"firstName"], [[self person] valueForKey:@"lastName"]];elsereturn displayName; // shouldn't happen but you never know...}

Now whenever you open an employee window, it will be appropriately titled.

It would also be nice if the list of departments and employees were both sorted alphabetically. By default, Core Data displays objects in the order in which they were loaded which is arbitrary. To sort the two array controllers, add anIBOutlet NSArrayController called departmentsArrayController to match thepeopleArrayController in MyDocument.h and connect it in Interface Builder to the Departments array controller. Also in Interface Builder, set both array controllers to ‘Auto Rearrange Content’. This ensures that the array controller will rearrange itself whenever any changes are made to its objects.

Next add the following code at the end of windowControllerDidLoadNib: in MyDocument.m:

NSSortDescriptor *departmentSortDesc = [[[NSSortDescriptor alloc] initWithKey:@"name" ascending:YES] autorelease];NSArray *departmentSortDescArray = [NSArray arrayWithObject:departmentSortDesc];[departmentsArrayController setSortDescriptors:departmentSortDescArray]NSSortDescriptor *personLastNameSortDesc = [[[NSSortDescriptor alloc] initWithKey:@"lastName" ascending:YES] autorelease];NSSortDescriptor *personFirstNameSortDesc = [[[NSSortDescriptor alloc] initWithKey:@"firstName" ascending:YES] autorelease];NSArray *personSortDescArray = [NSArray arrayWithObjects:personLastNameSortDesc, personFirstNameSortDesc, nil];[peopleArrayController setSortDescriptors:personSortDescArray];

Now the two array controllers will sort the displayed data in alphabetical order.

There is still the slight problem that we are able to open multiple windows for one person. To correct this, change theopenPersonWindow: method declaration to the following:

- (IBAction)openPersonWindow:(id)sender{NSArray *selectedPeople = [peopleArrayController selectedObjects];NSArray *openPeopleWindows = [self windowControllers]NSManagedObject *selectedPerson; if( [selectedPeople count] > 0 ){int i;for( i = 0; i < [selectedPeople count]; i++ ){selectedPerson = [[peopleArrayController selectedObjects] objectAtIndex:i];BOOL personIsAlreadyOpen = NO// check to see if person window is already openint j;for( j = 0; j < [openPeopleWindows count]; j++ ){NSWindowController *eachWindowController = [openPeopleWindows objectAtIndex:j]// only ask PeopleWindowControllers if they are open for our personif( !personIsAlreadyOpen && [eachWindowController respondsToSelector:@selector(person)] ){if( [(PersonWindowController *)eachWindowController person] == selectedPerson ){[eachWindowController showWindow:sender];personIsAlreadyOpen = YES;}}} if( !personIsAlreadyOpen ){PersonWindowController *newWindowController = [[PersonWindowController alloc] initWithPerson:selectedPerson inManagedObjectContext:[self managedObjectContext]][newWindowController setShouldCloseDocument:NO];[self addWindowController:newWindowController];[newWindowController showWindow:sender];}}}}

There are various ways to accomplish this task; the above code checks through each of the document object’s window controllers to see if it responds to theperson: method. If it does, it asks whether the selectedPerson is the person displayed by that window controller. If it is,showWindow: is called to bring it to the front. Otherwise, a new window controller is created and added to the document object’s window controllers.

Summary

At this point, we have a very simple but hopefully functional multiple window Core Data application. Making multi-window applications using the Core Data framework is actually not that difficult, it simply requires that you (understand and) keep hold of references to managed object contexts and pass around references to the NSManagedObjects held within them.

You might wish to keep a copy of the project in its current state as in the next article I’ll be expanding it by working with multiple managed object contexts.

If you’ve found this article useful, please let me know. If you think it could be made better (and I’m sure it can), please tell me and I will make the requisite changes!

原创粉丝点击