Configure player

Close

WWDC Index does not host video files

If you have access to video files, you can configure a URL pattern to be used in a video player.

URL pattern

preview

Use any of these variables in your URL pattern, the pattern is stored in your browsers' local storage.

$id
ID of session: wwdc2005-118
$eventId
ID of event: wwdc2005
$eventContentId
ID of session without event part: 118
$eventShortId
Shortened ID of event: wwdc05
$year
Year of session: 2005
$extension
Extension of original filename: mov
$filenameAlmostEvery
Filename from "(Almost) Every..." gist: ...

WWDC05 • Session 118

Advanced Document-Based Application Techniques

Application Technologies • 57:58

Move beyond the default document handling capabilities of Cocoa. Learn how to properly customize and extend Cocoa's document classes to support multiple windows displaying views of a single document, displaying data from multiple documents within a single window, advanced error handling, plugins, and much more.

Speaker: Mark Piccirelli

Unlisted on Apple Developer site

Transcript

This transcript was generated using Whisper, it has known transcription errors. We are working on an improved version.

Good morning. Welcome to session 118, Advanced Document-Based Application Techniques. My name is Mark Piccirelli. I'm an engineer in the Cocoa Frameworks Group. So Cocoa has always had a great architecture for document-based applications, doing the standard sort of things like opening documents, saving documents, even gives you a little help with printing and undo and things like that.

So if you know how, you can do a lot of different clever things with it. I'm going to show you some of the clever things today. And also, stuff I want to show you is some of the new features that we introduced in Tiger that we'd like you to take advantage of.

So prerequisites. Hopefully you've already worked a little bit with NS Document and friends. The sample code I'm going to show you today depends a lot on Cocoa bindings, so hopefully you've been experimenting with that. Understand the concepts behind key value coding and key value observing compliance. And the classes we're specifically going to spend a lot of time with today are NS Document, NS Document Controller, and Window Controller, and subclasses of our own making.

*sniff* So what you'll learn. Good error handling is important, and we've made some improvements in Tiger that makes that easier for you to do properly. Supporting new file formats with a plugin architecture, we've opened up our document type API a little bit to help you there. Auto-saving, a new feature we added in Tiger. Multiple windows per document, and multiple documents per window.

So today's sample app is Sketch 2. We are rewriting it to take advantage of all the features we've added in the last several years. Actually, I say we, I really mean I. It doesn't take more than one person to rewrite Sketch. So good error handling, a good Macintosh application helps the user when things go wrong. It gives them a good description of what went wrong and if possible, a suggestion of what they can do to fix the problem.

So in Panther, we introduced a class named NSError and used it in a few places. But we didn't use it in NSDocument. NSDocument's error handling hadn't really caught up to what was possible yet. So the result was error alerts like this one, which is not even remotely HI guidelines compliant and is unfortunate in a variety of ways.

So there are ways in Panther and earlier versions for your app to present good error alerts, but it was hard. Number one, how do you get on this document not to present its not so good error alerts and things like that? So for Tiger, we made a bunch of improvements.

If you don't do anything to take advantage of the improvements, the worst thing you'll see is an error alert that though it's not very descriptive, at least is HI guidelines compliant. So in this example, the document Migrate Sketch cannot be opened. It doesn't have the word error there, you know, no kidding. It just has a sentence saying what went wrong.

Now, for very simple applications, even in Tiger, you might see something like this, something really descriptive. The document Migrate Sketch cannot be opened. You do not have appropriate access privileges. And this document's code sends the error and is causing this error alert to be presented. And at the bottom, it even has a little bit of a recovery suggestion. To view or change access privileges, select the file in the finder and choose File, Get Info. So that's pretty helpful to the user.

So what do you have to do to take advantage? Because very often for backward binary compatibility reasons, in old applications, you just get the plain error alert with really not much description. Well, what you have to do is you have to stop overriding the NSDocument and NSDocumentController methods that don't have a place to return NSErrors.

