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-121
$eventId
ID of event: wwdc2005
$eventContentId
ID of session without event part: 121
$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 121

Cocoa "Tiger Makeover"

Application Technologies • 1:10:37

Don't miss this hands-on session where we will show you how to transform a simple application into a feature-rich application optimized for Tiger by adding Spotlight searching, language localization, scripting, and much more. Bring your laptop!

Speakers: Doug Davidson, Hansen Hsu, Corbin Dunn

Unlisted on Apple Developer site

Transcript

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

All right, good afternoon everyone. I'm Doug Davidson and I'd like to welcome you all to the Cocoa Tiger Makeover session. Now, one of the great things about Cocoa is that it makes it really easy for you to start developing your application and get it up and running quickly.

But we all know that there's a big difference between an ordinary application and an outstanding one. And what makes that difference is attention to detail. So, what we want to do this afternoon is to show you how Cocoa makes it easy to add the details that will make your application really stand out, especially with all the great new features that we have in Tiger.

So for this purpose, we have a simple basic application that we are going to transform. It is a simple personal finance application, the sort of thing you might use to keep track of your checkbook. It's a document-based application. Each document has a single window with a table view in it that contains a list of transactions. You know, money coming in, money going out, money going out, money going out, money going out. There's another view below that that shows the details of each transaction. It's all done with bindings, so it's really simple. And it can read and write its own custom file format.

So let's take a look at it. Can we go over to demo one, please? So here's our application. We call it iSpend. And I have some sample transactions loaded. You can see they have a date, an amount, a description, optional category, and account type. And we can add transactions, we can delete transactions, we can open up the detail view down below that shows the details of each transaction. We can modify them. So the application works.

But there's a lot we'd like to do with it. We'd like to spiff up this user interface. We'd like to be able to copy and paste and drag and drop these transactions all over the place. And we'd like to take advantage of some of the great new Tiger features like Spotlight. That's what we're going to show you today. So let's start the makeover. Go back to the slides.

We're going to do this in three parts. I'll be talking about the first part, which is all about dealing with the pasteboard. What we're going to do to this application is add support for copy and paste and for drag and drop and for services from the services menu.

So I'm going to start with copy and paste because that's the foundation of the rest of this. And I'm going to describe a particular means of dealing with the pasteboard here that I like to think of as the right way to handle the pasteboard. Why do I call it that? Because it's going to make all the rest of this really easy. And in this way of doing things, everything is going to have three steps.

So, for example, for writing to the pasteboard, we'll have step one. On whatever object is going to be writing to the pasteboard, we'll define a method that describes the list of types that we're willing to write to the pasteboard. Just returns an array of them. Okay? Step two.

is a fairly generic method, not really specific to this application, I'll show you the implementation in a minute, that takes a pasteboard and a list of types and writes those types to the pasteboard using step three, The method that really does the work, which takes a pasteboard and a type and writes that type to the pasteboard. And this is the method that has the custom code that knows about the format of each type you're going to write to the pasteboard and does that work.

Reading from the Pasteboard is very similar. Step one: we're going to define a method that describes a list of types that we're willing to read from the Pasteboard. It returns an array of those. Step 2 is another fairly generic method that I'll show you in a minute that takes a pasteboard and reads from it. It doesn't need to take a list of types because the pasteboard itself knows the types that it contains.

Step three, again, is a method that really does the work, that takes a pasteboard and a type and reads that type from the pasteboard. And this is the method that has a custom code that knows about each format that you read from the pasteboard. Okay, so let's take a look at that in code. If we could go over to demo one.

Bring up Xcode. Now, in this particular application, we've decided that the document is going to be handling the copy and paste because it's the document here that knows about the formats. I have added a category to my document class for handling the pasteboard. Here is the first method I told you about that describes the types we are willing to write to the pasteboard. In this case, we have, first of all, a custom pasteboard type that we are going to use for copy and paste within this particular application.

And then we also want to be able eventually to support promise drags. That is where we drag things to the finder or the desktop, say, and produce a file there if the receiving application wants it. And to do that, we have to write out two types, a special files promise pasteboard type and a file names pasteboard type. And we're also going to write out rich text, RTF pasteboard type, and plain text, string pasteboard type.

That's the first method. The second one is the fairly generic one I told you about that takes a pasteboard and an array of types. And all this method does is it goes through those types and picks out the ones that we are actually willing to write. And then it declares those types on the pasteboard and then just goes through them one by one and writes them to the pasteboard using this third detail method.

And so this is the one that does the work. It has code for each type that we're willing to write. So for example, for our custom type, we are just going to write the transactions out as archived data. Our application knows how to read and write that. For the file's promise pasteboard type, the way that works is that to support promise drags, for that we write out the extension of the file type and the similar types that we are willing to write out. And then the file name's pasteboard type, well, We don't want to do that right away. We don't have a file handy. We're only going to do that if the receiver actually wants it. So we'll just say yes and do that later on, lazily.

