Sunday, July 20, 2014

Refactoring the table provider

As you recall, the last version of the table provider had a hardcoded cell provider:
 func tableView(tableView: UITableView!, cellForRowAtIndexPath indexPath: NSIndexPath!) -> UITableViewCell!
    {
        let cell = tableView.dequeueReusableCellWithIdentifier("dieRoll", forIndexPath: indexPath) as DieResultCell;
        let data=myArray[indexPath.row] ;
        cell.dieLabel.text="\(data)"
        
        return cell

    }
This is fine for a simple table, but I would like to allow for different types of table cells within the view and provide a general routine.

The refactoring is to allow the datatype to implement a protocol that defines the nib identifier.  In addition a closure will provide a callback that initializes the table cell with the data it is to display.

The protocol is rather simple:
@objc protocol ProvidesCellIdentifier
{
    var cellIdentifier:String { get}

}

I encapsulate my roll result into a cass, and an associated data type.  This will allow for more flexibility, and uses the Swift extension mechanism.

@objc class RollResult
{
    var rollResult:String="";
    init(value:String)
    {
        rollResult=value;
    }

}
then add an extension to my RollResult type that supplies that cell identifier:

extension RollResult: ProvidesCellIdentifier
{
    var cellIdentifier: String
    {
        return DieResultCell.cellIdentifier;
    }

}
The next step is to modify my table so that it tracks information about the various cells that it can display:
class CellConfigurationInfo
{
    
    var cellIdentifier:String="";
    var dataClass:AnyClass=AnyObject.self;
    var configuration:((UITableViewCell,AnyObject)->())={(cell:UITableViewCell,value:AnyObject) in };


This is used so that when I display a cell with the given cell identifier, I will call the configuration routine with that data.  Note that the default callback doesn't actually do anything:

Now to initialize my table, the following call is used:
  tableControl=TableViewDataProvider(self.resultTable)
            .addMapping(DieResultCell.cellIdentifier, dataClass: Die.self, configurationRoutine: DieResultCell.dieCellClosure )

Where DieResult Cell is:
class DieResultCell : UITableViewCell
{
    @IBOutlet var dieLabel : UILabel
  
    class var cellIdentifier:String { return "dieRoll"};
    
    class var dieCellClosure:(UITableViewCell,AnyObject)->() {
    
    return {(cell:UITableViewCell, value:AnyObject)->() in
        let die=value as RollResult;
        let dieCell=cell as DieResultCell;
        dieCell.dieLabel.text="\(die.rollResult)"
        
        }}


The end result is a flexible table data source that can display different data types:  The full source for the TableViewProvider is below:
//
//  TableViewDataProvider.swift
//  SimpleDieRoller
//
//  Created by Jon Lundy on 6/28/14.
//  Copyright (c) 2014 Jon Lundy. All rights reserved.
//

import Foundation
import UIKit

class CellConfigurationInfo
{
    
    var cellIdentifier:String="";
    var dataClass:AnyClass=AnyObject.self;
    var configuration:((UITableViewCell,AnyObject)->())={(cell:UITableViewCell,value:AnyObject) in };
}

class TableViewDataProvider : NSObject, UITableViewDataSource,UITableViewDelegate
{
    var configurations = Dictionary<String,CellConfigurationInfo>();
    
    let table:UITableView;
    
    init(_ mytable:UITableView)
    {
        self.table=mytable;
        myArray=Array<AnyObject>();

        super.init();
        self.table.dataSource=self;
        self.table.delegate=self;
        
    }
    var myArray:Array<AnyObject>;
    
    var cellSelected:((AnyObject)->())={(value:AnyObject) in println("Cell Selected \(value)")};

    
    func addMapping(cellIdentifier:String, dataClass:AnyClass,
        configurationRoutine:(UITableViewCell,AnyObject)->()) ->TableViewDataProvider
    {
        let info=CellConfigurationInfo();
        info.cellIdentifier=cellIdentifier;
        info.dataClass=dataClass;
        info.configuration=configurationRoutine;
        configurations.updateValue(info, forKey: cellIdentifier)
        return self;
    }
    func onSelection(selectCall:(AnyObject)->())->TableViewDataProvider
    {
        cellSelected=selectCall;
        return self;
    }
    func addArray(arrayToWatch:Array<AnyObject>)
    {
        myArray=arrayToWatch;
        self.table.reloadData();
    }
    
    func clear()
    {
        self.myArray.removeAll(keepCapacity: true);
        self.table.reloadData();
    }
    
    func addItem(itemToAdd:AnyObject)
    {
        self.myArray.insert(itemToAdd, atIndex: 0);
        self.table.reloadData();
    }
    
