Programming Mac OS X with Cocoa for Beginners/Archiving

From Wikibooks, open books for an open world
< Programming Mac OS X with Cocoa for Beginners
Jump to: navigation, search

Previous Page: An Inspector calls | Next Page: Adding finesse

We already discussed some of the general principles of archiving, now we'll make it happen for our drawing documents. Once we have the ability to archive shapes, we will find that implementing things such as cut and paste become straightforward.

You'll recall that when we added code to MyDocument to read and write the document to and from a file, we used a pair of objects called an NSKeyedArchiver and an NSKeyedUnarchiver. At that point, we simply archived our entire 'objects' array as the so-called root object. We didn't really care what happened behind the scenes - the archiver turned the array into an NSData object which we handed off to Cocoa which wrote the data to a file. When we opened a file, the unarchiver rebuilt our array which we then used as our objects array. So we should be able to save a drawing as a file and open it again, shouldn't we? No, not yet. The problem is that so far we haven't archived the actual properties of our shapes. WKDShape is a class we designed, so Cocoa doesn't yet know how to archive it. Thus if we try and save our drawings, we'll be disappointed that we get a blank document back when we open them again.

We tell Cocoa how to archive our shapes by writing a little code. We need these two methods:

- (void)                encodeWithCoder:(NSCoder*) coder;
- (id)                  initWithCoder:(NSCoder*) coder;

These two are part of a formal protocol, NSCoding. We already met an informal protocol, which is just some methods that we agreed amongst ourselves to implement. A formal protocol is similar, except that it is strictly enforced - an object that implements a formal protocol MUST do so. In order to tell Cocoa that we want to be part of the formal protocol, we modify our class declaration like so:

@interface WKDShape : NSObject <NSCoding>

This says we are going to be implementing the formal protocol NSCoding. If we fail to do so, we'll get an error at compile time and will be unable to build our application.

Earlier we used NSKeyedArchiver, and its counterpart NSKeyedUnarchiver. Examination of the header files for these will show that these are in fact subclasses of a more general class called an NSCoder. This is what is passed to the two methods above, the pair of which are required to implement the NSCoding protocol. All we need to do is to tell the coder which properties to add to its archive, it does the rest. We give each property a name or key, and then use that key to retrieve the same property later. The properties we need to save for each shape are:

  • its size
  • its position
  • its fill and stroke colours
  • its stroke width

NSCoder has methods such as encodeObject:ForKey: and encodeFloat:forKey: that will handle the hard work. So here are our archiving methods:

- (void)                encodeWithCoder:(NSCoder*) coder
{
        [coder encodeRect:[self bounds] forKey:@"bounds"];
        [coder encodeObject:[self fillColour] forKey:@"fill_colour"];
        [coder encodeObject:[self strokeColour] forKey:@"stroke_colour"];
        [coder encodeFloat:[self strokeWidth] forKey:@"stroke_width"];
}


- (id)                  initWithCoder:(NSCoder*) coder
{
        [self init];
        [self setBounds:[coder decodeRectForKey:@"bounds"]];
        [self setFillColour:[coder decodeObjectForKey:@"fill_colour"]];
        [self setStrokeColour:[coder decodeObjectForKey:@"stroke_colour"]];
        [self setStrokeWidth:[coder decodeFloatForKey:@"stroke_width"]];
        
        return self;
}

The first, encodeWithCoder, is used when saving. The properties are saved to the coder using the keys given here, which are just strings that identify the property. The decode method is the reverse. Note that it is an init method, which makes sense - the object is created from the data stream by the coder, so it needs to be initialised using the saved properties. Normally, an object will call [super initWithCoder:coder] and [super encodeWithCoder:coder] before doing anything else, so that proprties of all the subclasses are also saved as needed. In this case, because we have inherited from NSObject, which doesn't implement <NSCoding>, we don't need to do this (in fact it would be an error to do so). That's why in this case we call [self init] instead.

Now if you compile and run, you'll find that saving a drawing to a file works - when you open that file again, your drawing reappears exactly as it was when it was saved, including all the properties such as colours, etc.

Cut and Paste[edit]

Cut and Paste leverages archiving so that we can move objects between documents, etc. Now we are able to archive a WKDShape, we are also able to archive any set of these shapes reliably and easily. Recall that an NSKeyedArchiver converts a "root object" of some kind to a NSData object, which is just a block of data containing an encoded version of that object. NSData is written to a file in the case of saving the document, but if we write it to an object called a pasteboard instead, we have put that data on the clipboard, thus implementing a cut or copy. Paste is the reverse operation, similar to dearchiving a file; we unarchive the clipboard data and add the objects so created to the document.

Cut and Paste are commands, so we implement them as action methods in MyDocument. Add the following to the class definition in MyDocument.h and SAVE the file:

- (IBAction)    cut:(id) sender;
- (IBAction)    copy:(id) sender;
- (IBAction)    paste:(id) sender;
- (IBAction)    delete:(id) sender;