RichText, I'm not going to show you this right here, but you can look at it in your sample code. This is somewhat interesting. We will write out a list of transactions to RichText using the new text table support in Tiger to write out a list of transactions as a table in RTF. And for plain text, there is also another method that will write it out as tab separated values. Pretty simple.

There's one more method that we need for our promise drags. If we are actually called on to write out a file, we need to have a method that writes out a file. And this is pretty simple. It just uses the NSDocument support. We already know how to write out our custom file format, so this one calls on that to do it. So, once we have all that, then our copy method is just a one-liner. That is, it writes to the general pasteboard all the types that we're willing to write.

All right, then let's go to the reading from the pasteboard. Here's the first method I told you about that declares the types we're willing to read. In this case, our custom type will read in file names. We don't need the file promise type. Rich text and plain text.

and the generic method to read from the pasteboard, which again is pretty simple. All it does is go through all the types that we are willing to read. For each one, it checks to see if it's on the pasteboard. If it is, it tries to read it and stops when the first success is encountered.

And then here again is the method that does all the work. For our custom document type, we read it in as archive data. File name is pasteboard type. We read it in as a file, so we know how to read files. And for rich text, we have a method that parses those tables that we wrote out, or if there is no table present, it falls back to the plain text parsing, which will parse tab-separated values or comma-separated values or similar formats. So let's try it out. Okay, run it. And... Bring up the data. Let's see if we can copy something.

And paste. That worked. That's our custom type within the application. Or we can try it as text. There's rich text. There's our table.

[Transcript missing]

The next thing I'd like to talk about, now that we've got copy and paste as our foundation, we can move on to other things, drag and drop and services.

The main data view in this application is a table view. So we're going to make that table view serve as a dragging source and dragging destination. Now in Tiger, there are some new methods for the data source for the table view that make drag and drop support really easy. So we're going to use them. Again, three steps to act as a drag and drop source.

Step 1: At initialization, we need to register a drag operation mask, it's called, that specifies the kind of drag operations that we're willing to support. Step 2: At the start of a drag, we'll be called on to provide the contents of the pasteboard. But we already know how to do that because we have our method that writes to the pasteboard.

And step three, if the drag ends up being a promise drag, say to the finder, then the recipient will tell us where it wants the files written out. And so there's a method on the data source that will be called that will specify the location where the file should be written out. And that just calls the method I showed you just now that writes out a file to a specific location.

To serve as a drag-and-drop destination, step one, at initialization time, we need to register the types that we're willing to accept in a drag. But that's just our readable pasteboard types, so we already know that. Step two, as a drag comes in as dragged over our table view, we will be called repeatedly to validate it.

And I'll show you the implementation of that in a minute. And step three, if the drag is ultimately dropped on our table view, we'll be called on to accept it and to write that data in. But that just calls on the method that we've shown earlier to read from a pasteboard.

Then we come to services. Services are a really neat feature in Mac OS X, so we should support them as much as possible because they're really useful and it's really easy to do. We wanna be able to use services from within our application. So for that, step one, at initialization, we need to register all the types that we're willing to hand off to a service or back from a service. But we already know those. those are our readable and writable pasteboard types.

Step 2: When the service menu is brought up, we need to validate specific entries in it. I'm not going to show you that method, but it's really simple. It's like other menu validation code. And step three, there is no step three, or rather, we already did it. Because step three is just to implement those methods that I showed you before, that writes types to the pasteboard or reads types from the pasteboard.

That's it. But we also want to make this application provide some services. And I've written two simple services for this application to provide. For providing services, step one, we need to advertise them by adding a services entry to our infop list. And this entry specifies all sorts of information about the service, like what the menu name is and so forth. But one important thing I want to note is that it gives a name, in this case, import data that's going to be used as part of the method that will be invoked if the service is actually used.

Step 2 is we need to register some object in our application to act as a service provider. Step 3: Implement the method that will be invoked when the service is used. For the import data service, the full signature method looks like 'import data with user data and error'. This is a very simple service, so the implementation is very simple. This service just copies data into our application, into the frontmost document, and so it just uses the method that we showed before to read from the pasteboard. So, let's take a look at this again in code.

Now, you remember that I said that these drag and drop methods are going to be implemented on the data source for our table view. In this case, that's our array controller. So I have a category on our array controller subclass. This is all for drag and drop. And here you see the initialization.

[Transcript missing]

For acting as a source, we register the drag operation mask. In this particular application, we have chosen to regard the transactions as being ordered by sorting and not by manual rearrangement. So the drag and drop operation we're going to support is always going to be a copy. So the mask we use is the or of copy operation or the generic operation, which just means whatever operation you're going to do with this.

And then remember, when a drag starts from our table view, we'll be called on to provide data to the pasteboard. And this just uses the method that writes to a pasteboard with our writable pasteboard types. And if the drag-out happens to have a recipient that wants prompt to redeem or promise,

[Transcript missing]