So instead of overriding read from file of type or write to file of type, override read from URL of type error, write to URL of type error. So the first two methods that you see there are now deprecated. And of course, you know, your application still works. We still invoke your overridden methods even if they're deprecated. But if you remove those overrides and override the new ones instead, the passing of the errors up and down the stack works much nicer.

So, and this applies to both the NSDocument and the NSDocumentController. Check out the documentation and the release notes. We deprecated a lot of methods in Tiger and replaced them all with something better, even if, you know, the something better was just adding a place to return an error. So, demonstration. Let's update Sketch for better error handling.

So here I have a version of Sketch that is pretty much rewritten, but I left-- The old style reading and writing methods in place. So read from file, Sketch doesn't actually have to override this. If you've worked with NSDocument, you know there's a series of method indications that happens during reading and writing, and the default implementation of each invokes something that's a little easier to override.

So read from file, you know, the default implementation of it actually gets an NSData and then invokes low data representation. Actually, there's another level in between there that involves file wrappers. But in this example, you know, both of these are invoked. So and then this method, this class still overrides the old style low data representation of type and data representation of type.

And if we, you know, open a file to which we don't have read access, you just get a very simple error alert. The document, a few circles, without enough permissions, that's the name of it, cannot be opened.

[Transcript missing]

And let's replace our override of this old data representation of type method.

with the newer data of type method that has a place to return an error. And we save that, and we build it, and we run it. And we try and open the same file again, to which we don't have permissions. And we get the same error alert. It's not very descriptive. What went wrong? Well, in SKT DrawDocument, the subclass of NSDocument, we still have this override of this old deprecated method.

So NSDocumentController, NSDocument machinery for backward binary compatibility overrides it. And once you start--I'm sorry, invokes it. And once it starts down this backward binary compatibility path, calling all these deprecated methods that don't have a place to return errors, it stays on that path. So the best you'll get is an error that is not ugly but is not very descriptive either.

So let's replace that to-- with the method that replaces it in Tiger, an override of the method that replaces it in Tiger. So read from URL of type error. And one of the things we did while introducing all this new API and NSDocument, Document Controllers, made up our mind whether we wanted to use file paths or URLs. So we went with URLs.

So we build it and run it. And we open that file to which we do not have permission. And it puts a very descriptive error alert up. And that's a good thing. This error alert actually, in particular, this NSError was returned by an NSData method that NSDocument called to get the data that it passes to read from data.

So NSData returned the NSError. NSDocument fiddled with it a little bit to put it in the context of a document error and then presented it to the user using an API, if you check the release notes in the documentation, which is really very open. And there are many, many customization opportunities for presenting errors.

what you just saw was that what you want to do, as soon as your application can depend on Tiger, get rid of your overrides of the old methods and avoid the backward binary compatibility code path. You know, your application will work but the error alerts won't be as good as they could be. And you'll trigger, you know, better default error presentation. And--oh, something I didn't show it.

There are lots of opportunities for you to return errors, you know, in those overrides, those methods that you saw. If your application is what senses the error, you can create an NSError object very easily and return it. And if you check the documentation for NSError, it's very powerful.

It has places for showing for a description of the error and a failure reason for the error which is just part of that that's used some of the time. And, you know, recovery suggestions, recovery options for putting, you know, buttons on the error alert, all sorts of stuff. So have a look at that. So plug-in architectures.

Over the years I've had a bunch of questions and requests from people who wanted to make their applications more flexible. They wanted to ship an application and add support for new file formats, either importing or exporting after the fact. Either they ship the plugins themselves or they allow third parties to ship them.

And NS Document Controller didn't really have enough API to make this easy for you. If you did it, you ended up doing it all yourself. You ended up having to reproduce a lot of the work that NS Document Controller does. To make that unnecessary, we've opened up the document type API in Tiger.

And the gist of the change is that if your plugins support new file formats, you want them to get used by AppKit's default machinery. So what we've done, we've added a few new methods to classes like NSDocumentController. DefaultType, which is the name of the type for new documents if the user selects new from the file menu. Type for contents of URL error when the user's opening a document, what type is it? And document class names, if your plugin architecture is so powerful that you allow it to add new subclasses of NSDocument to your application, well, we can accommodate that now without too much.

