Wednesday, June 4, 2014

CPTableView: a Step-by-Step Tutorial: Part 1

Yesterday, we reviewed the concept of the CPTableView.  Today, we learn how to use it through some fun examples.  Start a new project and follow along with the code as it develops here.

Connor Denman and our First Table


During my early investigations into CPTableView, I hit a lot of walls.  Connor Denman produced a tutorial that got me started, so out of respect to the man who got me started, we are going to start with his code and a brief explanation.  We are then going to move beyond Mr. Denman’s work and play with some other concepts.

If you having created a new project already, do so now.  We will copy and paste Mr. Denman’s code into the AppController’s applicationDidFinishLaunching method:


var theWindow = [[CPWindow alloc] initWithContentRect:CGRectMakeZero() styleMask:CPBorderlessBridgeWindowMask],contentView = [theWindow contentView];scrollView = [[CPScrollView alloc] initWithFrame:[contentView bounds]];tableView = [[CPTableView alloc] initWithFrame:CGRectMakeZero()];[tableView setUsesAlternatingRowBackgroundColors:YES];//[tableView setAutoresizingMask: CPViewMinXMargin | CPViewMaxXMargin | CPViewMinYMargin | CPViewMaxYMargin];
data = [[CPArray alloc] init];[data addObject:@"test"];[data addObject:@"another 1"];
var theColumn = [[CPTableColumn alloc] initWithIdentifier:@"theColumn"];[[theColumn headerView] setStringValue:@"The Column"];//[theColumn setMinWidth:300];
[tableView addTableColumn:theColumn];
//[theColumn setTableView:tableView];
[scrollView setDocumentView:tableView];
[tableView setDataSource:self];
[tableView setDelegate:self];
[contentView addSubview:scrollView];
[theWindow orderFront:self];

We will also copy and paste these two methods into the AppController object:

- (int)numberOfRowsInTableView:(CPTableView)aTableView {    return [data count];}
- (id)tableView:(CPTableView)aTableView objectValueForTableColumn:(CPTableColumn)aTableColumn row:(int)rowIndex {    return [data objectAtIndex:rowIndex];}
And lastly, we will copy and paste these variable definitions into the AppController's property list:
CPScrollView scrollView;
CPTableView tableView;
CPArray data;
When we run the application, we see the following:

Connor Denman's CPTableView Example
So, that is Mr. Denman's work, which he covers in the YouTube video linked above in the section title, and we will briefly review here.

A Quick Review: What is happening and What we will cover

Mr. Denman's table uses a lot of default settings.  He has turned the AppController into the data source, which is why he has added the two functions that we copied and pasted above.  In the CPTableView overview, we learn that these functions are required of all objects used as a data source.  After this minor customization of the AppController object, he is able to pass the AppController as the data source by calling [tableView setDataSource:self] from within the AppController.

Because the data that he returns from his provider function is a basic string data type, the table view column can use default views to display the data.  We will see later that if we return non-string or non-integer data types, the column reduces the object to a string using the description method, which returns a string with the object's class and what I believe to be a unique variable identifier that identifies the object instance within spectrum of all objects of that class currently loaded within memory.  This is not what we want to display, so later we will learn to create custom views for data objects.

Also, we notice that the data provider method that Mr. Denman has created returns a string based on the row number.  The table view passes an instance of itself as well as the column that will be used, but Mr. Denman does not use these added values in order to return specific and distinguished results for each column and row combination.  What this means is that for every row in Mr. Denman's table, the same string value will appear in every column.  Mr. Denman only has one column, so we don't see this now, but we will, and we will also learn how to use the column identifier in order to pull a specific value from a data object.

Playing with Denman's Creation

Playing with Table Dimensions

The table fills the entirety of the window, because the containing scroll view is initialized with the boundary of the borderless-bridge window.  This means that the origin is set to (0,0), and the width and height are the same dimensions as the containing view.  We can change the location of the table by setting a new frame.  For example, by adding "[scrollView setFrame:CGRectMake(50,50,200,50)];" immediately before the line "[theWindow orderFront:self];" we see the following:

Adding Table Borders

If you have tried this little experiment, you will notice that the table can scroll up and down to expose the hidden rows of the table, and the column header does not move.  This is the benefit of the scroll view.  The scroll view contains the header view and a clipped view that contains the table; it provides scroll bars as needed to allow the clipped view to move the underlying document, in this case the table.

This table is hard to see, because there are no borders or boundaries. We can use the scroll view's border type to change this and make the scroll view more visible. Acceptable border types are CPNoBorder, CPLineBorder, CPBezelBorder, CPGrooveBorder. We will use CPLineBorder.  Add the following line to our code after the last addition:
[scrollView setBorderType:CPLineBorder];
Now, we reload the page and see the following:
Now the table has a clearly defined edge which helps it stand off from the white background. Feel free to play with the size and shape of this table view to get a better understanding of it.