When the drag appears over the table view, we have to validate it, which means decide whether to accept it or refuse it and with what operation and decide where the drag indicator will show up in the table view. In this case, we are always putting the drag indicator at the end of the table view.

here, and we accept if the pasteboard contains some of the types that we're willing to read, I'll let the operation that's going to be a copy. There's a little bit of a subtle point if we're dragging within the single table view. I'll let you take a look at that in the source yourself. And then the last part is very simple. If the drag is actually dropped on our table view, we'll be called on to accept it. And for that, we just read in from the pasteboard using our read selection from pasteboard method. So let's try it out.

So, let me make some selections of various items in here, and then we'll try dragging them over. And you'll notice the drag indicator appears, and I can drop it. And there they are. Or perhaps I want to take and drag them to the desktop. There's a file written out. Or I can take and say, drag these out to text. And there's our table. Plain text. There are our tab-separated items. So we have drag and drop working. What about services? Well, let's see.

One of the services that is available is, say, TextEdit, new window containing selection. Boom, brings up a new window in TextEdit containing that data. But there are some more interesting services. Say, let's try Mail. Mail provides a service, send file, and up comes Mail with a new message with a file created from that data.

And then we also have the services provided by our iSpin application. So let me make a selection in TextEdit.

[Transcript missing]

So we have our drag and drop, copy and paste, and service is working. Let's go back to the slides. And now I would like to welcome up the next speaker, Hansen, who is going to tell us all about improving the user interface.

Good afternoon, my name is Hansen, and I'm an engineer in the Cocoa Group. And today I'd like to show you how you can add some cool new user interface improvements in your applications. So, Doug has already shown us how to do pasteboard handling. So I'm going to show you how to add an NSDatePicker to your application, implement undo support, and take advantage of view animations to create simple visual transitions between your views. So let's get started. First thing, an estate picker.

DatePicker is a new control in Tiger that allows you to display an NSDate instance. There are two styles of DatePicker. There is a textual style that contains a text field and a stepper. And this style, as many of you may know, is similar to the Carbon DatePicker control.

There is also a second style, a graphical style, that provides a calendar and a clock. Now, no matter which one of these two styles you use, you can specify which date or time elements should be visible. So, for instance, if you don't want to display the time, you can not show the clock. So now let's see this in action, and I'm going to do a demo.

So, first thing I'm going to do is open up another version of our application. Those of you who might be following along the sample code, you can open the ispend03 Xcode project. And now, Daypicker, we're going to be doing the Daypicker thing completely in Interface Builder today. So, I'm going to open up my document.nib file.

And we brought up the nib. And the date field that we're going to replace is in our bank transaction detail view. So you can see here that's the... The view that we want to bring up. And you notice that we have this field 'Date' and a text field here. Now let's look at the text field. Let's inspect it. Let's bring up the inspector and let's choose bindings.

And as you can see, this text field is bound to selection.date of the transactions controller, which means it's showing whatever the date of the currently selected item in the window is. So now, we want to replace this text field with our new NSDatePicker. So the first thing we're going to do is we're going to delete this text field.

And let's go to our pallets up here and go to the text section. And you notice this item here, this is the NSDate Picker object. So we're gonna drag that into our Bank Transaction view. So next, we're gonna go back to the Inspector. And let's change some of the settings. We want to display the month, day, and year. But we don't want to display the time. Great, so now let's make it a little bit smaller.

So now we want to bind this date picker to the same value that we bound the previous text field to. So I'm going to go back to the inspector and go to Bindings and click on Value. And we've already got Transactions Controller selected here and the controller key selection. That's exactly what we want. Now for the model key path, we want the date. So we're going to click this pull-down and select date. And now we've connected our bindings. So that's all we need to do to set up our Textual Date Picker.

So next, I'd also like to add a graphical version of the Date Picker. However, the graphical version is a little bit bigger, so I'm going to create another window for it. So let's go to our palette here. Actually, let me hide Xcode so that it's a little cleaner. And let's drag in a panel. Let's make this panel a utility window. Make it a little bit smaller. Let's give it a different name.

Now as before, we're going to go and drag in a DatePicker object. So we're going to drag this into here. So now we go back to the inspector and change its style. We can select the style from the pop-up and change it to graphical. And now as you can see, there is a calendar and a clock. We don't want to display the clock, so let's select time selection none, and center the calendar. There. So now, as before, we have to bind it to the correct thing. So let's go to Bindings, select Value, and from the pull-down from Model Keypath, select Date.

Great, so now we have a window that shows a graphical version of the date, but now we need a way to bring up this utility panel. So luckily, here I already have a nice little button with a calendar icon, so I am going to connect this button by control-clicking and dragging up to my date panel. And now that brings up a target action. I will select 'Order Front' and click 'Connect'. And now... When I click this button, this panel should pop up. So now let's go back to Xcode and build and run this.

Let's add a couple of items. Now you see that we have a date picker object here and I can click the stepper and you see all the selected items increment their month. You can increment the year or the date and I can click the button and change the date using the calendar. And it all just works. I didn't have to write a single line of code.