And then there's a few other old methods that have been there, like type from file extension and document class for type. They've always been there, but a lot of them weren't invoked when you would expect them to be. You would override them, and you're like, hey, my override's useless. We now very consistently use public API when you would expect it to be.

And that was for document opening. For document saving, roughly the same situation. There's a new method in NSDocument, a writable type for save operation. If your document, you know, sometimes when you support exporting, you can't export specific document formats because the document is using some feature that won't be representable in that file format.

If you have complicated situations like that, you have total control now over what the user sees in the save panel and things like that because you can just override this method. And it returns, you know, an array of type names and you can fiddle with that array all you want. And existing class methods like writable types and isNativeType are now, again, invoked when you would expect them to be.

So for a demonstration, I kind of made up a problem for myself so I could solve it using plugins. Sketch 2 uses a new native file format. It uses a new style property list so it gets to put numbers in them instead of having to, you know, make strings out of everything and put it in there. But any good application that updates its file format still has to open documents of the old format. So in Sketch, we'll add support for that old file format using an importer plugin.

So here we are in Sketch, and I added just a really simple plugin architecture, and I didn't want to get too clever up here. And where is it? Just to a subclass of Document Controller, I just put the stuff in there. There's a few new public methods, not public methods, methods on this Document Controller subclass. Exporter for type, where type is an NSDocument-style type name.

Importer for type, and the entire list of exporters and so on. And just when the Document Controller is initialized, it just routes around in a directory for all the bundles, whose file name extension is SketchPlugin, and instantiates the principal class out of those bundles and adds them to its list.

And in SKT Draw Document, we use this stuff in a few places. So in our override of read from data, read from data has passed a type name, which has already been figured out by the document controller. And this override, you know, it checks to see if the type is one of the ones that it natively knows how to read. And if not, it just asks Document Controller, "Hey, is there an importer for that type?" And if there is, it just invokes this method, which is defined on the importer class. I'll show you that.

From data, get graphics and the print info and an error if something goes wrong. So it's very specific to Sketch. And the other thing that an importer is responsible for telling its users is what type it is it's importing and what the valid file name extensions are for that type.

In Sketch, updating the file format, the file name extension is now Sketch 2. This importer understands the old Sketch file name extension and also Draw 2, which is an old sketch file name extension. an older one. So if we were to run this, this wouldn't work right now, because we haven't overwritten any of the methods that I was just telling you about to tell NSDocumentController about the types that are available now. So let's add overrides for that.

And the first thing to do is in NSDocument, override a couple of methods. Readable types, SKT Draw Document, in addition to the types that NSDocument has found by rooting around in your Info.plist can also read the types that are supported by various importers that are installed in the application. So it gets the list of importers and asks each one, you know, what's your supported type and adds it to the list and returns that to who's ever asking.

And typically who's ever asking is NSDocumentController. You're not supposed to have to care. And the same thing for writable types. It just roots around in the list of exporters asking each one what's your supported type and adds it to the list. But that by itself would not be enough. It's required, but it's not enough.

We also have to override a couple, excuse me, A couple of NSDocumentController methods, too. So NSDocumentController has a few methods. The default implementations of all of which just root around in information found in your application's Info.plist. But if we want to support different file extensions, we're going to have to tell it how to figure out the document type from a specific file extension.

And here we just use the importers to accomplish that. So we get the list of importers and And we check to see if this particular file name extension is in the set of extensions supported by the plugin, and if so, we say that the type for this file extension is the one supported by this importer.

And another thing we have to override is Document Class for Type. Our plugin architecture is not that super whiz-bang, so it doesn't let you add new subclasses in this document to the application. But what it does say is that for any particular type that's been found using importers and stuff like that, the Document Class is going to be SKT DrawDocument, because that still gets instantiated for different importer types. and file extensions from type is used in the save panel, oh, and also in the open, when presenting the open panel. For a particular type, what is the list of file extensions that corresponds to that type? Let's build that and run it.

