App Frameworks • OS X • 53:44
Lion now has the ability to use Views instead of Cells inside NSTableView. Get the information on how to quickly develop applications using this new technology. Basic TableView setup will be covered, along with more advanced features, such as insertion animations and dynamically updating content.
Speaker: Corbin Dunn
Unlisted on Apple Developer site
Downloads from Apple
Transcript
This transcript was generated using Whisper, it has known transcription errors. We are working on an improved version.
Hello. Welcome to ViewBase NSTableView from Basic to Advanced online. My name is 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 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 TableView. 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 TableView 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 of the features of the View-based TableView. So right here, this window is a Basic View-based TableView. It just creates the other windows for the application to show pieces of the UI. And it's completed -- or created complete 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 TableView. 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 TableView. This is the complex TableView 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 flow 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. We can do moves. So I can move from one row to another. A real easy API to do this. We can do things like animating the row height. And while the row height is animating, we can animate the views inside of it. It's really easy to do.
NSOutlineView is a subclass of NSTableView. It has all these features too. It does a cool animation throughout the OS when things open and collapse. A neat thing about this table view is we can do some cool insert animations. I create drag images and as I let go, the 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 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 an NSScrollView at the top, and the scroll view is 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 lot bigger than what you see there. And the ClipView is what actually does the clipping. Now all of that's pretty much the same for what we already had with the NSCell-based TableView. So let's take a look at what's actually new here. And the next level of a subview that's new is an NSTableRowView. So let's take a look at NS Table Row View 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 NSTableView Selection Highlight Style Regular. And of course we have a Source List Highlight Style, so NSTableView Selection Highlight Style Source List.
The rowView is 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. It can 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 the little gray to kind of separate a group. This is done by you implementing the delegate method TableViewIsGroupRow, or the equivalent OutlineView version, and saying yes for which rows are group rows. These give you that group row style, and if that's set, group row style will be yes, and it can draw differently.
In addition, there's a new property on TableView called 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 would be set to Yes. And then you as a subclasser could look at that property and draw differently if you want.
The row view also draws the separators between one row and another. The reason it does this is it allows the separator to move around with the row when it animates. The separator is specified by the grid-style mask on the TableView. There are a couple of options. There is the NSTableView Solid Horizontal Gridline Mask. It's really hard to see it in this screenshot. There's also a new one in Lion that's a dashed option. Again, it's really hard to see the dash, but you can look closer in the demo app and you can see it.
The row view is 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 TableView has and draws for you. There's NSTableView dragging destination feedback style regular, which is just the regular feedback style. 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 interior background style. 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 its 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 TableRowView computed. It doesn't compute all the other properties, the selection and whatnot. They are set by the TableView. You as a developer could use the new delegate method. TableView did add RowView for row to go ahead and override these properties to be whatever you want.
Let's take a look at NSTableColumn and how it works with NSTableRowView. The row view stores a number of columns and there is a view per column. If we look at this table here, the outline view example, you can see there are three columns. There's one, two, three. 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.
If we zoom up and look at it a little bit closer, you can see it has one view per column. It stores that and that view there could be any NSView. This first one is just a regular NSView with regular subviews, an image and an NSTextField. You can put whatever you want. It doesn't have to be a special subclass of anything. And so this is just a regular NSTextField. This third one is a custom NSView 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 text field and image view outlet. So you as a developer could set this text field outlet to your text field, and the image view outlet to your image view.
These are easy to access in code, but they also do a couple things for you automatically. They're hints for accessibility, for one. So when a user is using VoiceOver and goes to your TableView, the text field property will automatically be read off to the user without you having to do any work. It kind of specifies the main text for that row.
Now how many views do you have? So you have a View-based TableView, 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? Well, no you won't. What you'll have is just a view for each visible, or 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 actually will 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 TableView is 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 TableView layout. Let's move on and talk a little bit about construction to figure out how you make a View-based TableView. So the basic data source methods haven't changed. You still have to implement number of rows in TableView. 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 object value for table column row. Previously this was required with the NSL based, because it was set to the NSL 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 it's a View-based TableView. You need to implement TableView, ViewForTableColumnRow. And 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 is supposed to be.
So you knit your view, set the string value or whatever properties you want to it that are specific to that particular row, and then you can just go ahead and return an auto-release view. So it's really quite simple to do. All this is optional if you use bindings, and I'll talk about how to do that in a little bit.
[Transcript missing]
So how do we do this? Well, NSView now has an identifier property to uniquely identify that type of view. 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.
Here's a more typical implementation of a View-based TableView. If you've done any iOS programming with UI TableView, 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 allocate a new view that you want to create for that cell, set the identifier, then go ahead and set up your string value and other properties on it, and return that result.
So this is a very typical manual implementation. But we thought about 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 string value for that particular row.
So before I talk about how that method works, let's talk a little bit about NSNibLoading and how that works. NSNib has a method, InstantiateNibWithOwnerTopLevelObjects. Now what's important about here is the owner property. So this is just a screenshot of Interface Builder inside of Xcode. And the owner property there is that files owner that you go ahead and specify. The files 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 screenshot of how we do design time layout of the ViewBase TableView. There are three columns. Now what you can think of is each of these design time cells are effectively a little NSNib that the TableView will encode within itself. So we have this first one, which has an identifier of main cell.
Next one is the date cell. Third one would be that color cell. And the final one would be the group cell. So each of those will just be encoded into the NSNib automatically within the table. So the table has them and knows about them, can replicate everything about that view, including the bindings.
So how does MakeViewWithIdentifierOwner work? So you need a view with an identifier called MainCell. So you as a developer are going to call MakeViewWithIdentifierOwnerSelf. 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 instantiate_nib_with_owner, 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.
Updating ViewState for a Cell. If you set some properties, like the font to be bold or something specific, you as the 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. It's just an important thing to note that you have to reset all things like the font. So let's take a look at another demo and just see how basic TableView construction is done at design time.
This is a really basic view-based TableView. I want to show how you would add a new CellView. Inside the object library, you could just search for NSTableCellView. You could drag an image and text cell that's already predefined and set up. The new important part is the user interface IAM identifier. 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 the CellView, I can see that this one has an identifier of main cell. All right, so that's how it's actually set up. Some other interesting things about it. You'll recall previously I said that it had two outlets from NSTableCellView. It has an ImageView and a TextFieldOutlet.
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 buttons will be covering the latest and most recent applications using the new function, which is called the "Bottom Clicked Action".
So in a basic implementation, you would go ahead and create your model object. Here it's just an array of images. Number of rows in TableView, of course, is just going to return the array count. Then the interesting new part, View for Table Column is implemented. And as I showed you before, at design time there is 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 text field, set string value, and accesses the image and sets image value, and then returns the result.
So that's the most basic implementation of a View-based TableView. 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 TableView 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 set object value for table column row. Now, if you have a View-based TableView, what if you have more than one text field in that cell? It's kind of ambiguous how you would actually, or 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 going to do, and it's really easy to do.
How do you manually begin editing? Well, with NSCells, you would use EditColumnRowWithEventSelect, 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 TableView, 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 in 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 hitTest go to the view, if certain conditions are met. And the way it does it is it calls a new responder method on itself, validateProposedFirstResponder for event.
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 stop that.
Now why is this important? We use 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 your control could call validate proposed first responder up the responder chain and the table, if it's there, could do certain things that prevent editing or prevent like the IBM cursor from showing up.
Alright, so that's basic construction of a View-based TableView. Let's move on and talk a little bit about bindings and how they work. So how do you 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 TableView. You will not use an NSTableColumn and set up properties on that, because that only applies to the NSCell in the TableColumn.
Now here is where you could implement that TableView object value for TableColumnRow 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.
Simple Object Value Type of View This is our view which we gave an identifier of DateCell. It's just a basic NSTextField. NSTextField implements SetObjectValue. The table says, "Hey, view there, do you respond to SetObjectValue?" You do? OK. I'll take whatever object value I have, either from the delegate method or the array controller, and call SetObjectValue on you. Now, this is pretty much what regular NSCell-based TableViews 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 main cell, 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. NSTableCellView also has an object value property. And so again the table is 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 object value, and then any model object properties you have, like the image. Or your text field. You could do cellview object value dot 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 the 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. Zen. If you're binding to that, there's always going to be an object value property. So you can bind to the object value of that title in this case. So that's pretty easy to set up and do.
[Transcript missing]
"And we can just automatically use that same identifier to find a view that you designed at design time. So again in the TableView 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 sample window cell 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 and 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 TableView 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 Xcode for, and it's called Automatic Identifiers.
So what you can do is when you select the table column or the 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 TableView.
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. Some interesting things here is when I hover over a particular row, it does a cool hover animation. 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 give it this cool look. I'm going to show how to do that too.
How do you do custom row selection now? You can subclass NSTableRowView to do it. NSTableRowView has a bunch of hooks to customize drawing. You can override DrawSelection and Rect, and I'm not going to go over the code, but all it's going to do in this particular case is create a Bezier path and fill it with a light grey color and a darker grey stroke. 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 a selection with a gray. And the way that you can do that is you can look at the rowView property emphasized. If emphasized is set to yes, it should draw in 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 and 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 with animations, or a view might be the first responder and 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 enumerateAvailableRowViews using block, 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 a View at Column to find that particular view for the first column. Before we saw the view had a text field outlet, so we can go ahead and grab the text field 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 to do. 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, update tracking areas, to go ahead and update some tracking areas. In this case, it's going to allocate a tracking area and store it in an INVAR of the row view. If that's not in our tracking areas array, it'll call add tracking area on itself, and it has the tracking area set up.
The tracking area will then give the row view a mouse entered and mouse exited event when the mouse comes in and goes. And so all we're going to do is set a bit under our mouse inside to know when the mouse is inside of us and let us redisplay. And of course, mouse exited, it'll undo that.
So then in our drawing method, again there are lots of great drawing hooks. You can subclass and override, draw background and erect. 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 allocate a gradient and just fill with a gradient. And that's how you can do that hover effect really easily.
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 draw separator in Rect. For the row view, you are responsible for drawing the bottom of the separator, or the bottom separator. The previous rows will draw the top separator above you.
So you're going to grab your bounds. You want to draw at the bottom, so you're going to get the maxY of your separator, or maxY of your bounds. Set the height to 1. And then the interesting thing is you're going to probably use a common method called DrawSeparatorInRect to do the fill of 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 TableView already has a method to customize the grid drawing, called DrawGridAndClipRect. So here you can override that and do some custom separator drawing.
The code is 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 maxY of the last row. You're going to go past it because that last row drew it separate at the bottom. And now the code is 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 method, TableViewRowViewForRow, which allows you to go ahead and allocate 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.
[Transcript missing]
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 rowview 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 TableView 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.
Multi-image dragging. Previously, you would have one huge drag image for the table. Now, you can have multiple NSDraggingItems. If we take a look at NSDraggingItem, 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 it 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 on the pasteboard, is using initWithPasteboardWriter. So you provide your pasteboard data for the dragging item, and then you're going to provide an ImageComponentsProvider. 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 TableView method, PasteboardWriterForRow. And what you're going to do here is for that particular row, you're just going to return something that implements NSPasteboardWriting. So typically, you might just make your model object implement NSPasteboardWriting. In this case, in the demo, it's an ATDesktop entity that implements it.
What's going to happen is the table will automatically take that pasteboard writer, create an NSDragging item 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 NSPaceboard Writer? Well, it's pretty easy. You will 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.
Our implementation will implement the two required methods: writable types for pasteboard and pasteboard property list for type. We have that file URL, and its URL implements NSPasteboardWriter, so we're just going to delegate those methods directly to our file URL. That's how we can easily add drag and drop support, or 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 NSDraggingImage component. They have a key to identify what type it is, in this case it would be an icon, and a contents, which is the actual image that's being dragged around. Drag around two images. One with a key of NSDraggingImageComponentIconKey. This is the main icon the dragging system will know about. The other one is a label key to specify the label portion. 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 by you, or for you, by NSTableCellView. There's a dragging image components 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 text field and image view. If you as a developer set those up, then the dragging image components 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 an image for, like the color or something? Let's show how you would do that. You can subclass NSTableCellView, override dragging image components, call super, which is going to give you those components for the image and text. They're going to have to create an NSDraggingImage component 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 convert rect of the color view's bounds from the color view into our own coordinate system. Then you just add it to the result and return it. That's how you get that extra drag image to be dragged from your cell view.
How do you change a drag image? In Finder, if you have a big icon view with a big image and text, when you drag it over the TableView, you want to go ahead and morph into something that the table knows about. You want the image to shrink and the text to move over to the right. How do you do that? You want to be a dragging destination. What you can implement is 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 dragging image components for it to automatically animate from what was dragged over you to what, or from the source to what's going to be you and your particular representation. You create an NSTable CellView with MakeViewWithIdentifier. You're going to use that to stamp out the images. You also set up a cell frame for your particular row, just setting up your width and height.
[Transcript missing]
And then you're going to call a new method on NSDraggingInfo called EnumerateDraggingItemsWithOptions for View, Classes, Search Options, Using Block. 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 him.
[Transcript missing]
What you do inside of here is you have your sample cell view, you set the dragging item to it as your object value. 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 AT Desktop entity for us with the Pasteboard reader automatically.
Set the object value, bindings, and what not to display things. Set up the frame. And again, the CellView has the dragging image components. We'll use that to return the dragging image components. 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 up 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 proposed child index 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, child index, and TableView has a similar one. What you do inside of here is what you would do when you were 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 ATDesktop entity in this example. And you're going to call enumerate dragging items with options for view classes search options using block again.
Let's take a look at your implementation of what this block would look like. So again, that dragging item to item was automatically created from the array that we passed to this method. So it used NSPaceboardReader 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 it into our array that we have for our model. In this OutlineView example, it's just putting it directly into the children array.
So that updated our model, now we have to update the view. So the OutlineView has a new method, insertItemsAtIndexes in parent with an animation. I'm gonna talk a little bit more about animations in a second. And now what this does is you pass an animation of an NSTableViewAnimationEffectGap, and that's gonna tell the table, hey, open up a gap, 'cause we're gonna do an 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 into the table, 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 frame of cell at column row. And what you can do here is you'll just update the dragging frame. 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 are very similar to NSMutableArray. What we have is insert rows at indexes with animation, remove rows at indexes with animation, and move row at index to index. These are the TableView methods. There are also equivalent OutlineView ones, like I was previously showing a 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.
How does this work? Insert rows at indexes takes an index set. This is kind of pseudo code, but let's say I pass an index set with indexes of 1 and 3 and see how it would work. What would happen is a hole would open up for row 1 and that item would be inserted.
A hole would open up for row 3, and then that item would be inserted. So two new items that we pass in would automatically be inserted. The 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 begin updates and end updates TableView block. The reason that you want to do this is, let's say you go ahead and insert 100 rows, and you call insert rows of indexes with 100 of them, and then you delete those 100 rows, or you insert another 10 or 20.
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 is smart enough to optimize it and do only what the user would end up seeing. So for performance, you want to call begin updates and end updates.
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 of what I mean by that.
You call begin updates, do insert row at index with an index of 2, opens up a slot, you get that new first row. You call it again, 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 tried 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 I showed before, and this is a little video, is how do you animate the row heights for the table? In addition, the table is 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 note height of rows with indexes changed. So this assumes you're using variable row heights and using the variable row height delegate methods. You 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 ViewBase TableView.
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 begin grouping and end grouping 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 the image view.
Then you can go ahead and tell the table to do its animation by calling note height of rows with indexes changed. 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 to 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 work within a cell. And in fact, all these methods will work within an NSCell-based TableView. The insert, the remove, the move, the height of rows with indexes changed will all work. But you have to do one thing. To make them work with the NSL-based TableView, you have to call begin updates and end updates.
When you call begin updates, what the TableView will do, it's going to draw all of your NSCells into a temporary image and swap them out with a View-based TableView 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 a Lion for outline views which were not moved to a View-based TableView.
And if you want to see an example, the drag-and-drop OutlineView 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 a row view comes into play, and how it all fits together.
Construction, Data Source Methods, and Design Time Layout. Bindings, Bind Content, and Customizing TableViews. Download the Hover Table demo, check it out, play around with the source. Talked about drag and drop, creating dragging image components, 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. For more information, contact Bill Dedeny, our Frameworks Evangelist. Our documentation has been updated for the View-based TableView. We have the developer forums. I frequently browse around there and answer table questions.
We have 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 TableViews, 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 a view.