Monday, June 16, 2014

A first cut at a swift library to display a UIPickerView

Below is my first cut at creating a class that manages the data for a picker view.  The standard examples have your view controller managing the rows and data for the picker.  I think that this is a bad connection of concerns, so I wanted a generic class that will manage the data for me.

The example is a simple polyhedral dice roller, which lets you pick a number of dice, of a certain type, and add a modifier.

Below is a screenshot of the picker.

As you can see I just have a simple picker at the top of the screen.  Right now all it does is print out the results to the console:

selected row [1, d8, +, 0]
selected row [3, d8, +, 0]

selected row [3, d8, +, 2]


The view code is just concerned with establishing the data that we wish to display:

 var delegate:PickerProvider? = nil;
  
    init(nibName nibNameOrNil: String!, bundle nibBundleOrNil: NSBundle!)
    {
        super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
    }
    
    func getArray(minNum:Int,maxNum:Int) -> Int[]
    {
        var returnValue=Int[]();
        for i in minNum...maxNum
        {
            returnValue.append(i);
        }
        return returnValue;
    }
    
    
    init(coder aDecoder: NSCoder!)
    {
        delegate=nil;
        super.init(coder:aDecoder)
    }
   
    
    override func viewDidLoad() {
        super.viewDidLoad()

        // Create an array of numbers for dice.  Use the map function to prepend a 'd' in
        // front of them.
        let dieTypes=[4,6,8,10,12,20].map{ (var number) -> String in
            return "d\(number)";
         };
        
       
        // Note that this is actually a (selectedRows:AnyObject[])->() even though it is
        // declared as (selectedRows:AnyObject[]).  If you declare the paraemter of the method
        // to be (value:AnyObject[]) you will get a compile error.
        let returnValue = { (selectedRows: AnyObject[])->() in
            println("selected row \(selectedRows)");
        };
        
        let numDice=getArray(1,maxNum:20);
        let modifiers=getArray(0,maxNum:30);
        let modRolls=["+","-"];
        
        // Pass the callback to the new delegate, as well as the list of arrays.
         delegate=PickerProvider(selectedCall:returnValue
            ,arrays:numDice,dieTypes,modRolls,modifiers);

        // Set the picker view to use the subclass as both a delgate and dataSource.
        pickerView.dataSource=delegate;
        pickerView.delegate=delegate;
    }

    @IBOutlet var pickerView : UIPickerView
  
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
    

I started from a simple 'single view' template, added a picker view, and set the delegate to be 'pickerView'.

The core of the logic is in the PickerProvider class, this class is designed to be a general supplier of picker information.  Right now it only displays text titles, and will subdivide the picker into equal segments.

class PickerProvider :NSObject, UIPickerViewDataSource,UIPickerViewDelegate
{
    
    var myarrays:Array<Array<AnyObject>>;
    var callback:(value:AnyObject[])->();
   
    
    // The initial method will take a callback for when a selection si made as well
    // as the array of objects to display.
    init(selectedCall:(value:AnyObject[])->(), arrays: Array<AnyObject> ... )
    {
        myarrays=arrays;
        callback=selectedCall
    }
    
    // Standard override return the number of rows for the given component.
    func pickerView(pickerview: UIPickerView!,numberOfRowsInComponent component: Int)
    -> Int
    {
        let array=getArray(component);
        return array.count;
        
    }
    
    // returns the number of 'columns' to display.
    func numberOfComponentsInPickerView(pickerView: UIPickerView!) -> Int
    {
        return myarrays.count;
    }
    
    // Internal function to retrieve a specific array.
    func getArray( compNum:Int) -> Array<AnyObject>
    {
        return myarrays[compNum];
    }
    
    
    


    // Get the value for a given component.  Just use the standard swift to make it into a string.
    func pickerView(pickerView: UIPickerView!, titleForRow row: Int, forComponent component: Int) -> String
    {
        let array=getArray(component);

        let value : AnyObject=array[row];
        return "\(value)";
    }
    
    // Right now weight all arrays equally for the size.  For a real approach we would need to be
    
    func pickerView(pickerView: UIPickerView!, widthForComponent component: Int) -> CGFloat
    {
        let numComp:NSNumber=self.numberOfComponentsInPickerView(pickerView);
        let width:Double=pickerView.frame.width;
        let value=width / numComp.doubleValue;
        return value;
    }
    
    func pickerView(pickerView: UIPickerView!, didSelectRow row: Int, inComponent component: Int)
    {
        var returnValue=AnyObject[]();
        for i in 0..myarrays.count
        {
            let selected=pickerView.selectedRowInComponent(i)
            let array=getArray(i);
            let value=array[selected]
            returnValue.append(value);
        }
        self.callback(value:returnValue);

    }
    

}

The core of the logic is in the PickerProvider class, this class is designed to be a general supplier of picker information.  Right now it only displays text titles, and will subdivide the picker into equal segments.

Overall I would say that Swift makes for better written code.  I've done similiar constructions in Objective-C, and this is far cleaner.  The code is very readable, and barring a few quirks of Swift fairly easy to write.

This examples shows several usages of Swift constructs, the Closures are easier to write than blocks and are first class citizens.  The map function is very neat and shows the ability to transform lists.

List creation and manipulation is far less verbose than in Objective-C, as is displaying primitives as a string.

4 comments:

  1. Very slick solution to the picker problem, thank you for presenting. I'm trying to learn swift and the UIPicker simultaneously (kinda slow work) and would like to understand how you would use this PickerProvider class to respond to, say a button click which asks for the current value of the UIPickerView.

    Thanks,

    Sean

    ReplyDelete
  2. On further inspection I figured it out. Thanks again!

    Sean

    ReplyDelete
  3. Sorry I didn't see the comment at first. The callback is called when the picker view is changed. This allows the caller to be notified whenever a change is made. If you want to query on the fly the code:
    var returnValue=AnyObject[]();
    for i in 0..componentArrays.count
    {
    let selected=pickerView.selectedRowInComponent(i)
    let array=getArray(i);
    let value=array[selected]
    returnValue.append(value);
    }
    self.callback(returnValue);

    shows how I collect the selected values into an array and call the callback. This could easily be made into a routine that can be queried for the current selection as well.

    ReplyDelete
  4. Hi Jon,
    Thanks so much for responding! I struggled for a while with the "closure" you were using for returnValue. I'm still not sure I understand it but the callback is happening and I'm calling other routines from within it, so no big deal.

    I'll look at your latest mods. I suspect it will help me greatly to fully understand what you're doing.

    Rgds,

    Sean

    ReplyDelete