Wednesday, June 4, 2014

CPTableView: Step-by-Step Tutorial: Part 3

If you have been following along, you have all the files you need, but if you need to download the package as it stands at the end of Part 2, then you can get it at GitHub using this link: https://github.com/JaredClemence/CPTableView.

In the previous tutorial, we got the table up and running, but we still are using basic strings as the primary data object and the default view classes.  Today, we will use a custom object as the data element, and we will also create custom data views so that we can see how to start branching out of the default string display into more complex, layered views.

Creating Our Primary Data Object

Current Table
Each row of our table, shown in the "Current Table" figure relates to a single entity.  Each cell is one aspect or detail about the weapon characteristics, yet in our data source object, we do not treat this as a single item.  We have four separate arrays of data, and the fact that we have not gotten any of the data elements reversed in those arrays is pure luck (or attributed to our very small data set).

It is not practical for us to use individual data arrays for our more complex objects in large data sets. At some point, we need to learn to treat every Tuple of the table as a single object, and design it the same way. (Note: Tuple is a fancy database term which describes a row of data in a database table.   A tuple usually describes a single, unique object within a data set.)

Using object oriented design practices and some future intentions, which I have not yet shared with you, we arrive at the following object design for our data element:
UML created by MagicDraw PE
For our example, we can treat all these aspects as part of a single object called Weapon.  We will create these objects in files called 'Weapon.j' and 'Skills.j'.  Not all of the functions have been shown in the UML; I didn't want to make the drawing too complicated.

Copy and paste the following into Weapon.j:
@import <Foundation/Foundation.j>
@import 'Skills.j'
@implementation Weapon : CPObject
{
     CPString name @accessors;
     CPArray quality @accessors;
     Skill damage @accessors;
     SkillSpecialty attack @accessors;
}
-(id)initWithName:(CPString)aName
       attackSkill:(CPString)attackSkillName
       attackSpecialty:(CPString)attackSpecialtyName
       damageSkill:(CPString)damageSkillName
       qualityArray:(CPArray)qualityArray
{
     self = [super init];
     if( self )
     {
          name = aName;
          attack = [[SkillSpecialty alloc] initWithSkill:attackSkillName andSpecialty:attackSpecialtyName];
          damage = [[Skill alloc] initWithSkill:damageSkillName];
          quality = qualityArray;
     }
     return self;
}
-(CPString)quality
{
   //overrides accessor
   var joinedString = @"";
   for( var i = 0; i < qualityArray.length; i++ ){
         if( joinedString != "" ){
              joinedString = joinedString + ", ";
         }
         joinedString = joinedString + qualityArray[i];
   }
   return joinedString;
}
@end
Insert the following into Skills.j:
@import <Foundation/Foundation.j>
@implementation Skill : CPObject
{
     CPString skillName @accessors;
}
-(id)initWithSkill:(CPString)aName
{
     self = [super init];
     if( self )
     {
          skillName = aName;
     }
     return self;
}
-(CPString)asString
{
     return skillName;
}
@end
@implementation SkillSpecialty : Skill
{
     CPString specialtyName @accessors;
}
-(id)initWithSkill:(CPString)aName andSpecialty:(CPString)aSpecName
{
     self = [super initWithSkill:aName];
     if( self )
     {
          specialtyName = aSpecName;
     }
     return self;
}
-(CPString)asString
{
    return skillName + @" + " + specialtyName + @" Specialty";
}
@end
Now that we have created the model objects, we can adjust our data source accordingly.  Edit WeaponTableDataSource.j with the following changes:

Add to the top of the file:
@import 'Weapon.j'
Replace all the attributes with just one:
CPArray weaponList
Replace the init function with the following code:
self = [super init];
if( self )
{
     weaponList = [[CPArray alloc] init];
     [self fillDataValues];
}
return self;
Replace the fillDataValues method with the following code:
var weaponValues = [
           ["Battleaxe", "Fighting", "Axe", "Athletics", ["Adaptable"]],
           ["Morningstar", "Fighting", "Bludgeon", "Athletics", ["Shattering 1","Vicious"]],
           ["Whip", "Fighting","Brawling", "Athletics - 1", ["Slow"]],
           ["Bow, Hunting", "Marksmanship","Bow", "Agility", ["Long Range", "Two-handed"]]
 ];