So this particular file that we're opening, an old... There we go. An old sketch document that sketch. This isn't--oh, hey, you know what I forgot to do. If you're going to support a plugin architecture, you really ought to build the plugins. Let's try that again. So that's what happens, so you get to see what happens when there is no plugin available.

In the open panel, it's disabled. Now when we make the plugin available, when we built it, we just dropped it in the spot where Sketch could find it. There we go. Now this file with the old file name extension on it is recognized. So, and it opens just fine.

So that's it for importers. So what you just saw. I'd already added a simple plugin architecture to Sketch. I didn't really walk you through all the code and detail of how I designed that very simple stuff. Well, it wasn't that simple, but how I designed that relatively straightforward stuff.

We hooked it up so that it could be used in document opening and saving by overriding the NSDocumentController and NSDocument methods that are invoked when the app kit needs information. And now it reliably invokes those overridden methods. So autosaving. More new API that we introduced in Tiger. It's a feature of NS Document.

What it does is it helps the user, it's the same thing you've seen in a number of other applications, it helps the user recover from things like application crashes, perhaps caused by poorly written plugins, power failures, and things like that. Things like that, I mean, you know, kernel panics, cosmic rays, things like that.

So, it's not on by default. We explored the idea of like, hey, is this a feature we can add to every NS Document-Based app and they'll just get it for free? And no, we couldn't do that at all. So, you have to explicitly turn it on. But turning it on is pretty easy.

Basically, what turning it on requires is that you have to add a little bit of UI for this, for the preference for this. Users don't always want auto-saving. And your document saving code has to follow the rules that have been in the documentation for a while. For example, in an override of a method that has write in the name, it passes in, you know, a URL.

Actually, just write the file to that URL regardless of the current location of the document. Don't invoke file URL or file name as it was called in Panther before we added a bunch of new API. Don't assume anything that there's a relationship between the current document's location and where we're asking you to write. Even before this, we've been asking you to write in, you know, a folder, a directory inside the temporary folders. Then we'd play, you know, FS Exchange object games and things like that.

So, the final version of the file would be in the folder. You would be happy. And, you know, various metadata wouldn't get stripped off. But now with this auto-saving feature, it's one step more important that when NSDocument tells you to write a file by invoking your override of one of the writing methods, just write it right there and don't do anything else. So, let me show you. I will turn it on and sketch.

Actually, it's already turned on in Sketch, but let me walk you through what we did to do that. First of all, we had to add a new preference panel, Sketch's first preference. A simple checkbox, whether or not the user wants us to autosave documents or not, and if so, how often.

And by how often, I mean from the time that the user modifies the document, how long should NSDocument wait before it autosaves. If you have it doing it every one second, it'll get annoying, but once an hour leaves a lot of time to lose work if something goes wrong. Thank you.

So, and the way we set up this UI, we used bindings. It was pretty simple on this half of it. The value of this checkbox is just bound to a key path that's in the user defaults, auto-saving documents, a Boolean value. And the value for the delay, you know, how long before, you know, between when the user modifies a document and it's auto-saved, you know, the delay in seconds is also saved in the user preferences.

And that's pretty much all it takes to get that preferences panel up and saving stuff in the user defaults. Now, to get stuff out of the user defaults, I use bindings in a way that I guess is kind of novel. Not a lot of people have done it yet.

The problem that has to be solved is that what you saw in the UI is that there's a checkbox and a number. But in the API for this document controller, there's just a number, the autosaving delay. And if the delay is zero, then it doesn't autosave. But of course, that isn't the kind of thing you would want to represent in the UI to the user directly. Type a zero if you don't want autosaving. Not good enough. So we put the checkbox, we put the custom value. So we end up with these two numbers.

We have to make them, we end up with these two values. We have to make them just one number to show it on this document controller. And the way I set it up is that I added bindings to my app delegate. Why did I put it in the app delegate? Because it was there. And, you know, this didn't deserve like a whole other class. So, and there's other ways to do this besides bindings.

But I was kind of experimenting a little bit. So what we did is an interesting feature. What we did is take advantage of an interesting feature of the default. Implementation of key value binding. If you bind an object, any doc, any object, even like the app delegate like this.

