Understanding Table Views
In the most general terms, a table view is made up of rows and one or more columns that display the content of a data collection. Each row represents a single item within the data model, and each column displays a specific attribute of the model. A cell represents the content at a specific row-column intersection. The user selects rows and the app can perform the appropriate action on those rows.
Tables are made up of versatile user interface elements that can display simple lists of data or complex arrangements that combine data, functionality, and controls to provide a rich user experience. For example, the simple two-column table shown in Figure 1-1 uses one cell that displays an image and text and a second cell that displays only text.
Constructing this simple table view required no subclassing to display the content; both cell views are instances of the
NSTableCellView class. To create a similar table, you simply drag the stock classes from the Interface Builder Object library and set the values of the appropriate subviews. (You can examine the implementation of this table in the TableViewPlayground sample project.)
In contrast, the left area of the window shown in Figure 1-2 contains a complex table view in which each row consists of an image, some text, a custom color view, and buttons. Much, if not all, of the complex table view shown here is made using standard AppKit view objects that are arranged in a custom subclass of
NSTableCellView. (The implementation of this table is also contained in the TableViewPlayground sample project.)
Most Tables Are Based on NSView
Most tables are
NSView based, which means that each cell is provided by an
NSView subclass, often by
NSTableCellView (or a subclass). Some tables are
NSCell based, which means that each table cell is based on a subclass of
NSCell. For the most part,
NSCell-based tables are used to support legacy code; if you’re creating a new app, you want to use
NSView-based tables. In this document, a table is assumed to be
NSView based unless specified otherwise.
NSView objects as cells allows table views to enable rich design time opportunities. By default, an
NSTableCellView object includes an image view and a text field. To build a table view in Interface Builder, you create a cell for each column by dragging an
NSTableCellView instance from the Object library and dropping it into the appropriate table column. You then configure each cell, called a prototype cell, with text, images, or other attributes. At runtime, the data source loads a prototype cell for each cell in the row and displays the data according to your configuration. Using Interface Builder, it’s easy to modify the cell’s subviews, and to move, resize, and hide cells. And when you use
NSTableCellView instances in a table, VoiceOver automatically speaks the contents of the text field.
You can subclass
NSTableCellView to add additional subviews and behaviors, and you can use Interface Builder to modify a cell’s design and layout. Whether you use the standard
NSTableCellView class or a custom subclass, an app retrieves a cell view at runtime and populates it with data, either programmatically or using Cocoa bindings. Because tables reuse cell views when possible, cell views can be varied and complex without negatively impacting memory usage.
View classes support animation within their content, so it’s straightforward to animate content within
NSTableCellView instances. For example, the TableViewPlayground sample project uses
NSProgressIndicator instances to display content that’s loaded lazily.
Tables support animation of cells as they are moved, inserted, and deleted. Various animation modes are provided and they can be grouped to allow changes to happen in batches as you make changes within the table view and corresponding changes in the model data.
Tables Consist of Several Classes That Work Together
Table views are made up of the following classes:
This class declares methods that allow you to configure the appearance of the table view—for example, specifying the default height of rows or providing column headers that are used to describe the data within a column. Other methods give you access to the currently selected row as well as to specific rows or cells. You can call other table view methods to manage selections, scroll the table view, and insert or delete rows and sections.
The column class is responsible for managing the horizontal position and the width of the cells of the table. Columns can be configured to allow resizing, re-ordering, and content sorting, with the state (optionally) stored with the app. Each table column has an identifier associated with it, which is key when finding columns and retrieving cells. Identifiers are discussed in more detail in “Columns and Cells Have Identifiers That Make It Easy to Find Them.”
The header view class is responsible for the cell that does its actual work, the
NSTableHeaderCell. The header cell is responsible for drawing the column name—if it’s visible—as well as the header content itself, optional sort indicators, drawing highlighting, and more.
These two classes aren’t part of the table view—nor are they required—but virtually all table views are displayed using the classes that make up the scroll view mechanism.
NSView-based table views rely heavily on two additional classes (
NSTableCellView) and their subclasses. Figure 1-3 shows how these components come together to create a table view.
The blue rows shown in Figure 1-3 are highlighted as if selected by the user. The red rows are rows that have been subclassed to draw in their background in a custom manner. Empty or missing cells are shown in the table and are allowed within table views.
A table view consists of a collection of multiple
NSTableRowView instances, one for each visible row. The frame rectangle of the row view is the full width of the table view and the height of the row, taking into account the
intercellSpacing. Row views are responsible for drawing selection, drag and drop feedback, highlighting, column dividers, and any additional custom indicators that may be required, including a custom background. Each
NSTableRowView has a collection of subviews that contain each table column cell.
You can use the delegate method
tableView:rowViewForRow: to customize row views. You typically use Interface Builder to design and lay out
NSTableRowView prototype rows within the table. As with prototype cells, prototype rows are retrieved programmatically at runtime. Implementation of
NSTableRowView subclasses is entirely optional.
NSTableCellView instance is inserted as a subview of an
NSTableRowView instance, which represents the entire row. The default
NSTableCellView class has Interface Builder outlets for a text field and an image view. VoiceOver automatically speaks the contents of the text field, giving your app basic accessibility capabilities without effort on your part.
Apps typically create subclasses of
NSTableCellView to add additional properties; optionally, you can use custom
NSView instances to create new cells. Figure 1-4 shows the components of the default
A Table View Needs a Data Source and Should Have a Delegate
Following the Model-View-Controller design pattern, an
NSTableView object must have a data source. To control the display of data, a table view should have a delegate.
The data source class (
NSTableViewDataSource) mediates between the table view and the table view’s model data. The data source is responsible for implementing the support that provides the model data in a pasteboard format that allows both copying of data and dragging of rows. (Note that to support the drag and drop of single rows to the Finder, your model object must be compliant with the
NSPasteboardWriting protocol.) The data source is also responsible for determining whether incoming drag actions are valid and how they should be handled.
The delegate class (
NSTableViewDelegate Protocol) allows you to customize a table view’s behavior without requiring you to subclass the table view. It supports table column management and type-to-select functionality, and lets you specify whether specific rows should allow selection, among other behaviors.
The data source and the delegate are often (but not necessarily) the same object.
The data source object must adopt the
NSTableViewDataSource protocol, and the delegate object must adopt the
NSTableViewDelegate Protocol. When programmatically populating a table view, there are methods that must be implemented in both the data source and the delegate (to learn more about this, see “Populating a Table View Programmatically”).
To enable editing of table view cells, use the target-action methodology in order to edit the content.
The responsibilities of both the delegate and data source classes differ when Cocoa bindings are used for populating tables—for more information, see “Populating a Table View Using Cocoa Bindings.” (If you’re using an
NSCell-based table, the responsibilities of the data source and delegate objects are somewhat different; to learn more, see “Working with NSCell-Based Table Views.”)
Columns and Cells Have Identifiers That Make It Easy to Find Them
Every column in a table view has an associated identifier string that is set in the Identity inspector of the Xcode Interface Builder editor. This string is a convenient and versatile way of referring to and retrieving individual columns in a table view.
Apps typically use context-based names for the identifier of a table column, such as Artist or Name, so that the columns can be easily retrieved and identified by other aspects of the app. For example, if your app allows the showing and hiding of table columns, it can easily identify the column that must be shown or hidden by using the identifier and the
tableColumnWithIdentifier: method. Using column identifiers is important because if the columns get reordered, a column’s index in the
tableColumns array also changes.
Column identifiers relate a table column to a view instance that the column displays. When the
NSTableView delegate method
tableView:viewForTableColumn:row: attempts to locate the cell for the table column, it uses the column’s identifier to locate the cell. If you set identifiers manually, you must ensure that the column and cell view identifiers stay in sync if you change either one. If the identifier values get out of sync, a table that relies on bindings for its data won’t work. If you plan to use bindings, it’s recommended that you use the Automatic setting for the identifier values.
If your table view doesn’t require complex table column management, such as retrieving individual table columns or showing and hiding columns, you can take advantage of the Automatic function of the Identifier. By default, the Identifier field is set to Automatic. When you accept the Automatic setting, Interface Builder creates a unique identifier for the table column and ensures that the cell view instance within that table column has the same identifier. Further, it keeps these identifiers in sync, relieving you of that responsibility.
Table Views Reuse Rows to Increase Speed and Efficiency
Table views create and maintain a queue of used cell views that allow for the efficient reuse of previously created cells. A cell view is considered to be in use if it:
Is currently visible
Is the currently selected cell (even if it isn’t currently visible)
Has editing in progress, for example, a text field the user is editing
Views that don’t fit the in-use criteria are inserted into the reuse queue.
Because table view cells are instantiated from within the
NSTableView instance in the Interface Builder editor, each cell is treated specially, as if it were its own nib. Each cell view has an owner. By default, a cell view’s owner is the table view’s default owner, which is usually the table’s delegate.
When a new view for a specific ID is required, the table uses the process shown in Figure 1-5 to find and return the requested view.
By using the reuse queue, the table view is able to use memory effectively as well as increase the speed with which cell views are retrieved.