Could you take the slides, please? So now that you've seen how easy it is to create a date picker, I'm going to go on to our next topic, Undo Support. So, Undo support is provided by NSUndo Manager. NSUndo Manager records operations for undo and redo on a stack.

Now, in order to use the Undo Manager, whenever we have a change made to our model objects in our application, say the transactions in our iSpend application, we want to tell the Undo Manager a method to register. We want to register with the Undo Manager a way to revert the change back to its previous value. Document-based applications, such as our IceBand application, supply an undo manager by default. This default undo manager also maintains and keeps track of the edited state of the document so that the little dirty mark, The dirty mark is marked when there are still undo operations on the stack.

Now, of course, many of you know about Core Data. Core Data is new in Tiger. If you use Core Data to provide the model for your applications, you'll get undo support for free. However, today, the undo support I'm going to show you will work back on Panther and previous versions of Mac OS X.

So next, I'm going to tell you the two ways that you can use to register your methods for undo. The first way to register a method for undo is to use register_undo_with_target. This is a very simple way of registration which is only used when your method takes a single object-typed argument.

Now, often, you may have... your methods may require more than one argument, or maybe the argument is not an object, maybe it's an integer or a float. So in those cases, we provide a more general method called invocation-based undo registration. The method there is a method on NSUndue Manager called prepareWithInvocationTarget. And to use that, you specify the actual invocation to revert the object state. Now this is possible because of the dynamic nature of the Objective-C runtime.

So, next, what do we need to do to add Undo Support to our iSpend application? Well, First, when the user adds or removes a transaction in a document, we want to be able to undo that action. So to do that, we're going to take advantage of key value observing to observe changes made to the transaction's array.

Next, if the user changes a value in the transaction, such as editing the date or the amount, we want to be able to undo that. So to do that, first, each transaction model object needs a reference to the document's undo manager. Once we set that up, then, In each of the places where we change the transaction's value, Meaning, in each of the setter accessor methods for the transaction, we will register the undo instruction. So next, I'd like to go to the demo and show you this in action. So first, let's go to mydocument.m.

And the document, this NSDocument subclass, First, I'll go to the top. So our document subclass is already registered as an observer for changes to the transactions array. It does this because it needs to keep track of the current balance, for instance. So that observation is set up here in the init method.

So next, since we're already registered for notifications to the transactions array, when... When we receive a notification that the user has added or removed an object, a transactions object, we, this method, observe value for key path of object, change context will be invoked. This method takes a changed dictionary. This changed dictionary contains All the objects that have been removed and all the objects that have been added. So all the objects that are removed, we will save in an array called old transactions and the objects that have been added, we will save in new transactions.

So now, well, what happens when the user removes one or more transaction objects? Now we'll go into this code and we need to tell our undo manager how to add back in the objects that the user just removed. So to do that, We tell the UndoManager to prepare an invocation, and the method that we're going to give it is 'Insert objects in transactions at indexes'. This method I'm going to implement for you in a moment.

Now similarly, if the user decides to add a transaction, will go down to here, and we need to tell the undo manager a way to remove the transaction that the user just added. So we're going to, this time, RegisterUndoWithTarget and we're going to give it the method removeObjectsFromTransactionsAddIndexes. Now notice this time I can use the simple undo registration method because this method removeObjectsFromTransactionsAddIndexes takes a single argument which is a type of object.

The user has added a bunch of new transactions, and we want each new transaction model object to contain a reference to the document so that it can access the document's undo manager. So we're going to tell each new transaction to perform the 'select or set document' on itself. So now, I'm going to go down to the end of this method and I'm going to implement those two methods that I referenced above to revert the changes.

Insert objects and remove objects. And as you can see here, they're basically just array insertion and removal, but we're going to do a special thing. We're going to use mutable array value for key. This uses a mutable array proxy so that key value observing and key value coding compliance is maintained. Why do we need to do this? Well, we need to do this so that our transactions controller will be notified of the change that we make.

So that's all we need to do to be able to undo adding or removing a transaction. So next I need to...

[Transcript missing]

wants to undo a change made to the transactions, the transaction model object that is logically located in the transaction model. So we're going to go to the transactions.m implementation file.

The first thing I'd like to show you is we've already pre-populated this with the code. We need to implement the setDocument and undoManager accessors. So this gives us a way to tell the transaction object what the R document is and get it its undo manager. Once we've done that, we can...

[Transcript missing]

change to its previous value. So we're going to prepare an invocation and we're going to give it the previous value.

and All the other setter methods follow the same pattern. So in the interest of time, I'm not going to go over that today. You can check that out in your sample code. So that's all we need to do there. And I'd like to now run the program and show you how this works.

So I'm going to add a bunch of objects and I can change the descriptions. And I can delete a bunch of objects. And now notice I can undo the delete. I can start undoing the changes I made to the descriptions, and I can undo the ads, but I can also redo them. Now we get undo and redo support for free because NSUndoManager keeps track of both undos and redos on the same stack. There, so that's all you need to do to implement Undo. So next, let's go back to the slides.