And it doesn't have, you know, binding support already. The default implementation of key-value binding will just use key-value coding to set the value as it's changed. So we bind our-- Our app delegate to the user defaults using this key, it's just, it's the same, The key is autosaving documents, and there's another one, autosave delay. And we just use the same key that we're using to put it in the user defaults.

So we bind the app delegate directly to the user defaults. Now because we bound that, what that means is that key value binding is going to use key value coding to tell the app delegate when the value changes. So we have to be key value coding compliant for these two keys, the autosave documents, yes or no, and the autosave delay. And all we had to do to do that was to add two methods, autosaving documents and set autosave delay.

So, and in both cases, they do the right thing. It boils down to an invocation of this NSDocument controller method, setAutoSavingDelay. So if autosaving, then we set the delay to the real number of users typed in. If that checkbox is not checked, then we just set it to zero. So actually, you can see these get invoked.

[Transcript missing]

I don't know if you can see this too well, but it's invoking our set autosaving delay method. And what happened is basically the value went from bindings. It got set in the user defaults, and because user defaults is KVO compliant, you know, observable for a number of things, it bounced right back up into this bindings that we'd added to the app delegate.

So is this definitely the least code way to do this? I'm not really sure, but at least we didn't have to write anything that directly, you know, read or wrote to user defaults. So, and we'll set the value to something nice and short. We're not going to wait a minute here to see auto saving happen. And before we start pretending to crash the app, we should probably save that in defaults.

And now when the program starts up, the binding stuff is telling the app delegate what the values were that were stored in the user defaults. Okay, so now autosaving is on. Let's see it in action. Let me leave some space over here on the desktop. I'll open a file on the desktop. Oops. And just add a few things to it.

So this is the file we just saved on the desktop, our document. Now the document's not modified at all, but if we modify it by just dragging a graphic or something like that, and wait three seconds. There's the autosave document contents right next to it. So, and this works for untitled documents too.

If we go into our home directory and under library autosave information, You can see it's already stored the property list there where it's pointing to this document that is already auto-saved. If we make a new untitled document and just do anything to it, It just gets saved in that same folder in your home directory. So, and let's hear it for the finder for showing that so quickly.

So let's crash our app by just-- oh, there's so many ways to do it. Yes. So I'll just restart it. And there are those two autosaved documents right back. So, and as you can see-- And as you can see, the Document Modified button in the window close control is set. And what that means is that no matter how much you undo or redo, you're never going to get this document to the point where it's not going to ask you if it should be saved when you close the window.

[Transcript missing]

And by the same token, if you just close a document, throw it away,

[Transcript missing]

So what you just saw is, you know, stuck in a demonstration of something novel that you can do with bindings. And our modified documents got autosaved automatically. And it didn't take a lot of code. You know, that code I showed you, that was all the code I added to Sketch to support this feature. There was nothing new to do in NSDocumentController or NSDocument. It's just all built into NSDocument. Now, there's still nice customization opportunities for you.

In Tiger, we introduced a new kind of save operation enumeration that gets passed around between those different methods. So if you want to know whether or not a particular saving that your document is being asked to do is for autosaving or save or save as or save a copy, you can find that out. But if you're just doing something simple that reads and writes, it all just works. So multiple windows per document. This is something that's always been an AppKit, but there's never really been much of a demonstration of it before.

An NS document was designed for this, in contrast to something I'll show you right after this. Every NS document owns one or more window controllers. So if you have a UI, for example, I think a popular example is when you have, like, a 3D modeling tool, and there's, you know, the shaded version showing in one window, and the 3D, you know, wireframe in another window, and you're showing a bunch of windows onto the same document, and you don't want to really close the document until all the windows are closed.

This is the kind of UI I'm talking about. And also you can see this in Interface Builder, where it has the, you know, the main window showing you the instance view or the class view, and then the actual windows or menus of the app. So every document in this architecture owns one or more window controllers, and every window controller has a window.

