Hello. Welcome to "View-based NSTableView from Basic to Advanced on Lion". My name's Corbin Dunn. I'm a Cocoa software engineer.
So what are we going to talk about today. Well, new on Lion, NSTableView now supports NSView. You don't have to use NSCell, so it's much easier. You don't have to do any subclassing NSCell to do hit testing, drawing, whatever.
Now why is this great? Well, you can do easy design-time layout inside of Interface Builder and just do it all by adding in one view, another view, another subview. You can animate stuff because we can move views around really easily. You can customize drawing by just subclassing NSView and adding other things that you need. You can do other animations inside of the cell which were difficult to do before.
So what are we going to talk about? We're going to talk about layout, which is general NSTableView and how the new NSViews fit into it. Talk about construction, which is how to go ahead and actually create a view-based table view. I'm going to talk about bindings, so how to use bindings with this new technology. I'm going to discuss some customizing so how to do subclassing and add some cool new features to it. I'm going to talk about drag and drop, and doing some new multi-image dragging with the tables. And then finally, we'll talk a little bit about animating.
Now, when I say the word "cell", I'm just going to mean a particular row/column. If I mean NSCell, I'll go ahead and say "NSCell". So I just want to set that out there so you understand.
So let's just jump right into it and do a demo. And this is called the "Table view playground demo". You can go ahead and download it on developer.apple.com and play around with it. And I'm going to show a few features of the view-based table view. So right here this window is a basic view-based table view. It just creates the other windows for the application to show pieces of the UI, and it's created completely with bindings. There's actually no code really to do any of this. So I'm going to go ahead and hide that window.
So here's a basic view-based table view. It looks like any other NSTableView that you have. It has an image and cell, it does typical selection, and it's really easy to create. The basics here is that it looks just like any other table.
So let's take a look at a more advanced table view. This is the complex table view example inside the demo, and what you see here is as things scroll in, views animate in, which was difficult to do before. Up at the top, the headers can float over content, something really easy to do now. We can do animations so I can go ahead and select a row and delete it. We can do insertions, and we can do moves, so I can move from one row to another. Real easy API to do this. We can do things like animating the row height, and while the row height's animating, we can animate the views inside of it. It's really easy to do.
And of course NSOutlineView is just a subclass of NSTableView, so it has all these features too. It does a cool animation throughout the OS when things open and collapse, and a neat thing about this table view is we can do some insert animations. So I create drag images, and as I let go, a little gap opens up and the rows just slide on into place. I also show how to do reordering inside of it in an animated fashion.
So those are a few of the things that I'm going to highlight and talk about.
So let's go ahead and talk about layout. Now NSTableView layout, so here's a table in that sample application. Typical layout, we have NSScrollView at the top, and the scroll view's the thing that lets you do the scrolling. The next layer of the subview hierarchy is an NSClipView, and this is what actually clips the content. Then inside of there you have your NSTableView, which actually might be a whole lot bigger than what you see there, and clip view's what actually does the clipping. Now all that's pretty much the same for what we already had with the NSCell-based table view. So let's take a look at what's actually new here.
And the next level of subview that's new is an NSTableRowView. So let's take a look at NSTableRowView and see what it does for you. So the row view is responsible for drawing selection, and the table has a few selection styles, and the row view has some properties so you can access and see that selection style. So here's NSTableViewSelectionHighlightStyleRegular, and of course we have a source list highlight style, so NSTableViewSelectionHighlightStyleSourceList.
Row view's also responsible for drawing the background color, so you could set the rowView.backgroundColor to be a red color and that row will draw red, or whatever color you want. Of course the red looks kind of ugly, but this is just an example. You could make it green or whatever else you want. It's really easy to do.
The row view is also responsible for drawing the group row background. So the group row background we saw in that outline view demo, there's a whole row with a little gray, to kind of separate a group. This done by you implementing the delegate method tableView:isGroupRow:, or the equivalent outline view version, and saying YES for which rows are group rows. These give you that group row style, and if that's set, groupRowStyle will be YES, and it can draw differently. In addition, there's a new property on table view call floatsGroupRows, so all those group rows can automatically float over content optionally, and if that's set, the row view has a floating property which will be set to YES, and you as a subclasser could look at that property and draw differently if you want.
Now, the row view also draws the separators between one row and another, and the reason it does this is it allows the separator to move around with the row when it animates. So the separator is specified by the gridStyleMask on the table view and there's a couple of options. The NSTableViewSolidHorizontalGridLineMask. It's really hard to see it in this screen shot. There's also a new one in Lion that's a dashed option. Again, it's really hard to see the dashed, but you can look closer in the demo app. You can see it.
The row view's also responsible for drawing drag and drop feedback. So when you drag over that row, you want the whole row to show some type of feedback to the user. And so, there is dragging destination styles that the table view has and draws for you. There's NSTableViewDraggingDestinationFeedbackStyleRegular. And there's also a source list style that has a different look for source lists or sidebars or whatnot.
The row view computes a few properties and one of them is the interiorBackgroundStyle. So why is this important. Well depending on what properties the table set on the row view, like if it's selected or whatnot, the row view will calculate what it's background should be. So in this case, the interior background style is set to NSBackgroundStyleDark. So that lets you know that hey, all my content should probably be light text because I'm on a dark background. And then of course there's NSBackgroundStyleLight if it's actually a light background and you should probably have dark text to stand out against it. There are a few more NSBackgroundStyle options that I encourage you to look at which might be set. These are the two basic ones that are really important to know.
Now, that was the one thing that the table row view computed. It doesn't compute all the other properties, the selection and whatnot. They're set by the table view. You as the developer could use the new delegate method tableView:didAddRowView:forRow: to go ahead and override these properties to be whatever you want, so...
Let's take a look at NSTableColumn and how it works with NSTableRowView. Well the row view stores a number of columns and there is a view per column. So if we look at this table here, the outline view example, you can see there are three columns. There's one, two, three. And if we take a look at one individual row view, it has all three columns inside of it, so it stores its number of columns. Now if we take a zoom up of it and look at it a little bit closer, you can see it has one view per column. So it stores that and that view there can be any NSView. This first one is just a regular NSView with just regular subviews: image and NSTextField. You could put whatever you want. It doesn't have to be a special subclass of anything, so this is just a regular NSTextField. This third one is a customized view subview that does custom drawing and then another regular NSTextField.
Now, you could use any view you want, but we highly encourage you to use NSTableCellView. It's optional but it gives you some real cool and easy things to do. Specifically it gives you a couple of outlets. It gives you a textField and imageView outlet. So you as a developer could set this textField outlet to your text field, and the imageView outlet to your image view. These are easy to access in code, but they also do a couple of things for you automatically. They're hints for accessibility for one, so when a user is using VoiceOver and goes to your table view, the textField property will automatically be read off to the user without you having to do any work. Kind of specifies the main text for that row.
Now how many views do you have? So you have a view-based table view, and if you have 20,000 rows or something, does that mean you're going to have 20,000 row views and subviews within it? No you won't. What you'll have is just a view for each visible - er - a view in the visible rect, and that is it. There are no extra views outside of that visible rect. Now there are actually some caveats to this. For instance, if the first responder is scrolled off, the table will actually keep track of that. Or what if you had views animating around? The table will actually keep track of those views so that you can update them dynamically if you need to, and I'll show you how to do that in a short bit.
So what happens when you scroll? So you scroll up. There are no views there. And so the table view's going to just go ahead and pull those couple of views that are no longer visible, and toss them into a reuse queue. Now you might have something else already in this reuse queue, and you can just go ahead and pull those other views out of the reuse queue and reuse them. So it's good for performance to just reuse the views again and again.
So that's basic table view layout. Let's move on and talk a little bit about construction, to figure out how you make a view-based table view.
So the basic data source methods haven't changed. You still have to implement numberOfRowsInTableView: and the caveat to this is if you use bindings. So what you'll do is you'll just return your model count for the number of rows you want. An optional method is tableView:objectValueForTableColumn:row:. Previously, this was required with the NSCell-based, because it was set to the NSCell value. But now it's optional and I'll explain how you can use it in a little bit.
So now what can you do in your delegate is the thing that actually tells the table that it's a view-based table view? You need to implement tableView:viewForTableColumn:row:. Here's one of the most basic implementations. You would alloc/init your view, in this case a simple text field. Now, in this case I'm not showing a frame that you alloc with, and the frame really doesn't matter. The table controls the frame. You don't need to specify it. It will use the frame for whatever that cell's supposed to be.
So you init your view, set the stringValue or whatever value you want to it that are specific to that particular row, and then you can just go ahead and return an autoreleased view. So it's really quite simple to do. Of course all this is optional if you use bindings, and I'll talk about how to do that in a little bit.
So let's talk about identifiers. So when we look at this Complex OutlineView Demo in the Table View Playground demo app, we have a bunch of homogeneous cells. So all these would a MainCell, is what we're going to give it as an identifier. And then that next column, we could call it a DateCell. It's just going to be a plain NSTextField. Then that third column can be a ColorCell, and finally down at that bottom we have the GroupCell and it's another way of identifying kind of that class of cell.
So how do we do this? Well, NSView now has an identifier property, to uniquely identify that type of view. And here's how we do it. There's a new protocol, NSUserInterfaceItemIdentification, which gives an identifier property. NSView implements this protocol and now you have an identifier that can be set or read.
So here's a more typical implementation of the view-based table view, and if you've done any iOS programming with UITableView, it's quite similar to that. You're going to call a new table method, makeViewWithIdentifier:, passing in the type of identifier that you want to create, and an owner of yourself. Now, if it didn't have one from the reuse queue, which is what that method would do, you're going to get back nil. And so you'll have to alloc/init a new view that you want to create for that cell, set the identifier, then go ahead and set up your stringValue and other properties on it, and return that result. So this is a very typical manual implementation.
But we thought about it some more, and thought, "Hey, we can make this even easier!" So this is an even more typical delegate implementation. Inside of it you're just going to call makeViewWithIdentifier:, and it's going to always return a result for you. And I'll show how in just a second. Of course you still have to set up your initial properties here, like the stringValue for that particular row.
So before I talk about how that method works, let's talk a little bit about NSNib loading and how that works. NSNib has a method, instantiateNibWithOwner:topLevelObjects:. Now what's important about here is the owner property. So this is just a screen shot of Interface Builder inside of Xcode, and the owner property there is that File's Owner that you go ahead and specify. File's Owner is important because that's what you can set your target/action to, or your outlets from. So keep that in mind.
Here's a screen shot of how we do design-time layout of the view-based table view. There are three columns. Now what you can think of is each of these design-time cells are effectively a little NSNib that the table view will encode within itself. So we have this first one which has an identifier of MainCell. Next one is the DateCell. Third one would be that ColorCell. And the final one would be the GroupCell. So each of those will just be encoded into the nib automatically within the table, so the table has them. It knows about them. Can replicate everything about that view including the bindings.
So how does makeViewWithIdentifier:owner: work? So you need a view with an identifier called "MainCell". So you as the developer are going to call makeViewWithIdentifier:owner:self. Is that cell with that identifier already in the reuse queue? Well if it is, it's going to go ahead and return that old cell with that existing identifier that it already found. If it didn't, and here's the new part that's a little different from iOS, we see if there's a design-time view with that identifier. If there is, the table automatically calls instantiateNibWithOwner:, finds that view with that identifier, and goes ahead and returns you that new instance of that view. If there isn't, you're kind of out of luck, and at this point it does return nil. But that's how that method works.
Now updating view state for a particular cell. So if you set some properties, like you set the font to be bold or something specific, you as a developer will probably have to initialize it in this method. Otherwise if you got one out of the reuse queue, it might have whatever properties were previously set on it. So it's just an important thing to note that you have to reset all the things like the font.
So let's take a look at another demo and just see how basic table view construction is done at design time.
So this is a really basic view-based table view, and I want to show how you would a new cell view. Inside the object library you could just search for NSTableCellView and you could drag an image and text cell that's already predefined and set up. So the new important part is the User Interface Item Identifier, and you could call it "MyCell" or whatever you want.
So I'm going to undo that and just look at one that I already dropped down here. If I select this cell view, I can see that this one has an identifier of "MainCell". All right, so that's how it's actually set up. Some other interesting things about it. You recall previously that I said that it had two outlets from NSTableCellView. It has an imageView and a textField outlet. So I'll show you how those come into play in a second. In addition you can drop down other views like a button inside of it, and you could actually have the button's action set up to your File's Owner to do like a button click action here. So I'm going to show how you would actually respond to that button inside the cell view.
So let's take a look at the code for an implementation of this. So in a basic implementation, you would go ahead and create your model object. Here it's just an array of images. numberOfRowsInTableView: of course is just going to return the array count. Then the interesting new part, viewForTableColumn:, is implemented, and as I showed you before, at design time there was a cell with an identifier called "MainCell". So makeViewWithIdentifier: will always get a result back created from you, either from the reuse queue or a new one that it just instantiated. It's going to go ahead and grab the model object and then set up properties on it, accessing those outlets. So it accesses the textField, sets stringValue, and accesses the image, sets image value, and then returns the result. So that's the most basic implementation of a view-based table view. It's really easy to do.
Now how do you respond to an action? So if you click on that button, well this is my delegate and there are a bunch of rows. How do I know which row that action should be applied on? Well the table view has a method called rowForView:. So you can just find out what row that view is actually on. In this case we're going to find out which one the sender was on, and the sender was the button. So we know which row was actually clicked on and we can do some action based on that row. So that's also pretty easy to do.
So how do you do editing, that's the next question that people will usually ask. Well with NSCell, you would use tableView:setObjectValue:forTableColumn:row:. Now, if you have a view-based table view, what if you have more than one text field in that cell? It's kind of ambiguous what the table should do with this method, because we don't know which one you want to edit. And actually, we don't have to do anything special. It's just a text field. You can just use normal target/action based editing or notifications on that text field to find out when text changes. And so that's what you're gonna do and it's really easy to do.
How do you manually begin editing? Well with NSCells, you would use editColumn:row:withEvent:select, and that would go ahead and edit that cell. But again, it's ambiguous if you have more than one text field. And in fact, editing is really just making it the first responder. So in a view-based table view, you'll just call makeFirstResponder: and then it begins editing. So that's really easy too.
But how do we control editing to make it behave like a table? Normally in Finder, if you click on a row, it doesn't start editing right away. In fact, you can only edit a row in Finder if you click on text in an already selected row. And it actually happens at a slight delay, so you can actually do a double-click and have a double-click action take effect, or a single-click action to begin editing. So how does the table make that work? The table overrides hitTest: and doesn't let the hit test go to the view. And if certain conditions are met, and the way it does it, is it calls a new responder method on itself, validateProposedFirstResponder:forEvent:. So it takes that view that you clicked on and calls it with that as the responder, passes the event that was coming in, and the table then implements this and says "Okay, is this something with text that needs to be editing? Yes it is. Well, I'm going to save off what would be the first responder, and just wait until the double-click interval passes." After the double-click interval passes, the table just calls makeFirstResponder: on it and it starts to edit. This way, a double-click action could fire and it will that stop that.
Now why is this important? Well, you as a developer could subclass and override this and add your own behavior, or you could have a control which is in a table that might want to do something only if the table says it can do it. So you inside of your control could call validateProposedFirstResponder:, up the responder chain, and the table, if it's there, could do certain things that prevent editing or prevent like the I-beam cursor from showing up.
All right. So that's basic construction of a view-based table view. Let's move on, talk a little about bindings and how they work.
So how you do bind content for a particular cell? You can still use an NSArrayController, except you will use the NSArrayController only to bind the content of the table view. You will not use an NSTableColumn and set up properties on that because that only applies to the NSCell in the table column.
Now here is where you could implement that tableView:objectValueForTableColumn:row: method and actually return something. So if you don't want to use an array controller for your object, but you still want to use bindings, you implement this delegate method and you can return your model object from here. So let's see, if you do that, how it would actually work.
Now let's take a look at a simple object value type of view. So right here, this is our view which we gave an identifier of "DateCell", and what it is, is it's just a basic NSTextField. There's nothing fancy about it. NSTextField implements setObjectValue:. So what the table does, it says, "Hey view there, do you respond to setObjectValue:? You do? Okay, well I'm going to take whatever object value I have, either from that delegate method that you implemented or from the array controller, and just call setObjectValue: on you." Now, this is pretty much what regular NSCell-based table views were doing for the longest time. Nothing really special about here.
So let's take a look at a complex object value example. So this cell here, which we would call the "MainCell", it has more than one property that we want to bind to. Before, it was difficult to do more than one binding inside of a cell. So how are we going to solve that? The answer is to use NSTableCellView as your whole cell, and this table cell view also has an objectValue property. So again the table's going to see that it has it there, call it, and set the object value. So that means the whole cell has this object value inside of it. What you can do with that is you can do a binding from that particular view like the image, to the cell view's objectValue, and then any model object properties you have, like the image. Or your text field. You could do cellView.objectValue.title. So that's how you can really easily do bindings inside of it.
So let's take a look at how you set them up inside of Xcode 4. So this has got the text field selected inside of design-time environment. And the things to point out here is that there's a new binding target that you can select. You can select the cell that that actual view lives in, and if you're binding to that, there's always going to be an objectValue property. So you can bind to the objectValue.title in this case. So that's pretty easy to set up and do.
Now let me talk about automatic view loaded. So way before, I said tableView:viewForTableColumn:row: is actually optional. So how does that work? Well, we know what table column you're passing, and it has an identifier. And we can just automatically use that same identifier that to find a view that you designed at design time. So then again in the Table View Playground demo here, if I go ahead and select the table column, the thing that's interesting to point out is that the identifier is set to "SampleWindowCell" in this case. Now if I select that cell, and again you could have multiple columns, or you could have multiple cells, in this case it's just a one-column one-cell, but the interesting thing here is that that cell's identifier exactly matches the table column's identifier. And if it matches, the table view will go ahead and automatically use that cell from the nib, and you don't have to implement that delegate method at all.
Now, you're probably thinking, "Oh, well, it might suck to keep those two things in sync, because now I have to rename it in one spot and rename it in another spot and do a bunch of work, and if one's not set, you might get no views showing." Well, we have a solution in Xcode 4 and it's called "automatic identifiers". So what you can do is when select the table column for this cell, you just set the user interface identifier to automatic, which is the default value, and what's going to happen here is we will automatically create an identifier for you, and keep the two for the table column and the cell in sync automatically. So it makes it easy to do.
So let's move on and talk about customizing a view-based table view.
So I'm going to show another demo application called the Hover Table demo. Again, you can download this on the developer site and play around with the source code. And some interesting things here is when I hover over a particular row, it does a cool hover animation. So I'm going to show how to do that.
When I select a row, a couple things happen. It draws custom selection, and the text field's text turns bold. I'm going to show how to do that.
Another thing it's doing which is a little more subtle is the horizontal separators are actually a subtle little gradient, to kind of give it this cool look. So I'm going to show how to do that too.
All right, so how do you do custom row selection now? Well, you can just subclass NSTableRowView to do it. NSTableRowView has a bunch of hooks to customize drawing. You can override drawSelectionInRect:, and I'm not going to really go over the code, but all it's going to do in this particular case is it's going to create a Bézier path, and fill it with a light gray color and a darker gray stroke. So it's really easy to customize selection.
Now, what was hard before to customize selection with is, how do you know if that table should draw in the active or inactive state? So when the table is first responder, it would usually have a blue highlight, and when it's not, like in a background window or not the first responder, it would draw selection with a gray. And the way that you can do that is you can look at the row view property emphasized. If emphasized set to YES, it should the active state, and if it's NO, the inactive.
Now how do you do that bold selected text? And so when the selection changes, you get the typical delegate method, tableViewSelectionDidChange:. Now what's new here is, you as a developer want to enumerate all the views the table knows about. You could try to get all the views in the visible rect and work on those, but I said before, "Well, there might be some extra views floating around, animations, or a view might be the first responder and it scrolled away from the visible rect but still around." So you want to enumerate everything the table knows about. So you're going to call enumerateAvailableRowViewsUsingBlock:, and you're going to pass a block which takes the row view and the row that the table has and knows about.
So you have the row view, and the row view stores these views, so you can use viewAtColumn: to find that particular view for the first column. And before we saw the view had a textField outlet, so we can go ahead and grab the textField outlet. We can access some properties on the row view like if it's selected or not, and then based on that, well, if it's selected let's use a bold font. If it's not selected, let's use just a regular system font. So it's pretty darn easy to enumerate all the row views and all the views and update one particular property that you want. It's also quite efficient and fast too.
So how do you do this hover effect when you mouse over and it does this kind of cool little gradient. Let me show how to do that.
What you're going to do is just use the typical NSView method, updateTrackingAreas:, to go ahead and update some tracking areas. In this case it's going to alloc/init a tracking area and store it in an ivar of the row view. If that's not in our trackingAreas array, it'll call addTrackingArea: on itself, and it has the tracking area set up.
The tracking area will then give the row view a mouseEntered: and mouseExited: event when the mouse comes in and goes. So all we're going to do is set a bit, _mouseInside, to know when the mouse is inside of us and let us redisplay. And of course mouseExited:, it'll undo that.
So then in our drawing method, again there are lots of great drawing hooks. You can subclass and override drawBackgroundInRect:. You can fill with that background color that I talked about way long ago. You can check your bit to see if the mouse is inside of you, and if it is, you can alloc/init a gradient and just fill with the gradient. And that's how you can do that hover effect really easily.
Now how do you do the custom separators? It's kind of hard to see that separator again, but it's a nice little cool gradient. And what you can do is you can override drawSeparatorInRect:. For the row view, you're responsible for drawing the bottom separator. The previous row's will draw the top separate above you. So you're going to grab your bounds. You want to draw at the bottom so you're going to get the max y of your bounds, set the height to one, and then the interesting thing is you're going to probably use a common method called DrawSeparatorInRect to do the fill, the separator however you want.
Now I'm going to show why in just a second.
So the row view draws its own separator so that it can move around with the row view itself when an animation happens. But how do you deal with these empty separators that are at the bottom of the table view when you have more content to show? Well, the table view already has a method to customize the grid drawing, called drawGridInClipRect:. So here you can override that and do some custom separator drawing. The code's a little bit more complex, but what you want to do is you don't need to draw those separators for the spots where all the rows are at, so you grab the max y of the last row. Going to go past it because that last row drew its separator at the bottom. And now the code's going to scroll up to the next page. You're going to do what you did before, basically, which is you're going to figure out the bounds for the separator that you want to draw in. You're going to enumerate until you're actually at the bottom of your view. And you're just going to call that same method before, DrawSeparatorInRect, to go ahead and draw a separator. So it's pretty easy to do that, too.
So you have this custom row view. How are you going to actually tell the table about the row view? Well, there's a new delegate, tableView:rowViewForRow:, which allows you to go ahead and alloc/init a row view, your custom one, set any custom properties you want, and return the result. Now another thing to note here: again, the frame isn't really important. You could use the row height if you want, but the table will control the frame and so you can kind of specify whatever you want there.
Now, you can also do this at design time. You could toss your row view as a subview of the table view at design time, and just use makeViewWithIdentifier: to find your custom row view and create an instance of it. So you don't have to do any creation and then this uses the reuse queue too.
But even that's optional, so you don't even have to do that and you can do it all at design time. So here's how you can specify a row view at design time without having to write any code at all. I have the row view selected here, and the thing that is unique and must be pointed out is that there's a special user interface item identifier that is set: NSTableViewRowViewKey. If you use this special identifier, which is in the header, then the table automatically search for and find that row view and use it for all the rows. And then you don't have to write a delegate method or any code to specify it.
So let's do another demo of drag and drop in Table View Playground.
So if I go ahead and show the complex outline view example, I want to show some drag and drop features and talk about them again.
So one thing that's important to note when I start dragging, there is two separate rows that were dragged and I want to show how to create two separate rows. When I let go, it's subtle but what happens is the table opened up a gap and then the drag image actually animated from where you had the drag image all the way to that gap's ending location. So I want to show how to do that.
So multi-image dragging. So previously you would have one huge drag image for the table. Now you can have multiple NSDraggingItems. If we take a look at NSDraggingItem, now hopefully you went to Raleigh's talk yesterday. If you didn't, I'm going to do a quick review on NSDraggingitem. The dragging item has an item which is the pasteboard reader or writer that specifies the pasteboard data. It has a whole frame for that particular item, because inside of item you're going to have an image component, or really you might have more than one image component. In this case there's two.
So that's what an NSDraggingitem is composed of. The API for it, to create one when you put it in the pasteboard, is using initWithPasteboardWriter:. So you provide your pasteboard data for the dragging item, and then you're going to provide an image components provider. This provides all those image components. Now, it's an array of NSDraggingImageComponents, and it's a block because you don't want to provide all the components all at once, because they all may not be visible. You don't want to drag 20,000 rows and create 20,000 images. The dragging system will call you back and create them only when they're needed.
So how do you put an item on the pasteboard? There's a new table view method, pasteboardWriterForRow:, and what you're going to do here is, for that particular row, you're just going return something implements NSPasteboardWriting. So typically you might just make your model object implement NSPasteboardWriting. In this case, in the demo, it's an ATDesktopEntity that implements it. What's going to happen is the table will automatically take that pasteboard writer, create an NSDraggingItem for you, and use that to start the drag with.
If you don't want that row to be dragged, you can just return nil and that particular row will be excluded from that dragging.
So how do you implement NSPasteboardWriter? Well, it's pretty easy. You'll just implement the protocol, and I'll show you in a second what you'll do, but for this particular example we have a file URL that is the main thing that we're dragging around. So our implementation will implement the two required methods, writableTypesForPasteboard: and pasteboardPropertyListForType:, and we have that file URL. NSURL implements NSPasteboardWriter, so we're just going to delegate those methods directly to our file URL. And that's how we can easily add drag source support from the table.
Now, how do you provide those drag images? Because that NSDraggingItem has those image components provider to provide a couple image components. So like the image and text is being dragged around.
Each of those is an NSDraggingImageComponent, and they have a key to identify what type it is. In this case it'd be an icon, and the contents which is the actual image that's being dragged around.
So here we want to drag around two images. We want to have one that has a key of NSDraggingImageComponentIconKey, so this is the main icon that the dragging system will know about. And the other one is a label key, to specify the label portion, and that way the dragging system can animate the label portion from one view to the label portion of another view and move it around as necessary. Now, these can be automatically provided for you by NSTableCellView. There's the draggingImageComponents method on NSTableCellView and what that does is it goes ahead and uses your two properties that you set up, again the outlets from the textField and the imageView. If you as a developer set those up then the draggingImageComponents will automatically provide those for you without having to do any work.
Then the question is, well, what if you have another one that you want to provide a image for like the color or something? Let's show how you would do that. You can subclass NSTableCellView, override draggingImageComponents, call super, which is going to give you those components for the image and text. Then you're going to have to create an NSDraggingImageComponent with your key that you want, in this case we could call it "Color". You're going to set the contents to be an image that represents your view being dragged. It's pretty easy to do and inside of the sample application I show how to do it. You're going to set the frame with respect to the cell view's bounds, so it does a convertRect: of the colorView's bounds from the colorView into our own coordinate system. And then you just add it to the result and return it. So that's how you get that extra drag image to be dragged from your cell view.
Now, how do you change a drag image? So in Finder, if you have a big icon view with a big image and text, now when you drag it over the table view, you want to go ahead and morph into something the table kind of knows about. You want the image to shrink and the text to move over to the right.
So how do you do that? Well you want to be a dragging destination, and we're going to implement it as a new Lion method called outlineView:updateDraggingItemsForDrag:. Inside of this, you're going to want to take that pasteboard data that's being dragged over you and create a view and create draggingImageComponents for it to automatically animate from what was dragged over you, from the source to what's going to be you and your particular representation. You create an NSTableCellView with makeViewWithIdentifier: and you're going to use that to kind of stamp out the images. You also set up a cell frame for your particular row, just setting up your width and height.
Now here's what you're going to do is create an array of all your model objects that your table can create. In this case it's the ATDesktopEntity class I talked about previously. And then you're going to call a new method on NSDraggingInfo called enumerateDraggingItemsWithOptions:forView:classes:searchOptions:usingBlock:. I'm going to highlight some of the important parts about it, but you get this block callback for every instance of the model object that could be created. We're going to look at the dragging item and kind of ignore the index and stop parameters because we aren't going to do anything special with them.
So what the dragging item, that's that thing that was dragged over you, well you want the frame to become the frame for your cell view, so you go ahead and update that. And you want to specify a callback block, essentially, for the imageComponentsProvider, to specify the new image components that are going to be dragged over you. And what you do inside of here is you have your sample cell view. You set the dragging item to it as your objectValue. Now, this dragging item, that was automatically created from the classes there, so the classes which you have specified as your model objects are how that gets into the item. So the dragging info created an ATDesktopEntity for us with the pasteboard reader automatically. So we set that as our objectValue. We have bindings or whatnot to automatically display things. Set up our frame, and then again the cell view has that draggingImageComponents, and so we're just going to use that to return the draggingImageComponents and it gets whatever stuff we added to it, or whatever stuff the table implements automatically for us.
So how do we accept a drop and do that animation I was talking about? So we want the table to open the gap and wherever the drag image is located, drag from that location right to our final location. So let's see how to go ahead and do that.
So first, you have to tell the dragging system that, well, you accept the drop, and you want to do an animation. outlineView:validateDrop:proposedItem:proposedChildIndex: is a very old delegate method to accept a drop. So you'll do your typical pasteboard validation, and you can look at the demo application to see how we do it. We're going to set a new parameter on Lion called animatesToDestination on the dragging info. That's telling the drag subsystem that, hey, you do want to animate, and you're going to do an animation from that source location of the drag image to your final location of where your row is. So that's important to set.
Now, how do you actually accept the drop? You implement the old delegate method outlineView:acceptDrop:item:childIndex:, and table view has a similar one. What you do inside of here is what you would do when you're updating the dragging images, except you want to actually accept the drop and put things into your model. So you're going to create another array with your model objects, of like ATDesktopEntity in this example, and you're going to call enumerateDraggingItemsWithOptions:forView:classes:searchOptions:usingBlock: again, and let's take a look at what your implementation with this block would look like.
So again, that draggingItem.item was automatically created from the array that we passed to this method. So it used NSPasteboardReader to automatically create your model object. So we grab our model object and store it in a little local. We take that model object and insert into our array that we have for our model. In this outline view example it's just putting directly into the children array. So that updated our model. Now we have to update the view. So the outline view has a new method, insertItemsAtIndexes:inParent:withAnimation:. I'm going to talk a little bit more about animations in a second. And now what this does is you pass animation of an NSTableViewAnimationEffectGap and that's going to tell the table, "Hey, open up a gap because we're going to do animation into it." As soon as you call that method, at this point the table can be thought of in its final state. So you did an insert to the table and the table knows that you did that. You can call methods on it, like find out the row for that particular item you just inserted. You now have the row and you can find the actual frame of that item using the typical table methods like frameOfCellAtColumn:row: and what you can do here is you'll just update the draggingFrame. So what this does is it's telling that dragging item, "Hey, drag from wherever you are to this final row location," and animate it.
So that was how you would do drag and drop and accept it.
Let's move on and talk a little bit about animating. So there are three basic methods to do an animation, and they're very similar to NSMutableArray. What we have is insertRowsAtIndexes:withAnimation:, removeRowsAtIndexes:withAnimation:, and moveRowAtIndex:toIndex:. These are the table view methods. There are also equivalent outline view ones, like I was previously showing the use of, that insert into a particular outline item, and the children of that outline item. They work in a very similar way but I'm not going to cover them.
So how do you, how does this work? insertRowsAtIndexes: takes an index set, and this is kind of pseudo-code, but let's say I pass an index set with indexes of one and three. Let's see how it would work. Well, what would happen is a hole would open up for row one, and that item would be inserted. A hole would open up for row three, and then that item would be inserted. So two new items that we pass in would automatically be inserted.
A cool thing to note here is that, by using these methods, the selection is automatically updated. So you don't have to worry about the selection being stuck at the wrong row. This is much easier than using something like reloadData.
Now, what if you want to batch updates? So what if you have a bunch of changes that you are going to do? You're going to insert a ton of rows, remove a ton of rows. Well, it's better for performance to batch them together inside of a beginUpdates and endUpdates table view block. The reason that you want to do this is, let's say you go ahead and insert a hundred rows, and you call insertRowsAtIndexes: with a hundred of them. And then you delete those hundred rows. Or you insert another ten or twenty. Any time you call that method, the table will go ahead and create views and do an animation. Well, you don't want it to animate those hundred rows which might not be on screen at the end of your particular set of operations because you might be inserting and removing them right away. So if you batch them all together, the table's smart enough to optimize it and do only what the user would end up seeing. So for performance you want to call beginUpdates and endUpdates.
Now, how does this work? Now, it's important to realize this is actually a little bit different than the similar methods on iOS and UITableView. In that case, with UITableView, it would sort of freeze the table's contents and you could kind of modify each row. The one for NSTableView works much more similar to NSMutableArray. So here's an example what I mean by that. You call beginUpdates, do insert row at index with an index of two. It opens up a slot. You get that new first row. You call it again. It opens up a slot. You get that new row. Now, this isn't really too complex. It's really just the way an array works. You're inserting it again and again. But this is different than iOS, and if you try to insert the row twice in iOS it might throw an exception. So I just want to point out that difference there.
Now, how do you do row height animations? So one of the other things that I showed before, this is a little video, is how do you animate the row heights for the table. In addition, the table's animating the row heights, but you own that cell view, and inside that cell view some other things are happening. A couple views are fading away, and the actual contents of the image are shrinking. So how do you go ahead and do that?
What you're going to do is use noteHeightOfRowsWithIndexesChanged:. So this assumes you're using variable row heights and using the variable row height delegate methods. Create your index set, call this method, and now this method will just always animate. So it always goes ahead and animates for you, for every view-based table view.
So how do you take your views and sync them with that animation? Well, the way you're going to do it is use an animation context and do a beginGrouping and endGrouping to group it all together. So inside of that, you could do things like change the duration of the animation. You can update your views: say, do an animation to hide a view, or change the size of, like, the image view. Then you can go ahead and tell the table to do its animation by calling noteHeightOfRowsWithIndexesChanged: and it will animate. And then you end your grouping. They will all animate then together.
Then that begs the question, "Well, what if I don't want that method to animate?" The way you can make any of those methods that animate, such as the insert, remove, and move methods, is you can create an animation grouping and set the duration to zero. So if you set the duration to zero, it will tell the table, "Hey, I really don't want to animate this thing, so don't do that."
Now, it would be really cool if all these methods worked with NSCell, and in fact, all these methods will work with an NSCell-based table view. The insert, the remove, the move, the noteHeightOfRowsWithIndexesChanged: will all work, but you have to do one thing. To make them work with an NSCell-based table view, you have to call beginUpdates and endUpdates. When you call beginUpdates, what the table view will do, it's going to draw all of your NSCells into a temporary image and swap them out with a view-based table view with exactly what you were seeing there. You can then have an initial state to do an animation from, so you can call insertRowsAtImages:, removeRowsAtIndexes: or whatnot, and the animations will happen and work. This is actually how all the animations work inside of Lion for outline views, which were not moved to a view-based table view. And if you want to see an example, the DragNDropOutlineView demo application was updated. You can go find it on the developer site and see how it's done.
So, in summary, I talked about layout, how tables are actually constructed, how row view comes into play, and how it all fits together. Talked about construction, how to use the data source methods, the delegate methods to actually create a table view, and how to do a design time layout of everything. I talked a bit about how to do bindings and how to bind content and a new way where you have multiple values inside of your cell. Talked about customizing table views, so download that HoverTable demo, check it out, play around with its source. Talked about drag and drop, creating draggingImageComponents, and dragging multiple rows instead of a big single drag image. And then finally I talked about animating and how to do some cool animations.
So for more information contact Bill Dudney, our frameworks evangelist. Our documentation has been updated for the view-based table view. We have the developer forums. I frequently browse around there and answer table questions. We have a related sessions. James Dempsey is giving our "Design Patterns to Simplify Mac Accessibility" at 3:15 today. It's also the song talk, so I highly encourage you to go to that. He's going to talk about how to add accessibility to view-based table views, which is really easy to do now. Before, it was difficult to add accessibility to NSCells. Now it's pretty easy to make it with the view.