var curWeapon = nil, weaponNameIndex = 0, weaponAttackSkillIndex = 1, weaponAttackSpecIndex = 2, weaponDamageSkillIndex = 3, qualArrayIndex = 4;
var name = nil, damageSkill = nil, attackSkill = nil, attackSpec = nil, quality = nil, weaponObject = nil;

for( var i = 0; i < weaponValues.length; i++ ){
          curWeapon = weaponValues[i];
          name = curWeapon[ weaponNameIndex ];
          damageSkill = curWeapon[ weaponDamageSkillIndex ];
          attackSkill = curWeapon[ weaponAttackSkillIndex ];
          attackSpec = curWeapon[ weaponAttackSpecIndex ];
          quality = curWeapon[ qualArrayIndex ];
          weaponObject = [[Weapon alloc] initWithName:name attackSkill:attackSkill attackSpecialty:attackSpec damageSkill:damageSkill qualityArray:quality];
         [weaponList addObject:weaponObject];
 }
Replace the numberOfRowsInTableView function with the following:
return [weaponList count];
 Replace tableView:objectValueForTableColumn:aColumn row: with the following:
 var columnIdentifier = [aColumn identifier],
       curWeapon = [weaponList objectAtIndex:rowNum];
//we set the column identifiers when we first initialized them.  We will use these same values as names or identifiers here to distinguish one column from the next
switch( columnIdentifier ){
case 'Weapon':
      return [curWeapon name];
case 'Damage':
      return [[curWeapon damage] asString];
case 'Attack':
      return [[curWeapon attack] asString];
case 'Quality':
      return [curWeapon quality];
default:
      return @'Error String'; //we hope never to see this value.
}
That is a lot of code changes!  If you followed along as you were making these changes, you have noticed the following.  In our model, we have created a Weapon, a Skill and a SkillSpecialty data elements.  This particular program is not overly advanced, nor is it intended to serve a functional purpose, so we will live with the small conceptual error that some hard-core gamers might notice: Some hard core gamers might notice that we define our attack skill as "Agility - 1", when really this is a dice roll equation which uses a skill to define the outcome.  If we were building this application for production, we would go back over our UML model and correct our abstraction error.

After creating these data objects, we went to our data source object and made a lot more changes to convert all those separate arrays into a single and simple list of weapons, which programmatically is a little easier to grasp as we implement our designs and incorporate our design into a larger model and view architecture.

New Weapon based Table
None of the view elements were changed at all.  We did not adjust the columns.  We did not adjust the CPTableView, and we did not adjust the CPScrollView.  But the underlying structure changed completely.  Reload the page, and you will see that we have the same table that we had before:


However, while the table looks exactly the same, each row is tied to a weapon object.  When that object is selected, it can be passed as a group to another object.  Each cell on the row displays a different set of data, which depends on the column that is displaying the data, and not on the Weapon object itself.  The table tells the data source: "Hello, I would like to display the Attack information for the 2nd weapon on your list," and the data source responds by looking at its list for the second weapon and requesting the appropriate information on behalf of the table.

Customizing Views and Allowing Your Views to Decide How Data is Displayed

This is great, but we can do better!  What if the dataSource provided the information only, and did no additional processing to format the data?

What if the data view of the table itself decided how to display all the information?

The problem with our current design is this: in a MVC (model - view - controller) design, the dataSource that we have designed is not really part of the view, and yet it is making decisions on how to display information.  Even worse!, we allow the SkillSpeciatly object to define the display string, and this object is clearly removed from the View elements, being a core element of the object Model.

In the next steps, we will create some special views for the cells, and we will fix these issues.

No comments:

Post a Comment