Every window controller points back to its document, so it's a bidirectional connection. And the interesting thing is that you can add window controllers to an already open document, whenever you feel like it. And the kind of architecture you end up with looks like this. A document controller pointing to, you know, zero or more documents pointing to one or more window controllers, each of which points to a window.

And if you're supporting a multiple windows per document UI, you can do a lot of this stuff without NSDocument's help. So why even tell NSDocument about the window controllers that are pointing to the document? Well, it does a bunch of convenient things. Number one, at startup time, it tells the window controller about itself to establish a connection. It already has a mechanism built in for when the windows should be shown.

It tells all of its window controllers that it knows about when the document itself is edited, so that it can set that little window title bar close button thingy. And it also does interesting methods like synchronize window title with document name. So if you try this in something like Sketch, go ahead and go to the finder with a document that's open and rename it, or turn off file name extension hiding, or whatever you want.

And when you switch back to Sketch, the window title will just be updated automatically. So this is the kind of stuff it does. And of course, the document will tell all the window controllers it knows about to close themselves when it itself is closed. And as far as closing goes, you know, it has some rules built in for what does this mean. Are you closing this window? Are you closing the document at the same time or what? So as a demo, let's just real quick enable multiple windows for document and sketch.

And basically all we're doing is to our window controller class, because I didn't think this really went in the NSDocument subclass, even though this particular action will work there too, new document window. So all this does is it creates a sibling window controller and adds it to the document and then tells it to show.

And as a nicety, for this particular action, we did a menu item validation that tries to put up a window or a menu item title that's a little bit descriptive. So a new window for a specific document instead of saying new window. So let's run it and open up a document.

and hold down the Option key because I made this a mini-weight of an alternate. And so just new window for our document.

[Transcript missing]

So, an NS document nicely enforces behavior like you close one window, Okay, fine. The document still has a window. So only when you close another window does it bother putting up the save preference.

[Transcript missing]

So what you just saw, NSDocument, you know, we took advantage of a feature that NSDocument has always had. We just had to add a little bit of code to trigger it. Now, something that's going to involve a bunch more code is multiple documents per window. This is the kind of UI where in preview, if you open up a bunch of TIFF documents at once, it'll open them all up in the same window and have a drawer on the side with a thumbnail and all that stuff. And it's tricky. I really mean that. It's tricky, because then it's Document Window Controller. We're not designed for this. But you can do it.

So, and the things that are in, you know, the design that makes this difficult is the fact that NS documents create and own window controllers. Well, that's a fine pattern in simple applications, but if you have a window controller that's displaying the contents of multiple documents, you know, that's not a good assumption.

And then this document controller doesn't know anything really about window controllers. All it knows is that it tells documents that it opens to make new window controllers. It doesn't do anything, any of the stuff that we need in this case that I'm about to show you about, you know, managing window controllers. When do they get created? When do documents get associated with them and stuff like that.

So, but that's okay. All we have to do is, you know, make a couple UI decisions, make up rules for when new windows are created and therefore new window controllers and what documents get associated with them. And then we have to subclass to make a smarter document controller class.

We're also going to have to deal with some assumptions that this window controller makes. A window controller, its default implementation in the AppKit, can only point to one document. Well, that's not good enough for what we want to do, but that's okay. What we have to know is the what's and the when's of the messages that are sent back and forth between document and window controller so we can override some of the methods and things like that. The good news is they're all public. There's no SPI back chatter secretly between window controller and documents, so we can see everything that's going on and change it. And we'll basically just subclass to make a smarter window controller.

And what we'll end up with is this, an object diagram like this. A document controller kind of temporarily pointing to a window controller. You know, it creates it, adds a bunch of documents to it, and then forgets about it. But the document controller, as always, will still keep a pointer to a bunch of documents. And the window controller will now point to a bunch of documents, and documents will still point to a window controller.

And, you know, the nice thing about what's there in the AppKit is that even though it wasn't designed with this in mind at all, the fact that the API is pretty good and it's nice and public, you know, allows us to rearrange things into an object diagram that doesn't look at all like the last one that we showed for, you know, multiple windows per document. So let's do that. Let's trigger this feature in Sketch.

