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 may have transcription errors.
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.
Let me go back to the slides. And we're going to do this in three parts. And I'll be talking about the first part, which is all about dealing with the paste board. So what we're gonna do to this application is we're going to 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. And 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. Returns an array of those. Step two is another fairly generic method, 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. Thanks.
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. And so I've added a category to my document class for handling the pasteboard. And here's the first method I told you about that describes the types that we're willing to write to the pasteboard. In this case, we have, first of all, a custom pasteboard type that we're gonna 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 gonna write the transactions out as archived data. Our application knows how to read and write that. For the files promise pasteboard type, the way that works is that to support promise drags, for that we write out the extension of the file type that we are willing to write out. And then the file names pasteboard type, well, We don't wanna do that right away. We don't have a file handy. We're only gonna do that if the receiver actually wants it. So we'll just say yes and do that later on, lazily.
And then for rich text, I'm not going to show you this right here, but you can look at it in your sample code. And this is somewhat interesting. We will write out a list of transactions to rich text 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's also another method that will write it out as tab separated values. Pretty simple. So Now there's one more method that we need for our promise drags. If we're 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, we'll 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's 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 data. Let's see if we can copy something. Say, copy.
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. plain text, or, well, let's see how about pasting it in, say, to Excel as plain text. So that all works. So let's go back to the slides.
And 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 gonna 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 gonna use them. Again, three steps to act as a drag and drop source.
Step one, initialization, we need to register drag operation mask, it's called, that specifies the kind of drag operations that we're willing to support. Step two, 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 type, 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. And, 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 want to 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 get back from a service. But we already know those. those are our readable and writable pasteboard types.
Step two, when the service menu is brought up, we need to validate specific entries in it. I'm not gonna show you that method, but it's really simple. It's like other menu validation code. And step three, there is no step three, 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 in a services entry to our info.plist. And this entry specifies all sorts of information about the service, like what the menu name is and so forth. One important thing I wanna note is that it gives a name, in this case, importData, that's gonna be used as part of the method that will be invoked if the service is actually used.
Step two is we need to register some object in our application to act as a service provider. And so step three, that object needs to implement the method that will be invoked when the service is used. And, for example, for the import data service, the full signature method looks like import data with user data and error. And 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. God bless you.
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.
that registers that I told you about. So we register for acting as a drag destination, we register the types that we're going to accept, our readable types. And for acting as a source, we register the drag operation mask. Now, in this particular application, we've 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. Thank you.
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 methods 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, then we'll be told the name of the destination location in the file system. And we call on the method that I showed you earlier that writes a file out to destination. And then we're supposed to return the names of the files, or in this case just one file.
and acting as a drag destination, 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, you will 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, say, of various items in here. And then we'll try dragging them over. And you notice the drag indicator appears, and I can drop it. And there they are. Or perhaps I wanna 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 a table, plain text. There are 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 iSpend application. So let me make a selection in TextEdit. this document, make a selection in here, and let's try our iSpend service, add to iSpend, and then it gets added, got added to this one, I think. 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, Hanson, who is going to tell us all about improving the user interface.
Good afternoon, my name is Hanson, 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 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 gonna 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 is Selection. That's exactly what we want. Now for the Model Key Path, we want the date. So we're gonna 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. 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 gonna go and drag in a date picker object. So we're gonna drag this into here. So now we go back to the inspector and change its style. We can select the style from the popup 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 for 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.
and let's add a couple of items. Select a few selections. So now you see that we have a, let's make this a little bit, now you see 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.
Can I get a slide, please? So now that you've seen how easy it is to create a date picker, I'm gonna go on to our next topic, Undo Support. So, Undo support is provided by NSUndoManager. NSUndoManager 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 gonna take advantage of key value observing to observe changes made to the transactions 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 documents 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 we're 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 insertObjects in transactions addIndexes. This method I'm going to implement for you in a moment. Now similarly, if the user decides to add a transaction, we'll go down to here, and we need to tell the undoManager a way to remove the transaction that the user just added. So we're going to, this time, register_undo with target, and we're gonna give it the method removeObjects from transactions at indexes. Now notice this time I can use the simple undo registration method, because this method, removeObjects from transactions at indexes, takes a single argument, which is a type of object. Lastly, 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 gonna 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... come up with a way to undo value changes. So, I'm going to go to the transactions.m file. Now, when the user wants to undo a change made to the transactions, the transaction model object, that is logically located in the transaction model. So we're gonna 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 go to the setter objects, the setter methods. So every time we want to register, we want to change one of the values in the transaction, we need to tell the undo manager a way to revert that object, that... changed to its previous value. So we're gonna prepare an invocation and we're going to give it the previous value of $$$.
to set it back to. So 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 gonna 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. and 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 undo's and redo's 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 NSFvueAnimation 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 gonna 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 gonna 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 expandable view controller.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 update view 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 gonna add some code. which has 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. 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. in animation dictionary. Before I do that, I need to calculate the old view's final frame. So 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 fade out effect. 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 resize, window resize dictionary. So here, we're resizing the window. So we want the window resize to take place simultaneously as our other tree animation, so we're gonna 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 NSViewAnimation. The reason is, If the end frame of the animation 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 gonna 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're gonna do we need to create a dictionary for our final animation, which is the fade-in of the new view that we're gonna show. So here we're gonna replace this code where it abruptly unhides the new view, and as before, we're gonna 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 gonna 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 gonna go down here, and we're going to instantiate an instance of NSV animation using our array that we created above. Then I'm gonna set ourself as the delegate so that we get called when the animation stops or ends. And then I'm gonna 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 gonna 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 contracting effect. So I just implemented that on state for you.
So that's basically all you really need to do to create your view animations. You just create a bunch of dictionaries and you concatenate them into a single list and then you run the animation. It's really that simple. So that concludes my portion of the talk today. Go back to the slides. And next I'd like to introduce Corbin Dunn, who will teach you all how to use Spotlight and implement searching technology in your applications. - Cool, thanks.
Thanks, Hanson. So my name's Corbin Dunn. I work for the application kit in Cocoa at Apple. And today I'm going to show how to add three cool new features to our Tiger application here. First, well, first I'm going to show 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 added 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. Second, On side 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.
And 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 gonna 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 gonna bind to is gonna be this type contains dollar sign value, and that's gonna be for the category.
So I'm gonna 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 gonna 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 gonna go over and drag a custom view on over to the my document nib file.
And let's just rename this to Search View. All right, so we have that search view there. Now if we go over to the text section, I'm gonna go ahead and grab an NSSearchField over here, drop it on down to the search view. We'll make it a little larger. All right, 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 outlet section in the inspector, add a new outlet, and this is gonna be the search field outlet. And what is the type? Well, it's an NSSearchField. So we're gonna fill that in.
All right, and I'm gonna go over to code, and we have to add the same type of thing inside of the header file. So inside of my document dot H, so here's the my document 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 file's 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, it automatically fills in the predicate format. And this is what I said I would explain later. It says key path contains dollar sign value. So what does that mean? Well, the key path is the key path in our model that we want to filter on. And what we want this, we're going to give it a display name of, let's see, how about description. And so what key path do we want? Well, I can kind of cheat, drop down the model key path section and see what's in my model. And one of the items is description string. So that was the name of my model. So I'm gonna replace the word keypath with description string. And after contains, I'm gonna add a little bracket C. That way 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 key path, I'm not sure what the key path 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, category.
All right, 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 gonna add some code here. So first I'm gonna set up the standard properties. So I'm gonna 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 gonna take that NS custom view and drop it directly into our toolbar.
Finally, the last thing I'm gonna do is, is use toolbar item, set min size and max size to the current size of that search field outlet's frame. That way it'll control the size. So I should be able to go ahead and compile this, give it a quick run.
Let's hide some other things. So... 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. Well, anyways, so it would normally work, but I have a typo. All right, 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 underscore apple underscore iSpin balance, 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 CF mutable dictionary ref with called attributes, where we fill in the attributes that we want for that file.
Now, what will we do? Well, we're gonna 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 gonna 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 gonna go ahead and open up the Spotlight Plugin 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.ispn 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 gonna 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 getMetadataForFile, there's that standard getMetadataForFile. 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 CFMutableDictionaryRef, and we just, via tool-free bridging, typecast it to an NSMutableDictionary. 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 gonna go over. As you see in that screenshot, I'm gonna show how to add a cool little search window directly inside of our application. Now, we could search for all the files on the system for whatever we wanted, or just in our application. We're gonna do it in 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 an NS search field on the top.
In addition, we need to control it. So I created this file, search controller dot H and search controller dot M that we're gonna use to actually access it and code to it. So editing search controller dot 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 underscore 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 custom-defined or the standard ones. like KMDItemTextContent, KMDItemFSName for the file system name. And that's what we'll go ahead and directly do. So in addition, we wanna 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 gonna call this custom function, create search predicate. Now that gets back into what is NS predicate? Well, NS predicate 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 NSPredicate because I'm just lightly covering it.
So creating the search predicate, it goes back to that custom method called createSearchPredicate. Inside of it, I'm gonna lightly go over this, and then I'm gonna 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 gonna 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 gonna filter on exactly RUTI file types. So I use NSCompoundPredicate and predicate, or and predicate with subpredicate to take those two and them together to a one, and that way I limit it to RUTI for exactly what we wanna 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 NSArray controller down, set the content array to bind to query.results, which we exposed. The table column one, we're gonna just bind the value to the KMDItemFS name in the arranged objects. So that's inside the arranged objects of query.results. Another one for 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 ispend04 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 gonna do the typical NSMetadataQuery alloc init pattern that you see everywhere. Then we're gonna create a sort descriptor. This sort descriptor is gonna 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 gonna call this createSearchPredicate method. So createSearchPredicate. What are we doing here? Again, we want the KMDItemTextContent 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 CAMD item content type is our UTI, the com.apply.spin document. Finally, we have to take those two, use NS compound predicate 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 gonna open up search pound.nib. Let me close some other stuff here.
All right, so inside the search panel, what we want an array controller. So we're gonna go over to the controllers, drag an array controller down over to the search panel. Inside of the inspector here, I'm gonna go over to the bindings. And the content array, well, what did we expose? Or first of all, we exposed it in the search controller, so I'm gonna select it. And the model key path was query.results. Hopefully I'll type it right. And next, we need to double click on the table columns file name.
We want to bind the value to that new array control that we just specified, the arranged objects, and the model key path, that's the KMDItemFS name. So it's the file system name attribute. The balance, same type of thing, except the model key path 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 NSSearchField. So the value, I'm going to bind it to the file's owner, the search controller. And let's see, it was called what, search key? All right, 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. So... 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, www.dc2005. Or you can contact these people, Matthew Formica for our Cocoa Core Data Evangelist, Cocoa Development, we have that mailing list. Apple Bug Tracking System, used for reporting bugs.