So next, I'd like to talk about View Animations. So View Animations are another new feature in Tiger. There are two new classes that we added to Tiger: NSAnimation and NSViewAnimation. NSAnimation manages the timing and progress of animations. Its subclass, NSViewAnimation, provides a very simple and easy way to add transitions between views or windows. Some of those transactions include: you can fade in or fade out a viewer window, or you can animate a change in the size or location of a viewer window.

NSV animation can manage several animations simultaneously. So this is useful, for instance, if you want to fade in one view while you fade out another view. Now, for each of the animations that you want to animate, you will specify that using a dictionary. The most important thing in the dictionary is a target key. The target key is the view or the window that you want to animate.

You can also provide several optional keys. The effect key, you can provide fade-in or fade-out effect. And you can also provide the start and end frame of your animation. Now, if you do not provide the start or the end frame of the animation, by default, the NSFv animation class will use the current frame of your target view.

So, for more examples of View Animations and NS Animations in general, I invite you to attend session 146, Cocoa Advanced View Techniques, Friday at 3:30 p.m. So now, what steps do we need to do to add NS animations to our Iceman application? Well, What we want to do is, in the detail view, when we click the disclosure triangle and bring up the detail view or change from one detail view to another detail view, we want a cool fade-in and fade-out effect.

So, we're going to do three animations simultaneously. The first animation we're going to do is, we're going to fade out the old view and reposition it. Secondly, the two views might be different sizes, so the document window might have to resize itself. So, we're going to animate the window resize. Third, we're going to fade in and expand the new view.

And now, once we've created all three of our animation dictionaries, we're going to concatenate them into a single list. Then we can run the animation. Once the animation is done running, then we can clean up any temporary state that we set up. So now let's go to the demo, and I'll show you this in action. So, let's go to the expandableviewcontroller.m file. And this file contains the logic for controlling our detail view, expanding and hiding our detail view. So all that logic is contained in the updateView method here.

So the first thing that I need to do is there could be a previous animation already running. So if that is the case, let's go down here, and I'm going to add some code. just handled that case. Now, if previous animation is already running, I will set its progress to one to display its final frame, and then I'm going to stop the animation.

So now let's start and create our three animation dictionaries. The first one I'm going to do is Here, we are abruptly hiding the old view. So now I want this view to fade out. So I'm going to replace it with an entry in our, a div. and Before I do that, I need to calculate the old view's final frame. This code calculates that. Then I'm going to create its dictionary. The target key will be the current view. The effect key will be FadeOutEffect. And the end frame is the frame we just calculated above. Lastly, I will add the dictionary to our array of view animations.

Now we're done with the first dictionary. The next one is our window resize dictionary. So here we're resizing the window. We want the window resize to take place simultaneously as our other tree animation, so we're going to replace this with another entry in our list. So, first thing we need to do is we're going to nudge the Windows frame to a non-integral value. Now, this is actually sort of a workaround for a bug in NSView animation.

The reason is, If the end frame of the animation is the same as the frame's current, is the same as the Windows frame, then it won't actually animate anything. So we're going to do a little tweak here. So then we're going to create the dictionary. The target key will be the document window.

and the end frame will be the frame we nudged up here. And then we're going to append the dictionary to our list. So, lastly, We need to create a dictionary for our final animation, which is the fade-in of the new view that we're going to show. So here we're going to replace this code where it abruptly unhides the new view, and as before, we're going to create the new view's start frame, calculate the new view's start frame.

So this code is actually pretty complicated. I don't have time to get into it here, but you all have this sample code. You can take a look at this yourself. And as before, we're going to, once we've set up our frame, we're going to create its dictionary. Its target key will be the new view.

And the effect will be Fade In. And then we're going to give it the start frame and the end frame we calculated above. And last, we're going to add it to our list of view animations. So now that we have a list of three animations, we can create the animation and run it.

So I'm going to go down here and we're going to instantiate an instance of NSV animation using our array that we created above. Then I'm going to set ourself as the delegate so that we get called when the animation stops or ends. And then I'm going to start running the animation. So now notice before, we used to have this auto-resizing code here. Now, this no longer makes sense to be here because at this point the animation could still be running. So we're going to move it down to a later point.

So now, since we're the delegate of the animation, when the animation stops, we're going to get called. So we're going to take this opportunity to do some cleanup. So in the animationDidStop, we're going to invoke animationDidEnd and we're going to implement animationDidEnd. And we can clean up all the temporary state we've set up, including the origins. We've changed the origins around during the animation.

We need to tell the table view to redisplay itself, et cetera, et cetera. And then we release the animation and set it to nil. And now at this point, we can go back and restore the resizing behavior of the other views in the window. So now, that's it, we're done. So now, well, what does this look like? Let's run it.

And... let's add a bunch of items. Resize this a little bit. Now notice if I click the disclosure triangle, you see there, we have a nice sort of shrinking and expanding effect. If I click between the bank and the stock view, the bank view will fade out and the stock view will fade in.