So the first thing I want to show you is the nib that we made for this. And for this demo, this work on Sketch 2 right now, I'm not fooling around with having a drawer with neat thumbnails and stuff like that. But that would be nice to do someday. All I did was use bindings to add this pop-up.

This will be our document selector here. And if you look at the bindings that are used in it, It binds to the files owner, which is a window controller, and it's binding to the list of documents. That's what it's going to present. Specifically, what the user's going to see is a new property that I added to this document called Notifying Display Name. And when the user selects something in Selected Object, that's going to change the document of the window controller.

Yeah, and one thing to keep in mind with that is that, you know, this documents property, this is something that we're adding to our subclass of window controllers. So, in a key value coding and observing compliant way so that it can be used in a binding. And another thing to tell you is that the reason why we added this notifying display name property to our subclass of NSDocument is because NSDocument has a display name property already, but it's not KBO compliant for it. It doesn't send out any notifications when it changes. So to get around that, because you want to show the real display name to the user, we just added this other property. It was pretty easy.

So SKT Document Controller. Now, I'd mentioned that we have to figure out some rules about when we're going to batch windows or documents into single windows. And what we've done here to implement those rules in our subclass Venice Document Controller is override the method that gets invoked for each document. Open document with contents URL, display error.

And what we do is if-- because in this method, we don't really know if this document is going to be just open by itself. The user just double clicked on it by itself. Or it's part of a big batch that the user opened in Finder. And at this point, you may be asking yourself a question, well, why doesn't a document controller have open documents with contents of URLs? And that's a good question. But it doesn't have such a method right now. So we have to do a little bit of coalescing with these files as they get opened.

So what we do is we open the documents right away, but we don't display them. We put that off until we know how many documents need to get displayed. And by display, I mean having a window open on it. So what we do is we just collect them in a mutable array and schedule something to happen at the end of the run loop, you know, display pending documents.

What Display Pending Documents does is if the document count is one, you know, the user only clicked on one document at a time, it just does what a document controller would have done if we hadn't fooled with it by overriding, you know, open document contents URL. Just make window controllers and show windows.

Now if we have a bunch of documents to open, What we do is we create one window controller and we add it to each one of the documents. So it's called Add Window Controller, but really we're just, but there's really only going to be one window controller per document, even though there's multiple documents per window controller.

So basically we're just telling the document controller about each one, I'm sorry, we're telling each document about the window controller it should use. And it's the same window controller over and over. And then we make the first document the current one after all that. That's the one that the user would expect to pop up in the front. And then we show the window controller.

So we also created our own subclass of window controller here just for this purpose, with a nice long descriptive name. And just to introduce it, you know, this is setting up a bunch of bindings with the graphic view. One of the design decisions we made is that there would just be one graphic view per window, and what it was being displayed would be bound to the current document of the window controller, and if the current document changes, that's fine. So, you know, the graphic view is bound to a keypath document.crawl, and if the current document changes, that's fine.

And that's just regular stuff in WindowDidLoad that we would have done for Sketch, whether it supported multiple windows per document or not. So here in setDocument, this is pretty interesting. NSDocument always invokes this method for each one of the window controllers that is added to it. And we really have to take advantage of, you know, what's being invoked when information like that.

So we do a couple things. Number one, NSWindowController is KVO compliant. I'm sorry, this particular window controller is KVO compliant for something called the graphics controller, which is the NSDocument. So we have one NSRA controller that the graphic view is bound to so it can get its selection.

Well, we want to store selection on a per-document basis, so we have one NSRA controller per document. And here, this is just the KVO compliant way to tell the graphic viewer, whoever is bound to this, that, you know, this graphics controller thing is going to change because the current document is going to change because we're in setDocument.

And the first thing we do when a document is set on the window controller is, because we can't assume that there's just one document, we add the document to the list that we're maintaining. And we do it in a KVO-compliant way. You know, this method name, the fact that it matches the KVC-compliant method name for documents matters.

Now that we've invoked it, anybody who's observing the set of documents, like that pop-up that you saw in the main window, will hear about the change to the list of documents. And they'll also hear about the change in the current document. And here I'll just show you real quick here in Insert Object. This is what I was talking about with, number one, recording the list of documents, and then also creating a new array controller that goes with the document.