We will be implementing all four. Note however that a cut is equivalent to a copy followed by a delete, and a delete is just a case of deleting the objects in the selection, so the only 'hard' work is in the copy: and paste: methods. Before we implement them, let's hook them up in IB.

double-click 'MainMenu.nib' to open it (if it's not open already in IB), and double-click the 'FirstResponder' icon. Recall that we use FirstResponder whenever commands can be implemented in a variety of places that depend on the context. This is just such a time, since cut and paste etc can be implemented in all sorts of ways by all sorts of objects. As it happens, you'll find that we don't need to do anything - cut, paste etc are all prewired for you when Mainmenu.nib was created. Verify that the menu commands are indeed connected to these methods.

In Xcode, flesh out these four methods in MyDocument.m. For now, don't add any code. Compile and Run the project. You'll now see that the Cut, Paste commands are available in the Edit menu. In fact we'd rather that these only became available when there was something selected to actually cut, but for now we'll accept the default behaviour until we have built and tested the code.

The object responsible for the clipboard behaviour in Cocoa is NSPasteboard. There are in fact a number of pasteboards for different tasks but the one we need is the basic one, called the 'general' pasteboard. When we receive a 'cut' command, we archive the selection into an NSData object, then place this data on the general pasteboard. Because this data is private to our application, we don't need to worry at this stage about converting it to a form that would be readable by other applications, such as TIFF or PDF.

Here is the implementation for our edit methods:

- (IBAction)    cut:(id) sender
{
        [self copy:sender];
        [self delete:sender];
}


- (IBAction)    copy:(id) sender
{
        if ([[self selection] count] > 0 )
        {
                NSData* clipData = [NSKeyedArchiver archivedDataWithRootObject:[self selection]];
                NSPasteboard* cb = [NSPasteboard generalPasteboard];
                
                [cb declareTypes:[NSArray arrayWithObjects:@"wikidrawprivate", nil] owner:self];
                [cb setData:clipData forType:@"wikidrawprivate"];
        }
}


- (IBAction)    paste:(id) sender
{
        NSPasteboard* cb = [NSPasteboard generalPasteboard];
        NSString* type = [cb availableTypeFromArray:[NSArray arrayWithObjects:@"wikidrawprivate", nil]];
        
        if ( type )
        {
                NSData* clipData = [cb dataForType:type];
                NSArray* objects = [NSKeyedUnarchiver unarchiveObjectWithData:clipData];
                NSEnumerator* iter = [objects objectEnumerator];
                id obj;
                
                [self deselectAll];
                
                while( obj = [iter nextObject])
                {
                        [self addObject:obj];
                        [self selectObject:obj];
                        [(WKDShape*)obj repaint];
                }
        }
}


- (IBAction)    delete:(id) sender
{
        NSArray* sel = [[self selection] copy];
        NSEnumerator* iter = [sel objectEnumerator];
        id obj;
        
        while ( obj = [iter nextObject])
                [self removeObject:obj];
                
        [sel release];
}

cut: is simply a copy: followed by a delete:

copy: first checks that the selection isn't empty, and if not, it archives the selection to an NSData object using a keyed archiver. Then it obtains the general pasteboard. To put data on the pastebaord, you first declare the types that the data will have. This allows multiple representations to be placed on the clipboard of the same data - the receiver can decide which representation suits it best - for example a graphics program might want a TIFF image of the data, whereas a word processior might want a text representation, etc. Here, we only have one representation, which is a private format we call "wikidrawprivate". Thus the "list" of types in fact only contains one item. Once we have declared the type, we can put the data on the pasteboard associated with that type.

paste: reverses this procedure. First it obtains the general pastebaord and checks that data of the type "wikidrawprivate" does exist. If not, it does nothing, so it won't attempt to paste data from another application that it cannot understand. Then we download the data fron the pasteboard, and unarchive it. We know it's an array of shape objects - whatever the copied selection contained in fact. We need to ADD these objects to the document, so we just iterate over the array and add and select the objects we come across. We also first deselect all the objects in the document so that the user gets the expected result - the pasted objects are selected after the paste.

The delete: method deletes the selected objects from the document. It does this by iterating over a COPY of the selection array. A copy is needed here because removing an object also deselects it - if we didn't make a copy we'd be modifying the same array that we are iterating over and things wouldn't work correctly. Once we've removed the objects from the document, we are done with the copy so we release it.

Compile and run the project and confirm that you have the ability to cut and paste objects between different documents.

Further exercises:

  • Modify the code so that it also exports a PDF representation of the selection. This is moderately involved, but not that difficult. Hint: look at NSView's writePDFInsideRect:toPasteboard: method
  • More complex: Add drag and drop behaviour. This is almost the same as Cut and Paste, and uses the 'drag' pasteboard. Override methods from NSView to get notified when to create the drag data and when to receive data dragged to the view.

Previous Page: An Inspector calls | Next Page: Adding finesse