And we also continue to do this sort of expanding and

[Transcript missing]

So that concludes my portion of the talk today. Go back to the slides. Next, I'd like to introduce Corbin Dunn, who will teach you all how to use Spotlight and implement searching technology in your applications.

Thanks, Hansen. So, my name is Corbin Dunn. I work for the Application Kit in Cocoa at Apple. And today, I'm going to show you how to add three cool new features to our Tiger application here. First, I'm going to show you how to add a cool search field to the toolbar on our little applications window. That way we can filter on and find exactly what we want inside of that document file. Second, well, think about searching. You're trying to find this one little file on your huge hard drive.

It can be kind of hard to do. So, in Tiger, we add this cool new feature called Spotlight, allowing you to easily find things. So, we need to tell the Spotlight database about our custom iSpin file format. So, to do that, we're going to go ahead and write a Spotlight metadata plugin.

The third and final thing I'm going to show you how to do is to further take advantage of Spotlight by adding a Spotlight search window directly inside of our application. So, let's jump right into it. A toolbar search field. What does that mean? As you see in this little screenshot here, we're adding this custom, an NS custom view to the top right of the toolbar.

And it's pretty easy to do. So, adding a search field. Oops, let me go back. Okay. First, how do we do it? Inside the my document nib file, we will add an NS custom view to the nib. On-site of that, we will add an NSSearch field inside of the custom view.

Then we're going to need to access it in code, so we will add an outlet that connects the NSSearch field up to the My Document header file and access it in code. Finally, new in Tiger, we can add some bindings and we can bind the NSSearch field and have it automatically filter the array controller to see exactly what we want to see.

What are the bindings? Well, the bindings that we'll be looking for is the predicate, which is done with an NSSearch predicate. It looks like this thing you see here, this description string contains with a little bracket 'c' dollar sign value. I'll go into further exactly what that is in a little bit, but that predicate is going to be our description, which we want to search on. Once you have one predicate bound, you can bind to another one. So the second predicate that we're going to bind to is going to be this type contains dollar sign value, and that's going to be for the category.

So I'm going to present a little bit more information on NSPredicate a little bit later in the session, so you'll have to bear with me for just a second. Finally, to add the actual custom view, we'll access that outlet and inside a code we'll create the toolbar item and it'll all fall together. So let's go to a demo on how to exactly do this.

What I have here is the iSpin04 project. And inside of it, I'm going to go ahead and double-click on the mydocument.niv file and open it up in Airface Builder. So, we need to add an NS custom view here. So, I'm going to go over and drag a custom view on over to the my document nib file.

And let's just rename this to 'Search View'. Alright, so we have that search view there. Now if we go over to the text section, I'm going to go ahead and grab an NSSearch field over here, drop it on down to the search view. We'll make it a little larger.

Alright, and we want to access it in code, so we want to have an outlet for it. So I'm going to go over to the files owner, is the 'My Document'. I'm going to go to the classes. Go to the Outlets section in the Inspector, add a new outlet, and this is going to be the Search Field outlet. And what is the type? Well, it's an NSSearchField, so we're going to fill that in.

All right, and I'm going to go over to code, and we have to add the same type of thing inside the header file. So inside of mydocument.h, so here's the mydocument header. So inside of here, I'll just paste in the NSSearchField outlet. All right, now back over to Interface Builder.

So now we need to actually hook that up so I can control drag from the files owner on over to the search field and this is the typical outlet hookup stuff that you're probably used to. Now we need to do some bindings. So we can click on that NSSearch field, go over to the binding section, and now, new in Tiger, we have this predicate binding here, and if I click bind, The key path is the key path in our model that we want to filter on.

We want a display name of 'Description'. I can cheat and drop down the model key path section and see what's in my model. One of the items is 'Description' string. That was the name of my model. I'm going to replace the word 'key path' with 'Description' string. After contains, I'm going to add a little bracket 'c' so it's case insensitive.

So once we have one predicate bound, we can bind another predicate. So let's go ahead and bind another one here in a similar way. This one, we're gonna call it 'category'. And again, the model keypath, I'm not sure what the keypath was, so I can kind of drop it down and try and figure it out. I guess category is type. So I'm gonna go ahead and make this type contains, it's really tiny, I'm sorry, c value.

Alright, so that hooks up the predicate bindings. So now we can go back over to Xcode and actually create the item. So, inside of Xcode we have a category that does all the toolbar work. In here, the first thing that you see, there's this line here, which is an identifier, search toolbar item identifier. We're gonna use that to identify our search toolbar item.

We have a couple delegate methods in here. Here's the first important one. The toolbar, item for identifier, will be inserted in the toolbar. And what you see here is you can see that it already has the, or first thing it does is it creates a toolbar item. and it's creating the specifics for the add button.

It creates the specifics for the delete button, for the save button. And so we want to add a section for a new search field. So inside of here, I'm gonna see if that item identifier is the search toolbar item identifier. And if it is, oops, And if it is, we're going to add some code here.