And then an SKT draw document. Just a few more things we have to override to make all this work. The first one is that we have to disable some unfortunate behavior in NSDocument's addWindowController. What it does is whenever you invoke addWindowController, for some reason, it looks to see if there's another document already on that window controller and just kicks it off. Just makes it, you know, tells it to go away. And sends it setDocumentNill. Well, we don't want that to happen. So we just added a quick check.

Um... If we're in the middle of add window controller, or I'm sorry, here. If we're in the middle of add window controller, if we're not in the middle of add window controller for some other window, for some other document, then only then do we do the remove window controller. So this is what I was talking about with the what's and when's of what gets invoked.

Now another thing that we have to tackle with this is we have to override the sentence document method should close window controller, delegate should close selector context info. Because any particular document, you know, the default implementation assumes that the document owns the window controller. Well, it doesn't really in this case. So we had to make our document subclass a little bit smarter than that.

And what it does is it basically invokes the default implementation in a way that makes sure that it gets called back before passing it on to whoever was calling that in the first place. So, and basically it tells whoever was calling, no, don't close the window controller, but then it goes ahead and closes the document anyway, because that's the right thing to do at this point.

Something else we have to add to the subclass of NSDocument is we have to tell any window controllers that are attached to the document that this document is about to close. And we just added a new method to our own window controller class. And what it does is very simple.

It just records that there's a document being closed so that when it itself gets a close message, it knows that it's coming from a specific document. A little bit of internal knowledge, only NS document ever sends the closed message to window controllers. So this is the kind of stuff you have to know to do this kind of interface. So, yeah. So after all that, let's run this. And let's make a few more copies.

So we open all those, and the document controller figured out that, hey, we're opening a bunch of documents at once, and opened them all in the same window. And this pop-up is just bound to a property of the window controller. And it's just changing which--what the current document is. And it's the same graphic view in each case. And as you notice, we took the time to make an array controller per document so that we have selection that tracks the document properly.

So, oh, and one other thing. Something I've seen people do is overriding NSDocument display name when they could be overriding This name, Window Title for Document Display Name. This is what we overrode to just put this little indicator here about, you know, which document is which. And as you close them, they just get removed from the list properly. And if you have to save, you can do that. So, and then when you close the last one, of course, the window disappears. That's it for multiple windows per document. Multiple documents per window.

So what you just saw, in contrast to contrast it with the earlier thing you saw with multiple windows per document, these two things together could be a good example of following the precept of, you know, simple things, make simple things simple and complex things possible. You know, multiple windows on one document was really simple. We just had to add one method to trigger the feature and, you know, a validation method because we wanted the menu item to look just so.

This last thing we did, multiple documents per window, as you can see, was a bunch more work. But we could do it, and we didn't have to use any SPI or anything like that. Just you needed to know a few internal details, but you know them now, so.

So, for more information, documentation, sample code, and other resources, the typical URL. So, the sample code that I've just been showing you isn't on the disk image you have yet. So, and it's probably not going to be available any time this week because it has to go through this DTS sample code process, which sounds quite grueling, but probably just boils down to a bunch of emails.

But eventually, the end result of this will be that Sketch, the sample code, is going to be up on the DTS website, and I can, you know, update it and get new things out to you without having to wait for the next major shipping release or the next WWDC or something like that. So, now that we've rewritten Sketch and it's being worked on rather actively, you should start to see a bunch of new things appear in it. So who to contact for more information? The person to point you in the right direction for stuff is Matt Formica, [email protected].

and related sessions. I would recommend the Cocoa Tiger Makeover later on this afternoon. And also, oh, something I left out is I did get permission to give away copies of the sketch sample code as it is right now. So if you catch me in that Cocoa Tiger Makeover lab, I'll plug in my iPod shuffle to your computer and you can have a copy of everything that we've just been looking at even before it goes through all the review. So, and of course on Friday morning is the feedback session so you can share your opinions about whatever you like.