    func updateData()
    {
        self.table.reloadData();
    }
    func tableView(tableView: UITableView!, numberOfRowsInSection section: Int) -> Int
    {
        return myArray.count;
    }
    func tableView(tableView: UITableView!, didSelectRowAtIndexPath indexPath: NSIndexPath!)
    {
        var selectedObject=getObjectAtPath(indexPath);
        self.cellSelected(selectedObject);
    }
    
    func getObjectAtPath(indexPath:NSIndexPath)->AnyObject
    {
        return self.myArray[indexPath.row];
    
    }
    
    // Row display. Implementers should *always* try to reuse cells by setting each cell's reuseIdentifier and querying for available reusable cells with dequeueReusableCellWithIdentifier:
    // Cell gets various attributes set automatically based on table (separators) and data source (accessory views, editing controls)
    
    func tableView(tableView: UITableView!, cellForRowAtIndexPath indexPath: NSIndexPath!) -> UITableViewCell!
    {
        let data=myArray[indexPath.row] ;
        let myclass=data.classForCoder
        println("Getting object for class \(myclass)")
        let dataProtocol = data as ProvidesCellIdentifier
        
         var config=configurations[dataProtocol.cellIdentifier]!

        let cell = tableView.dequeueReusableCellWithIdentifier(config.cellIdentifier, forIndexPath: indexPath) as UITableViewCell;
        
        config.configuration(cell,data);
                return cell;
        
    }

    
    

}

Saturday, July 12, 2014

A simple table provider

For my next task I wanted to do a first cut at a simple table view.  I personally have found that the having the TableViewDataSource and TableViewDelegate be my view controller to mean duplicated code.  My first version is just a simple array of 'any object' that can be managed by a table view.

In this case I'm trying a slightly different UI than most.  I have a picker view and a table view on the same window.


In this case the top is the picker view, and the bottom is the table view.  Right now I just made a very primitive custom view that had a single 'label'.  In Xcode I gave it an identifier of 'dieRoll'.

The code to hook up my view to the table is:


        tableControl=TableViewDataProvider(self.resultTable)

To add a new row to the table I do:


            let result=currentRoller!.roll();
            self.tableControl!.addItem(result)

In this case result is an instance of my 'Die' class:

Finally the class for the table controller is:

Note that I have code in here where I can get selection events, but I currently just use a segue in interface builder for going to a subview.


class TableViewDataProvider : NSObject, UITableViewDataSource,UITableViewDelegate
{
    
    let table:UITableView;
    
    init(_ mytable:UITableView)
    {
        self.table=mytable;
        myArray=Array<AnyObject>();

        super.init();
        self.table.dataSource=self;
        self.table.delegate=self;
        
    }
    var myArray:Array<AnyObject>;
    var cellSelected:((AnyObject)->())={(value:AnyObject) in println("Cell Selected \(value)")};

    
    func onSelection(selectCall:(AnyObject)->())->TableViewDataProvider
    {
        cellSelected=selectCall;
        return self;
    }
    func addArray(arrayToWatch:Array<AnyObject>)
    {
        myArray=arrayToWatch;
        self.table.reloadData();
    }
    
    func clear()
    {
        self.myArray.removeAll(keepCapacity: true);
        self.table.reloadData();
    }
    
    func addItem(itemToAdd:AnyObject)
    {
        self.myArray.insert(itemToAdd, atIndex: 0);
        self.table.reloadData();
    }
    
    func updateData()
    {
        self.table.reloadData();
    }
    func tableView(tableView: UITableView!, numberOfRowsInSection section: Int) -> Int
    {
        return myArray.count;
    }
    func tableView(tableView: UITableView!, didSelectRowAtIndexPath indexPath: NSIndexPath!)
    {
        var selectedObject=getObjectAtPath(indexPath);
        self.cellSelected(selectedObject);
    }
    
    func getObjectAtPath(indexPath:NSIndexPath)->AnyObject
    {
        return self.myArray[indexPath.row];
    
    }
    
    // Row display. Implementers should *always* try to reuse cells by setting each cell's reuseIdentifier and querying for available reusable cells with dequeueReusableCellWithIdentifier:
    // Cell gets various attributes set automatically based on table (separators) and data source (accessory views, editing controls)
    
    func tableView(tableView: UITableView!, cellForRowAtIndexPath indexPath: NSIndexPath!) -> UITableViewCell!
    {
        let cell = tableView.dequeueReusableCellWithIdentifier("dieRoll", forIndexPath: indexPath) as DieResultCell;
        let data=myArray[indexPath.row] ;
        cell.dieLabel.text="\(data)"
        
        return cell

    }


This is rather primitive, but a starting point. I've abstracted from my view the ability to display an array.  However this currently has a hardcoded 'cell manager', which gets the proper cell, and initializes it.  In addition it only works for arrays, I would also like it to work for a NSFetchedResultsController.


Refactoring goals:

  • Pass in the configuration for determining which cell to use, and how to initialize it.
  • Abstract the management of data to a subclass to allow other sources (such as CoreData, web, etc).