So first I'm going to set up the standard properties. So I'm going to set the label to be our search, set the palette label and set the tool tip. But the thing that really does the work is using the toolbar item set view. I'm setting it to that search field outlet. So it's going to take that NS custom view and drop it directly into our toolbar.

Finally, the last thing I'm going to do is

[Transcript missing]

I should be able to open up something with a bunch of files. And now we have a cool little search field. So as you see, description category there, and let's search for a deposit. So it filters on deposit.

We can drop it down to category, and let's say I want to see meals. I must have typed something in wrong, as it's not quite working. Well, normally it would work. Oh, well anyways, so it would normally work, but I have a typo. Alright, so let's go back over to some slides.

Second thing I'm going to show is how to create a Spotlight metadata plugin. Hopefully you went to yesterday's talk where we went over the details of exactly how to do this, so I'm going to quickly cover it. But what this does, it allows the Spotlight database to know about our custom file formats.

How does it do it? It does it via UTI, a universal type identifier. And in this case, it's com.apple.ispin.document that identifies our iSpin documents. With that, we can write an importer, or sorry, a Spotlight metadata plugin, which will fill in custom attributes. mditem.h has some custom attributes defined for us, and we can fill in the KMDItem text content for the particular text content of our file.

We can also define custom attributes. For our iSpin files, we can fill in the total balance. So we'll define one called com_apple_iSpinBalance, which will contain the total balance for that file. So the Icebin metadata plugin. What's done is inside of Xcode, I just use the wizard to create a new metadata importer plugin project.

Then I added some proper entries into schema.xml and Info.plist. I'll quickly go over exactly what I did. Then the most important part is to fill in getMetadataForFile. There's a function called getMetadataForFile. Inside of it, we have a CFMutableDictionaryRef with called attributes, where we fill in the attributes that we want for that file.

Now, what will we do? Well, we're going to go ahead and load path to file into an NSData object and deserialize it. Our custom file format is really just a plist. So here we're going to use property list from data to deserialize it. We can then walk through each of the transaction items, concatenate all the strings together to generate one total description string, and we can concatenate all the balances together to find a total balance and then set the custom attributes.

So then once that's done, we'll compile it and install it. Just compile the project like usual in Xcode. To install it, you drop it into Tilda Library Spotlight or Library Spotlight, depending on exactly where you want and how you want it to work. So let's give a quick demo of that.

So, I'm going to go ahead and open up the Spotlight plug-in project. And inside of here, we have the schema.xml file. The key thing to notice, we're defining some custom attributes. So inside of the attributes section, there's that one I mentioned, com apple ispin balance, which is a CFNumber. There's also a few others added here.

Then for the types, we have our UTI, the COM Apple iSpin document. This is telling the Spotlight database that these are the types of documents that our importer actually imports. We tell it the attributes that we actually handle. So inside of here, we see that balance attribute that we defined, and we're going to use one of the standard ones, KMDItemTextContent.

Next, we'll notice that we have the transaction inside of our project so we can access our model. Then inside of 'Get Metadata for File', there's that standard 'Get Metadata for File'. Now it has a bunch of code here. Again, as I said, it iterates through everything. If you have any questions, feel free to come to the lab afterwards. I'm going to just highlight this key portion here.

We take that CF mutable dictionary ref and we just, via tool-free bridging, typecast it to an NS mutable dictionary. Use the typical set object for key in this dictionary pattern to fill in the total balance for that key that we defined, com apple ispin balance. In addition, I computed the total descriptions in this one string and I used the typical set object for key of the KMDItem text content. And that way the importer will import all those values for us. So let's go back on over to some slides.

Spotlight search window. This is the third and final thing I'm going to go over. As you see in that screenshot, I'm going to show how to add a cool little search window directly inside of our application. Now, we could search for all the files in the system for whatever we wanted or just in our application. We're going to do it just inside of our application by limiting it to just that particular UTI.

So what I already did here is I have the view search window menu item hooked up to display this window. And that window is just a new nib file with a window in it. And it has a, inside of the search panel, it has a table view with a couple of columns and in a search field on the top.

In addition, we need to control it. So I created this file, search_controller.h and search_controller.m that we're going to use to actually access it and code to it. So editing search_controller.h, what's inside of here? The key thing is NSMetadataQuery. It is the Cocoa entry port into our searches. So inside of our search controller file, inside of the header, we'll have an instance of it, an instance variable called _Query. We'll also have a search key. Now the reasons we have these is we want to expose them for bindings.

The search key we'll use to bind it to the search field, and whenever that search field changes, it'll change the search key. The query will be exposed so we can actually bind to the results. So exposing it for bindings. So you see the query, that's going to be how we expose it.

And what can we bind to? Up here is a little snippet of the header for NSMetadataQuery. In it, it has this NSArrayResults, which are the results from the search. The results is an array of NSMetadataItems. Now they are bindable, so we can directly bind to any of the attributes that we want to bind to. So we can find the name of the item, the name of the file system name, and that's what we'll go ahead and directly do. So in addition, we want to expose the search key for bindings.

