Mac • 52:27
NSTableView and NSBrowser allow extensive customization that can add polish and ease of use to your application. Learn to create user interface elements such as inline cell editors, custom column headers, and preview columns. See how to combine them with the dynamic animation support in Leopard and Snow Leopard to add the sophistication your customers expect.
Speakers: Corbin Dunn, Raleigh Ledet
Unlisted on Apple Developer site
Downloads from Apple
Transcript
This transcript has potential transcription errors. We are working on an improved version.
Welcome to Presenting User Data with Table Views and Browsers. My name is Corbin Dunn and I'm a Cocoa Software Engineer. So what is this talk about? Well, it's an advanced talk focused on a sample code application that we created called the AnimatedTableView. And we expect you to have a firm knowledge of Cocoa and an understanding of how to work with typical things like data sources, delegates, work with NSWindow, OutlineViews, TableViews. Hopefully, you know how to create custom cells, do drawing, and whatnot.
Most of this code is geared towards Snow Leopard, but a lot of the concepts will work on Leopard and although it is an advanced talk, a lot of the concepts you can take and just kind of run with and extract stuff from the demo and figure it out yourselves so it is good for beginners too.
So what are we going to talk about? Well, we're going to say how to subclass and enhance NSTableView, how to batch load a whole bunch of information like images, dynamically as is shown in the table view. How to add subviews to table view rows to do things which you normally can't do in a cell.
How to do a custom cell editor that edits outside the normal tracking view loop. We're going to talk about how to do some custom animations such as animating from a table view over to another image view inside of the same window, also, how to do a pop-up window type of animation in the custom cell field editor. Finally, we're going to discuss some new Snow Leopard-only API working with NSBrowsers, and discussing column previews and header previews with them. So, the focus of the talk is around this AnimatedTableView sample code.
I encourage you to download it. It will be available on developer.apple.com. You can go on the attendee website and find Session 110. You can see it associated with it. I encourage you to download it after the session, play with it, modify it, reuse the code. So let's take a look at that demo app so we can kind of know what we're talking about.
So I'm going to launch the application. And what we see here is, on the left we have a regular table view or subclass table view, have some group rows. It has an image, little color in it, and a title. And as we scroll through, we're batch loading images and using NSView to do the animation while it's loading. If I click on a particular row, we do an animation from the row in the table view over to another regular NSImageView.
We also animate some other properties like the opacity fades out and the background color fades. You can click on a particular color and we do a little pop-up, allowing you to select the particular color for the background color. So we're going to display or talk about how to create that and how to create an editor for your complex cell. You can also double-click on a particular image and we're going to pop-up another window which shows a custom NSBrowser.
We can modify properties of this by applying core image filters such as sepia tone and pixelate. And up at the top are our little previews such as a column preview and also a regular preview. We're going to talk about how to use new Snow Leopard API to create that. Finally, once you actually have a resulting image, we're going to show how you can go ahead and set it as the desktop wallpaper, so you can easily change the wallpaper.
So, let's understand how this demo application is created and take a quick look at the class hierarchy of our model for the application. What we have is we have a base subclass in NSObject called an ATDesktopEntity. It has a file URL and a title. But the real work is done in a subclass of it. We have a subclass that's going to hold an array of the children for a particular folder.
So, we have ATDesktopFolderEntity and that represents the whole folder in a-- everything in a particular group of images. Each particular item in the row is going to be represented by an ATDesktopImageEntity. This has additional other properties like a thumbnail image, the regular image, whether there's loading or not and the fill color that we're going to use for the desktop background. Now, for our views, here's a screenshot of that sample application.
So we have a custom in this table view subclass and it's called the ATDynamicTableView. Notice that I use a custom prefix here because we want to make it sort of like a framework where we can easily reuse that code and we recommend using custom prefixes for your frameworks and in this one I chose a two-letter prefix AT. In that way it won't conflict with any of our standard Apple-provided ones.
A three-letter prefix is also good to use too. So inside that table view, it's going to keep track of other subviews for things like this little spinner. The actual other rows are going to just be a regular cell. It's a custom NSTextField subclass called the ATImageTextCell and that normal text field cell is going to do the normal type of drawing.
The rest, the bits, we're actually going to delegate out to other cells. So inside this cell, we're going to use subcells to draw the image within NSImageCell and we're going to have another custom cell subclass called the ATColorCell that draws the color. So it's got little color swatch in the color name.
All these color names are really just standard things I got from the NS color list. I'm just printing them out directly, so it's something that we provide an app kit that you can iterate through for finding standard colors. Now, this pop-up window is going to be a custom NSWindow subclass. We're going to call it the ATPopupWindow.
The reason we subclass in this window mainly is because we want to overwrite it can become key windows so you can interact with the keyboard and have the focus go in the table view for selecting a particular row. But since we're also subclassing there, it's another good place to put that pop-up code animation.
So we'll show how to add that type of code in. Inside of it we're using that same cell, the ATColorCell, to do the actual drawing of the color. Of course, in model view controller design, we have a controller that hooks everything together, so we'll have an ATContentController. This is the main controller for the application.
And it links that model over to the views. In addition, when I double-click on the image on the right, that's how the browser window came up. We're going to use another controller, an ATFilterBrowserController that controls that browser, and what it does is we provide a sourceImage for it and let the user choose particular pass in the browser and it generates a target image called the filteredImage with a whole bunch of core image filters applied to it that we can then get back out of this controller.
Finally, the purpose of the demo app is just to change your desktop wallpaper, so down there at the bottom we have that little change of wallpaper button. So, how do you do this? Well, it's really easy to do on Snow Leopard. We have this new Snow Leopard NSWorkspace API, setDestkopImageURL, forScreen with some options, and an error. And we're going to pass in a few options or really one option, like here are some of the ones you can provide.
You can change things like the ImageScaling allow it to clip or not. And the one that we fill in and care about is the NSWorkspaceDesktopImageFillColorKey so we can set the fill color behind the image. Subclassing NSTableView, so the first thing we're going to talk about is how to go ahead and batch load your cell contents for everything that you see in the table view and only in that visibleRect of the table view.
Now, maybe you're thinking, well, why don't we just use tableViewWillDisplayCell? Isn't it the place where you're going to be using for the cell drawing? And that's a good idea but it's not a great place to do the way voting of images and other things, because that method is called more times than you may normally expect. We have to call it so we can get a fully prepared cell to do multiple operations, not just drawing but for other things too in a table view. For instance, type selection.
What if you type that character Z in your table view? We're going to have to try and find a match for that particular row and the way that the table view does this, it gets a fully prepared cell using the preparedCellAtColumn:row. Now, that has to call your willDisplayCell method.
So it may not actually be displaying that cell at that time, so it's not really a good place to load images. In addition, we also use it in other places like if you double-click on a column, it will auto resize it to fit and there's some other ones that we use it for too.
So batch loading of cell contents, how are we going to do that? We want to preload everything in the visibleRect of a table view. As you may be aware, there's actually a clip view that's clipping out other contents that you can't see there. What we want to do is we want to cache an NSRange of just those visible rows.
So the range will have the range allocation indicating where it starts at and arrange that length for how many rows are actually there in the visibleRect. We want to make this code really easy to reuse. So we're going to subclass a table view and subclass its delegate and provide more extensions to the delegate.
On Snow Leopard, we use formal protocols so we subclass the NSTableView delegate and create ATDynamicTableViewDelegate. That adds a new method, DynamicTableView, changedVisibleRowsFromRange, toRange, so that way things can be loaded dynamically as they're needed for a particular visible range. Notice here that I used the prefix DynamicTableView.
Again, you don't wanna conflict with standard app kit or Apple-provided classes or delegate methods that we provide, so you don't wanna use the standard prefix TableView, because what if we introduced it later and it conflicts with one that you happen to have the same name with? So, how do we actually implement this in the cable view? Well, we're gonna subclass that table view at an NSRange ivar to keep track of the visibleRows. And you can also see here in this little snippet of the ATDynamicTableViewDelegate provide as an override declaration of the one in NSTableView.
The best place do this batch loading of cell contents as lazily as you can, is to use a new Leopard API called viewWillDraw. So, what you're going to do in viewWillDraw is you want the superclass to do whatever work it was going to do. And in this case, table view actually doesn't-- delayed layout. So, it's always important to call super viewWillDraw whenever you override that method. Now, we just grab the visibleRows and the visibleRect.
We figure out if the visibleRect is actually changing-- or sorry, if the visibleRows are actually changing. So, if that row range is different, then we can just tell a delegate, a delegate the dynamicTableView change the visible rows from some old range to a new range, really easy to do. I'm glossing over some facts here which are actually included in the demo such as you normally do a response to a selector to make sure that the application delegate response have a particular selector before calling it See the app for the details of that.
So, what does our demo app do in this method, in this changedVisibleRowsFrom a range to a range? Well, we have our table contents, array of everything in that table, so we want to create a subarray of just what you're seeing. So, we're going to store that in the observedVisibleItems. So given that subarray, we can just iterate through it and tell each of our model objects that we want them to load their images. In addition, if they haven't actually loaded it yet, they're going to change at some point.
So, we're going to use KVO to add an observer for the thumbnail image path on that particular model object. Now if you're familiar with KVO, then looking at the outline you may think, oh, you're going to be observing those items, you have to stop observing them at some time, and that's why we have the old visible items stored off.
So really, the first thing you would do here is you'll iterate through and remove observer for that key path of the thumbnail image first and then you'll do the work on the previous slide that I just showed you. So, the model is going to do all of the work to actually do this loading of the image.
So, here is the loadImage method in our model objects. What we're going to do here is we're going to use a shared NSOperationQueue to do all the heavy lifting, so all the work of asynchronously loading the images in the background thread, and we're going to use blocks. So the specific code highlighted here is the actual block callback that will get called in the background thread.
Now, why are we using a background thread? Well, we have this helper method, ATThumbnailImageFromImage which actually takes your regular image and converts it-- creates a thumbnail image from it which will potentially slow it can block, and you don't want to stall your UI to do that on the main thread.
So, that's why we do it in the background thread. If you're doing it on a background thread, you want to synchronize things. So, we want to synchronize the accessing of the image in the image loading variable so that they're always in sync and that we don't do this twice and overstep things.
So, that's why we use @synchronized only in this method. We also use it when we're actually going to assign the variables, because remember this bit of code's helping in the background thread during a callback when the thread is doing its work. The last thing this does is it assigns the thumbnail image.
Now, we're just using KVOs, so that's going to send out a KVO notification when it changes. As you remember from before, the controller's watching for those KVO notifications, so it's going to get an observed value for key path of object with a context. Now, these KVO notifications sent from the background thread because that's where it was changed.
So, we only want to modify the UI on the main thread. So, we will do a performSelectorOnMainThread with the object of the model object that changed. We don't care about waiting until it's finished. But one thing we do care about is the modes. We're going to pass in an array with the RunLoopCommonModes. That way that updating the UI happen when normal run loop operations are happening or a model run loop happens.
It's also good to use the modes of-- common run loop modes may do a performSelectorOnMainThread or just a regular performSelector on delay. The actual work that will happen on the main thread is what's really easy. We just grab the row from our model or go from our array to figure out what row it's going to be. We call a new Snow Leopard API reloadDataForRowIndexes, columnIndexes to just reload the contents of that cell. Really, all it's doing is redisplaying it at this time.
So, if you're targeting Leopard, you could easily do a setNeedsDisplayInRect on just that cell that needs to redraw. Let's move on and talk more about how to add subviews to a table view, now that we've talked about batch loading of cell contents. Let's go back to the demo and take a look at that again so we know exactly what we're talking about.
So again in the demo application, we have a normal cell that does our-- or normal complex cell that does our drawing of the image and everything else. Cells are great because they're very fast for performance, but you can't do certain things like maintain certain state for animations. So, we want to use an NSProgressIndicator to do this animation on the left as the thumbnail images are loading.
So as I said, some problems that you have in table view, well, no easy way to do animations and things that you want that have animations that you can't use because we don't have a cell that does it, NSProgressIndicator, NSPathControl. Path control has the cell but no way to do the animation because the animation scored in the view which maintains that state.
So, how are we going to do this? Well, we want to have some really easy to use reusable code. So, we're going to have a delegate method to do it. We only want to show views in those visible rects that we're actually are displaying. It doesn't make sense to have views on screen that you can't see or can't interact with.
We also want the table to manage everything for us so that we don't have to do that work. So, let's go ahead and extend that delegate method or extend the delegate protocol that we showed earlier will add a method, dynamicTableView, viewForRow. Now here, we're just going to have for the demo purposes a simple one view per one row. You could easily take this application and extend it to have multiple views per column on multiple columns for each individual row, or for simplicity sake, the demo just has one view per one row.
The ATDynamicTableView subclass is going to keep track of the views with a dictionary. So we're going to add some more ivars on MutableDictionary views in visibleRows, and also Boolean to keep track of when we have to update them or not. Here is an example what the dictionary looks like.
So inside of it, there are going to be keys. The keys are just NSNumbers where the number is the row that maps over to the view for that row. These first three rows don't actually have a view so we're going to put NSNull null in the dictionary to indicate that there is no view for it.
The other ones are going to have the view stored there. Again, a great place to do this lazy view edition is viewWillDraw. This is the place that you want to add subviews, do setNeedsDisplayInRects and other last minute work. You don't want to do it in drawRect or draw interior frame of a cell. That's too late to do it. The appropriate place is viewWillDraw. So as before, the first thing that we would do is we're going to update the visibleRows. In addition, we'll also set a Boolean whether or not the views need update.
So, obviously we need to update the views whenever those visibleRows are changing. So, once they do need updating, but we want to remove visibleRows which are no longer visible. So, it's really easy to do that. We create index set with the old visibleRows, subtract any that are still visible and that gives us the resulting index set of what's no longer on screen. We can just iterate through that index set.
So, we'll just iterate through each row in index set and create an NSNumber for that row, see if it's in our dictionary. If it is, we remove it from the super view and remove it from our dictionary, so that view storage is just gone. Now, the code to actually add in the views is really easy.
We iterate through all the visibleRows, create a key for, again, that row and access it to see if a view is already there. If it's not, we ask the delegate. Hey, dynamicTableView or viewForRow, give me a row back, or sorry, give me a view back for that row.
And if it did give me a view back, we're going to add as a subview and store it on our dictionary. If it didn't, we don't want to call the delegate back again and again, so we're just going to use an NSNull and null place holder so we don't call it back again and again.
And of course, you can't put nil into a dictionary, so that's one of the reasons why we use null as a place holder.
So, other key places that you actually want to do some updating of the viewsNeedUpdate variable. Things like reloadData, noteHeightOfRowsWithIndexesChanged, and another in new Snow Leopard API, reloadDataForRowIndexes, columnIndexes.
That way, you know when it needs to be updated. So, what does the actual delegate do for this? Well back to that sample application, it's going to implement in its controller, dynamicTableView, viewForRow. And what it wants to create is a progress indicator for all its model objects which don't have an image or a thumbnail image loaded yet.
So we grab the model object for that row, see if we actually don't have the thumbnail image created yet. If we don't, we're going to alloc init a progress indictor for it, and I'm going to snip out and ignore some code where it's actually going to set the properties for the progress indicator.
Now, we want to place it at the right location within that cell. So, we're going to grab the frame for the cell of that column row and just place the progress indicator's frame right in the center of that. You could place it wherever you want if it's some other type of view or some position that you want that's not centered.
This is just left up to the delegate for that purpose because the table doesn't know where you'd want that view inside the cell. So, we just talked about adding subviews to a table view. Let's move on a little bit and talk about creating custom cell editors for our table view. Again, let's take a look at the demo so we can understand fully what we're talking about.
So in the demo application, when you click on one of these particular colors, we do a neat little pop-up window and it allows you to select a particular color, and it changes that color in the cell. Of course, the cell has multiple values in it. You could potentially edit the value here and you could potentially change a different color in the same cell.
So, we have multiple values that we want to edit in one single cell. So, that begs the question, how do you change those multiple values in one particular cell? The normal object value is just going to be the title that you see there for the cell, but this particular cell has multiple values like the fill color.
You could also imagine making the image changeable too. How do we change that value and somehow tell the delegate or data source that this cell had one particular different value than normal object value that changed? And, how do you actually edit that cell outside of normal tracking, but what is normal tracking? Normally when you're editing a cell like a button or a slider or something else in a table view, you click and you hold your mouse down in it. The cell tracks completely as you are holding down that mouse until you let it go, and then the object value changes. But what we're doing here is we're going to actually change that value outside that normal mouse tracking loop.
So how do we do that and what's a good way to do it? So how do we solve these problems? Well, we want the cell to just talk to the table and tell it when it needs to do things. So we'll extend our ATDynamicTableView and add a couple more methods for the cell to call. This property or these methods here are willStartEditingProperty forCell, didEndEditingProperty forCell successfully, but the key thing here is that the cell is going to tell the table view what property it's going to be editing.
That way, the table view can deal with multiple properties from a single cell. So, how is this going to work? We have this one cell here. You click on it and it is going to say to the table view, hey, we're editing the fill color. And it's going to do it by saying, tableView willStartEditingProperty:@"fillColor" forCell:self. What does the table have to do in this case? Well, the table view needs to save off the editing cell, save off the editing row, and column.
Now, why is it going to do that? Here is a snippet of the header where it's got the extra ivars for the row and the column and the cell. We override preparedCellAtRow column-- or sorry, preparedCellAtColumn row. And if that editing row is the one that we are actually editing, we want to return that same cell as being edited right then. That way, you could have something which is dynamically changing the properties of that single cell outside of its normal tracking loop and it's constantly reflecting and updating the values right then live.
So if that is the particular row column that we want to know, we just return or retain auto-release version of that cell. The cell is actually going to do its editing of its property with its own controller. So we created an AT color table controller inside the demo app that edits cell color properties. So, all the controller does is it completely abstractly pops up a particular color picker thing around custom one at a particular location. The user can then go ahead and change the actual selected color to something.
The controller is watching for that color to change. After that color does change, the controller can then go ahead and tell whose watching or using the controller, in this case it's the cell. So the cell knows that its color went and changed. So, now the cell can just tell the table view, "Hey, I'm done editing the fill color with a tableView-- didEndEditingProperty:@"fillColor" forCell:self successfully:YES. So, you can imagine other places in the demo code. We tell it successfully no when you hit Escape or clicked away from that particular pop-up. The table view itself now needs to update the data source.
So normally you have a delegate method, tableView:setObjectValue:forTableColumn:row. We're going to extend that delegate method and have an implementation that's very similar, dynamic tableView:setObjectValue:forTableColumn:row property. So, we can tell the table view when its updating one particular property in a multi-valued cell, Now the controller, here's the implementation of that method, and what it's going to do is it's going to grab its model object and see what's property changed. So, if the fill color changed, it's going to go ahead and update its fill color model object property.
So, we just talked about some custom cell editors inside table view. Now, let's go ahead and talk about some cool custom animations that you can do and how to actually go and do them. So, how do you actually animate from a table view? Again, let's take a look at the demo to see exactly how it's done.
So inside of the demo application, the animation we're talking about is when you click on a particular row, we're animating several things at the same time here. I can slow down with the Shift key, and first of all, we're animating clearly the image from the table view to the image on the right. But also look at the background color above. We're fading from one background color to another and at the same time, the image that was there has an opacity fade or an alpha-value fade that's going out at the same time.
So, those are the animations we're going to talk about. So, how are we going to do this? We're going to use an overlay window that starts at that location in the table view with an image-- the same image as what we're displaying. We're then going to just use the animator or proxy object to animate that windows frame from one location to the other, and at the same time, we can animate the opacity or the alpha-value out of the image and also animate the background color from one color to another.
So, that begs the question why are you using an overlay window? On Leopard, we support views where you can have one sibling view that overlaps another sibling view if it's got a higher Z-order in the view hierarchy. So, you could have just have one view that overlaps and moves around from one to the other and that's a great way to do it and it works really well in those cases. But in this case here, we want to do that opacity and fade and background color animation and it's really easy to do those animations on that side view with core animation, we can just animate the alpha-value really easily.
So, we want to do setWantsLayer:YES, so that we can actually do that type of animation. Now the table view on the other side, well, it's not gonna be a layer back table view because it doesn't need to be and then there's a problem. How do you have a nonlayer back table view work with a layer back view and have one sibling view that overlaps them. That actually won't work because the view layers aren't quite the same. So, that's a problem which we're trying to solve and why we're using overlay window to do it.
So, how do we actually go ahead and create an overlay window that does it? Well, it's really easy to do. We're just creating using alloc init with a-- to create a simple NSWindow.
So NSBorderLessWindowMask is passed to the styleMask. We don't want it to be opaque and we want to have a background color of a clear color, so that way anything beyond the window's contents are just completely invisible and you can't see them or click on them. The main contents for the window is just regular image view that has the same contents as the starting image of the thumbnail. So really, the work is just figuring out how to move this window from one location to another. We have that custom cell there and we're going to create a special method to figure out where we're going to start the animation from.
And we're going to call it screenImageRect for a particular row. We can create a completely prepared cell for the particular row column and given that, we can find the frame of the whole cell. Once we know the frame of the whole cell, we can have the cell tell us the imageRect for particular bounds and that gives us the cellImageFrame within that overall cell frame.
We can then just convert to the rect coordinate system for the window. So we do a convertRect to the View and convertBaseToScreen to get the screen coordinates for a particular rect, which is the appropriate rect for the window. Now how do we set up the animation? Well the main thing is that we want the animations to remain in sync. So we bring that window forward and we create a grouping for animation context.
We set properties that we want in animation like the duration then we just use the animator proxy object to do the animation for us. So we do things like the frame window animation, the alpha-value fadeout, and the background color changing from one color to another. Now there's a problem here is if you are using a normal NSAnimation, there's a delegate method to know when things finished. Or if you're using CoreAnimation animation similarly, there's a delegate to know when it's done.
So how do we know when these animator proxy ones are done? Well there are a couple of ways to do it, one easy way is to use an NSTimer to figure out after that work is done to actually do a particular cleanup work like remove that window from things. You could also do a performSelector after delay with an appropriate delay, but for this purposes, in the demo app it was easy to use a timer so we could actually cancel, rewind it and control it a little better.
So when the timer is finished, meaning the animation is done, we can do some cleanup like we can set the final image in that big image view, set the alpha back to 1.0. And then we can force the window to display so that way it's completely displayed before we remove our animation window actually out and disappear.
This way everything moves and it's smooth and you don't get any flicker. So custom animations, how do you do the pop-up window-type animation? We just finished talking about the animating from a table view to another image view. And let's take a look at that pop-up animation again so we are all on the same page and know what we're talking about.
So now the thing we want to talk about is this little pop-up animation. So how do we create, oops, a pop-up window like that?
[ Pause ]
So the first thing we are going to do is we are going to cache the contentView as an NSBitmapImageRep. So given that content view there we are going to, again this is an NSWindow subclass, we are going to grab the content view, save it off for later.
And then take our visible rect, what we're seeing and create a BitmapImageRep for that display rect and then cache the contents of the view, the table view that we're seeing there, into an NSBitmapImageRep. After we have that we can replace the contentView with a layer-backed view, so it's just going to have no contents at that point.
The way we are doing that is just a regular NSView which has setWantsLayer:YES as the content view. Inside of that we can add a child layer to actually do the animation, so we are just going to add a regular CALayer that's a sublayer of it. That's going to be the thing that actually is animated within that content view.
So the code for that is we just create a regular CALayer layer, set a bunch of properties that we want on it. But the key property for these purposes is we're taking that BitmapImageRep, grabbing the CGImage from it and saying that as the contents. So that way that layer has an image for the view, which we want to actually see and animate.
We take it and we shrink the view down with a transform. So there's a helper method we wrote that creates the transform for a particular scale. So we start at a really small scale and then we just add as a sublayer. So we are starting at a really small scale and we want to use CoreAnimation to animate.
That layer is transformed to the bigger size. So again, it's great to have a helper method that does this, so we have a method called addAnimationToScale for a particular duration. So we create a basic CAAnimation animation to do this for us. We set the fromValue to be our current transform, so it's going to be that current small transform that we created. Set some properties like the duration and delegate, and I'll explain why we set ourselves to delegate in a second.
Then on the layer itself, we set its final size to the final scale for that animation. In that way if you end an animation, the layer size is the ending size that we want it to be. Then we go ahead and add that animation to the layer, so the layer is going to do the animation for us.
Now we want to create that pop-up effect, so how can we make a particular pop-up effect? Well we can take those animations and chain from one animation to another. So inside of animationDidStop finished, we're going to get this callback when the animation is done, because we are the CAAnimation's delegate.
So we can just keep track, well if we are actually doing a growing we can go ahead and say well we're not growing anymore, we're going to be shrinking. And then we can add an animation to the particular shrink scale that we want with a particular shrink duration.
Then when that animation is done we can say well if we were shrinking we are going grow back to our original 1.0 scale and add an animation for that. Finally, after they're all done we want to do some cleanup, so we'll have a cleanup and restore views method. So in the cleanup method we want to restore that original content view so you have the original table to actually interact with. It's really easy to do cleanup.
So inside the cleanup we're going to disable some screen updates and restore things like the original content view, release some of the animation views and remove it from the super view. And some other minor details which you can look at the demo app to see what we do.
But this makes it restore it without any flicker. So now that I've discussed a bunch of things about table views, I'm going to hand it over to my colleague Raleigh Ledet to talk about Custom NSBrowser API.
That was great. So we have some new API in Snow Leopard for NSBrowsers that will hopefully make your life a lot easier and allow you to do some really nice nifty neat new things.
So Corbin showed you a whole bunch of stuff with table views and how to add some custom cell editors and do some animations. And we're going to focus on NSBrowser and we're going to focus on some new Snow Leopard API. Let's get a refresher. We're going to go back to the demo station and show you that portion, here we are.
So here's our sample application and we'll find us a nice image here, let's see, here we go, wait for the fish to come up. We double-click on the fish and we'll bring up our browser and we'll pull up three columns so we can see that. So we have here a header column, so this is our sourceImage so these controls are disabled. And we'll go ahead and do a hue adjust so we can change the color because I like blue, here we go. And then we'll go ahead and add a blur onto this and soften it up just a little bit.
And now you see over here a preview column, and we can hit the Apply Filter button, and it goes back and we can now set this as our wallpaper and that all works. So we're going to show you how to do those header columns in browser and how to do those preview columns using some of the new API.
[ Pause ]
So the new API is item based, so we are going to cover that and we'll cover the custom headers and the preview headers. So the new API is very similar to the API that already exists in NSOutlineView. It's an item-based API as well. We've made one enhancement in that the items don't have to be unique for the new NSBrowser. They still have to be unique for NSOutlineView but they don't have to be unique, so you can have the same item in multiple columns.
That works out really nicely. The implementation no longer uses NSMatrix, so if you-- in the old version you would have to push the content you wanted onto the matrix, matrices or override willDisplayCell and put all the content there and keep track of all that. So now it's set up more like a data source and we'll just query for the information, which means that the old matrix API will no longer work unless you are continuing to use that old code and that old method.
So you can't mix and match the new API and the old API. The old API is still there for backwards binary compatibility and the header view in the preview columns that we're going to talk about only work with the new API.
So there's some existing API that still works no matter which mode you're using, the matrix or the item-based mode, so you can still set your class cells, you can have a custom cell and you could still override willDisplayCell and you can make custom additions to your cell that way. So, we're going to talk about the new API in relation to how we implemented it for this demo application.
So we're going to look at our class hierarchy of application. That whole table view is controlled by a window controller that loads that from a NIB and that's our custom ATFilterBrowserController. It has a sourceImage and the filteredImage that Corbin had mentioned earlier and it contains an AT, a root AT filter item and an AT filter item has each children and that's all of your different children for your columns as you go along. Each filter item has an input source and some various other properties like the resulting image and the localized filter name.
So for NSBrowser, you have a parent item for the next column. So, and in that column has, however many children are associated with that parent item and this continues on down the line, just like when you turn out a triangle in outline view. So, your next selected item becomes the parent for the next columns, children. And this is a new API that you actually have to implement and this is the API that we have in our controller class. So once you implement browser numberOfChildrenOfItem in your delegate for NSBrowser, we'll switch over to using that item based API.
We expect you to have all three of these methods implemented. So we asked for the number of children of a particular parent item and in this case we'll just return the count for the child items then we'll ask for a particular child of that parent item, a particular child index.
Again, it's real simple. We just take that item and we get the child items from it and return back that item, that index in that array. And then we'll ask for the object value for that item to put into the cell and here we'll just use the localized filter name for that property. So again, you just hand it back the items that you've already given to us as we go down the chain.
You can grab your properties out of there. It works really nicely. You also notice that these methods are really, really simple and unlike outline view that we're not worrying about nil as some of you might remember. Because by default, just like outline view, the initial parent item is nil. And you can have extra code in your delegate methods to compensate for that. So if the parent item is nil, you know you're on your zero-width column.
Well, in browser we've added an optional delegate method that you can implement that we will actually ask you for that root item and that's what we did in our controllers. We've implemented this so we've created our first child item. We supply that as the route browser, the route, excuse me, the root item for the browser using this delegate API.
And, so we know that we're never going to be given a nil to start off with and it cleaned up all that extra code that we would have had it do to make special case for nil. We load our root item lazily so we wait until the controller is loaded and the browser is actually trying to display something.
So if we don't already have one, we'll go ahead and create our filter item. Once we have a filter item, we'll just return our root item from there. You may also be asked if an item is a leaf item and what a leaf item means is it doesn't have any children and it can't have any children and you may potentially wanted to show a preview column. This is our implementation. The method is browser isLeaf of Item, I'm sorry is browser isLeafItem item and we just return if the child items for our item are counted zero.
Now, it works great for this sample application because there's only one item, the non-filter and it doesn't have any children obviously, so it's the leaf item. If you're doing a directory hierarchy, for example, you might have an empty folder that can have children. It just happens to have no children, so you need to have the right kind of logic for your application here. And so along with these new delegate APIs, we have some new APIs on NSBrowser itself to help you get it some different things and here is one of them.
Parent for items in column. So when the columns are changing, we needed to hook up the sourceImage from one column to-- the resulting image from one column to the sourceImage of the next column so we could chain our filters together, and so we do that by getting the parent item for that particular column.
So we know which columns are changing, we can get the parent item for that column and we have been using the item-based API to do this, and we can get the parent item for the previous column and we can go ahead and bind the resulting image from the previous to the sourceImage of the current column. So you have some column and for example column 1 and there was the parent item and that is what I was talking about earlier.
So you can always get the parent item for a particular column number and there is the short version of the API parent for item and column. So we're going to move on to the custom header views and they turn out to be really easy to use. This is incredibly simple. This is great, I love this.
And it's all based on the view controller and then you have your view controller and your view controller is going to load a NIB and that NIB defines what goes into the header for that column. So, view controllers have a represented object and we'll automatically going to set the represented object to the parent for that column and in this case, our represented object has all sorts of nice nifty properties because it's the AT filter item such as the input values and the resultingImage and the sourceImage.
So, all you have to do is you go into Interface Builder when you're setting up your NIB for your view controller and you could bind all your various properties to your represented object, in case we're binding the image there to the representatedobject.resultingNSIimage. And so now, the sliders are going to work and because they're bound to the input min and max values so they get the right range for the particular filter and they have the current value and they'll automatically enable and disable themselves.
And that's all you have to do and here's our implementation and the delegate method is browser headerViewControllerForItem. We just go ahead and create a standard view controller, that's all we needed in this case, and we need it with our NIB that we wanted for our header column and we return that for each column that is not a leaf. We return that for every time we're asked for a header view column and we wanted them to be the same.
If you don't want a header view in a particular column, you can just return nil. If you want different header columns for different items where you're given the item, you can inspect the item and decide to return a different view controller or just load in a different NIB if you're using the standard view controller. And that's it. That's all you have to do and preview works exactly the same way. If it's a leaf item, we might go ahead ask you for the previewViewControllerForLeafItem.
So if you implement this delegate method, you can return your own view controller there and you can have your different NIBS for different items or you return nil if an item doesn't have a preview column associated with it. So, that was the end of our implementation of the things that we've done with the sample code and here's a few more additional APIs that I want to go ahead and cover. We don't allow any setting of the object values but this is one of the new things that you can do with the item-based APIs.
You can actually do in-cell editing, so if you have it turned on, you can double-click the item, the cell and modify the change there and so browser will call back your delegate items, setObjectValue:ForItem and you can also modify the height of each row in each column. So, here's a delegate method for that where we will ask you for the height of each row. And finally, we want to look at how you can get your selection.
It turns out that, you know, in column 0 you might have item 4 selected and in the next column, you have row 1 selected and in the final column you have row 0 selected, and this looks a lot like an NSIndexPath and well, so we go ahead and we allow you to get and set the selection based on the index path, create an index path and you could set the selection that way and we'll fill out all the columns, or you can get what the current selection index path is.
Of course, you might have more than one item selected in your last column and if that's the case, you can get an array selection index paths or you can even set an array of index paths with setSelectionIndexPaths. Its important that on the array of index paths that only the last item in the index path changes for each item-- for each index path in the array and we check for that because you can only have a multiple selection in the last column.
So sometimes it gets-- you'll need to know what a particular item is in a particular row in a particular column and so there's a couple of different ways of determining that.
The first way is you might just say, "Well, I know the index path, so give me the item at that index path." And so, you can really ask for that item at index path or you might know the particular row and the particular column which some of the other API that were always-- we've already given you back, you can just simply ask item at row and column and we will hand that to you.
[ Pause ]
So, if you want to reload the children of a particular column, we made it real simple for you to do that and you can just call reloadItem if you already know the item and we'll reload just that items and we prepare the cell and we put the values there or you can reload a whole particular column, you just provide us with the column number and if you try maintain selection and if you reload columns there on the selection changes, we will go ahead do the right thing and wipe out all the other columns for you.
And that's pretty much the end of the new API browsers, real simple to use and it's pretty straightforward. If you know NSOutlineView you'll be a step ahead in using this API because they're very similar and I just want to point out that finder as you know is now Cocoa based and so this gives you the power to do everything that they're doing in the browser view mode and finder with the header columns and the preview columns and it works really great.
[ Applause ]
So, we had a good talk about subclassing, enhancing table view, talking about the batch load with cell contents, add subviews, doing custom cell editors, creating custom animations from one table view location to an image view, creating the pop-up animation and Raleigh just finished talking about the new NSBrowser Snow Leopard APIs. What we encourage you to do is to take that sample code, download it, play with it, reuse it, figure it out...