Playing Briefly with New Columns

Lets add a new column and see what happens.  Earlier, we predicted that the data would be copied into every column on the same row, because of how the data provider function is called.  Let's add another column and find out.

In your code, you will find that you already have the following lines:
    var theColumn = [[CPTableColumn alloc] initWithIdentifier:@"theColumn"];
    [[theColumn headerView] setStringValue:@"The Column"];
    //[theColumn setMinWidth:300];
    [tableView addTableColumn:theColumn];
    //[theColumn setTableView:tableView];
After that, add the following:
    var secondColumn = [[CPTableColumn alloc] initWithIdentifier:@"secondColumn"];
    [[secondColumn headerView] setStringValue:@"Column 2"];
    [tableView addTableColumn:secondColumn];
Let's also change the frame definition of the scrollView from CGRectMake(50,50,200,50) to CGRectMake(50,50,200,250);

Here, we create a column with an identifier string. Next, we ask the column to provide the headerView so that we can set a string value.  We set the string value as "Column 2".  This headerView is carried by the column so that the table and the super views can keep the header cell aligned with the table rows below.  The CPTableColumn is a direct descendant of CPObject and therefore is not a CPView and never actually appears on the display.  Instead, it holds and organizes the views for the header cells and the data view cells that form the column. As the order of the columns is changed, the objects that build the view will ask the column which view objects should be stacked along the line that forms the left edge of the column's vertical stack, and then for each instantiation of a data cell view, the constructing objects inject the data object and let the data cell view determine how to display it.  Lastly, we add the column to the table.

The table will display columns in the order that they are added.  Each time a column is added, it is appended to the end of a column array.  So, if we move the code that we just copied and pasted in front of the line "[tableView addTableColumn:theColumn]" then it displays as the leftmost column, because it is added first and takes the [0] position within the column array.  The table already contains methods that allow users to reorder columns after they are displayed using a drag-n-drop interface.

Let's reload the page and see what we have:
The string on the column headers equals the string value we set on the column's header, and the values, as predicted are just a copy of the string values that are returned from the data provider function, we will learn to change this later.

One Last Thing Before Moving On

Before we move on, let's play with one last element of Mr. Denman's Design.  Let's remove the scroll view and see what happens.  To remove the scroll view, we must only change two lines in our code.  Currently, there is one line in which the scroll view is added as a subview to the window; this line is what causes the scroll view to become part of our view hierarchy, until that moment, it exists in memory only.  Remove the line [contentView addSubview:scrollView], and we no longer have the scrollView appearing as part of our view hierarchy.  This simple action removes the scroll view, and all of its sub views.

Now, we must add the table to our view directly.  Call [contentView addSubview:tableView] to pass the table to the content view.  If you previously added tableView to the scrollView, the addSubview method will cause the table to remove itself from any previously attached superviews.  A view cannot exist in two locations on the view hierarchy at the same time.

When we reload the page, we see the following:


We see lots of things here if we are looking for them.  First, the column headers are gone!  We see here that the tableView does not draw column headers.  Instead, it provides column headers to the clipped view that held it before.  Second, we notice that our boundary is gone.  The border was set on the scroll view, so it does not affect our table view.  Thirdly, if we consider it carefully, we realize that the table does not fill the whole window as it did before, and it clearly has a new frame..... What's going on here?

What's going on is interesting.  The frame that we played with before belonged to the scrollView.  Through all of these exercises so far, we have never changed the table's frame from CGRectMakeZero() to anything else.

The origin of the table has always been (0,0) in reference to its parent view, which up till now was the clipped view.  If we had changed this, earlier, we would have noticed that the table separates from the column headings, because the headings are managed by the scroll view, and the table is contained within the clipped view, which is a subview of the scroll view.  We won't play with that here, but an image is provided so you see what happens:
The scrollView shows the table as being offset when the table's frame is changed to (50,20,0,0). This is why we always ensure that the table's frame origin is at (0,0): to keep the column headers in line with the columns they describe.
The size of the table has always been 0 width and 0 height, so we learn that it is the clipped view which caused the table to stretch to the edges of the window frame, and the columns define the minimum width, which then resizes the table as necessary to fit the data within.

If we have played with the table columns and re-ordering, we also see in our table-only example, that we are no longer able to select columns, but rows only.  CPTableView allow us to select only a row.  The scroll view provided a handle to select the column through the header view. So, without the scroll view, we are not able to reorder the columns.

Closing Thoughts on Mr. Denman's Design

Mr. Denman is to be thanked, because from his very simple example, and playing with it, we have learned much about how table views operate. In the next tutorial on CPTableView, we will expand our studies and do some more advanced processing.  We will create custom displayViews, and we will also create custom data source objects as well.

No comments:

Post a Comment