That's done pretty standard. We'll have the search key accessor and a set search key writer. Inside set search key, it's going to call this custom function 'createSearchPredicate'. Now that gets back into what is NSPredicate? Well, NSPredicate, on its simplest sense, it's used to define logical conditions for filtering and searching. So what does that mean? For example, a description like 'Tom', that would return something where it matches 'Tom' or 'Thomas' or 'Tomato'.

You could use equals to filter on just equal to that and not like it. In addition, there's a descendant class of NSPredicate called NSCompoundPredicate. That allows you to combine two predicates into a new one. So you could have description like 'Tom', description like 'Fred', combine them together with 'or', and you have a new predicate, which is the 'or' combination of both of those. So I highly recommend reading the documentation for in this predicate because I'm just lightly covering it.

So creating the search predicate goes back to that custom method called 'CreateSearchPredicate'. Inside of it, I'm going to lightly go over this, and then I'm going to go over it again in code to further emphasize the points. So inside of here, the first line, it's creating a predicate where the KMDItemTextContent is like the search key passed in. So that's going to filter on exactly what was given to us as a search key.

The second line is creating another predicate, and that one, it's KMDItemContentType is equal to RUTI. So that's going to filter on exactly RUTI file types. So I use NSCompoundPredicate and predicate, or and predicate with sub-predicate to take those two, and them together to a one, and that way I limit it to RUTI for exactly what we want to find. Then I call querySetPredicate and queryStartQuery to get it all rolling. How does it actually work? Well, bindings is how it actually all works. We'll drop an NSArrayController down, set the content array to bind to query.results, which we exposed.

The table column one, we're going to just bind the value to the KMDItemFS name in the arranged objects. So that's inside the arranged objects of query.results. Another one from Balance is going to be bound to our custom attribute that we defined: com.apple.ispin.balance. So let's give a demo on how to exactly do this.

So this is back to the iSpin04 project. So inside of the search_controller.h file, you can see that here's that instance of NSMetadataQuery and also the search key NSString. So let's go over to the .m file, search-controller.m. So in the init, we're going to do the typical NS Metadata Query alloc init pattern that you see everywhere. Then we're going to create a sort descriptor.

This sort descriptor is going to be inited with a key of KMDItemFS name. So this will allow the query results to automatically be filtered on the file system name for us. So we create an array with that object and call QuerySetSortDescriptors. You could have other sorts of descriptors to sub-sort if you wanted.

Next, we need to expose some things for bindings. So the query is going to be exposed. Pretty simple. Then the search key is going to be exposed. The key portion in setSearchKey, so when the search key changes, I'm going to call this createSearchPredicate method. So createSearchPredicate. What are we doing here? Again, we want the KMDItem text content to be like that string passed to us. So I use NSPredicate, predicateWithFormat, given that predicate format, and the search key that we had already given to us.

Next, we want to limit it to just our files. So inside of here, I'm looking for when the CamDatom content type is our UTI, the com.apply.spin document. Finally, we have to take those two, use NSCompoundPredicate and predicate with sub-predicates to combine them together to create a new one, and then do something with it. So we'll go query, set predicate, and query, start query to get all rolling. Then the real magic happens inside of the bindings. So I'm going to open up search-pound.nib. Let me close some other stuff here.

Alright, so inside the search panel, we want an array controller, so we're going to go over to the controllers, drag an array controller down over to the search panel. We're going to go over to the bindings. And the content array, well, what did we expose? First of all, we exposed it in the search controller, so I'm going to select it. And the model keypath was 'query.results'. Hopefully I'll type it right. And next, we need to double-click on the table column's file name.

We want to bind the value to that new array control that we just specified, the arranged objects, and the model keypath, that's the KMDItemFS name, so it's the file system name attribute. The balance? Same type of thing, except the model keypath is going to be com, apple, iso, then balance. So it's that custom attribute that we defined.

The last thing to bind is the NSSearch field. So the value, I'm going to bind it to the files owner, the search controller. And let's see, it was called what? Search key? Alright, so that should be enough for the bindings. So now if I go back over to Xcode, compile this guy, run it.

So now we have... let's hide some stuff. View, search window, brings up that search window, and we could search for deposit. and look, it found that file directly inside of it. So that's an example of how to create, use searching directly inside of your application. Let's go back on over to the slides.

The makeover is complete! We went over how to add pasteboard handling, user interface improvements, and some cool searching enhancements. For more information, come to tonight's lab, the Cocoa Tiger Makeover Lab in the Application Technologies Lab tonight at 6:30. Drop by it, we can answer any questions that you have. For more information, documentation, sample code, other resources, developer.apple.com/wwwdc2005 Or you can contact these people: Matthew Formica, our Cocoa Core Data Evangelist, Cocoa Development, we have that mailing list, Apple Bug Tracking System, used for reporting bugs.