Saturday, June 28, 2014

Version 2 of swift UI picker view

This version's Goals

The previous iteration of the picker view was great, but only supported pure text.  The next version supports using an object as well as supplying more utility routines to provide data.

For this version I want to clean up the API and make it more general and add the following capabilities:


  • Be able to use an object to supply the data, and make that object available when it is picked
  • Have an easier interface to provide data to the picker view
  • Be able to have varying widths when implementing displaying the text.

These goals will implement the vast majority of pickers required, the main item missing will be the ability to provide custom views for the picker images,

The previous version supported an object, but that object had to implement a protocol and it was not a very general solution. This version will keep the implementation of the object pure, and let the picker view work with pretty much any object.

Calling Implementation

Die data type

My datatype is a very simple die type.  It contains the ability to define the die in the constructor (initializer) of the data type, as well as a method that generates a random number:
@objc class Die
{
    var dieType:Int
    
    init(_ die:Int)
    {
        dieType=die;
    }

    
    func roll () -> Int
    {
        let result=Int(arc4random_uniform(UInt32(dieType)));
        
        return result + 1;
    }

}
Note that my previous solution had the Die implementing a protocol and the picker provider.

Creating the dice

When creating the dice array, we can use the map function to create the array just like before:
let dieTypes=[4,6,8,10,12,20].map{ (var number) -> Die in
            return Die(number);
         };
In this case since the die type is no longer implementing a protocol, I supply a simple closure that takes an AnyObject, casts it to a Die and supplies the text I want:
 let dieToText={ (value:AnyObject)->String in
            let die = value as Die;
            return "d\(die.dieType)";
        };
This is a more general solution, since I can now decorate any object with a simple provider, and it should scale to supplying views as well as title strings.

Creating the picker view

The big change was that I modified the picker view to have a fluent interface.  Objective C and Swift supply some of these capabilities, but in this case I wanted to support many different objects without writing a ton of constructors. In this case I also supply a 'weight'. The sum of all weights is added up (it defaults to 1), and then that is used as the basic to calculate the width of each component.
I also added methods that create numeric ranges.

delegate=PickerProvider().changeCallback(returnValue)
            .addComponentRange(1, maxValue: 20).setWeight(1.5)
            .addComponent(dieTypes)
            .setTextProvider(dieToText).setWeight(3)
            .addComponent(["+","-"]).setWeight(0.5)
            .addComponentRange(0,maxValue:30).setWeight(1.2);

The callback

The callback for when an item is selected will create a roller object with the selected values. Right now I have to do a lot of casting, I'm not sure of a good solution for that, but since I supply the components at the same location as I supply it, then it should be relatively type safe.
 func getDieText(value:AnyObject[])->String
    {
        var numDice:Int=value[0] as Int;
        var dieType:Die=value[1] as Die;
        var modifier:String=value[2] as String;
        var num:Int=value[3] as Int;
        
        currentRoller = Roller(numDice: numDice, dieType: dieType, modSign: modifier, modifier: num)
        
        return "\(numDice)d\(dieType.dieType)\(modifier)\(num)";
        
    }

The Roller is simple:
@objc class Roller
{
    var myDice=0;
    var myDie:Die;
    var myMod:String;
    var myModifier:Int;
    
    init(numDice:Int, dieType:Die, modSign:String, modifier:Int)
    {
        myDice=numDice;
        myDie=dieType;
        myMod=modSign;
        myModifier=modifier;
    }
    func roll()->String
    {
        var result=0;
        var stringResult="\(myDie.dieType):"
        
        for i in 1...myDice
        {
            if i != 1
            {
                stringResult+=",";
            }
            
            let roll = myDie.roll();
            result += roll;
            stringResult += "\(roll)";
        }
        if myModifier > 0
        {
        if (myMod == "+")
        {
            result += myModifier;
            
            stringResult += "+\(myModifier)"
        }
        else
        {
            stringResult += "\(myModifier)"
            result -= myModifier;
        }
        }
        stringResult += "=\(result)"
        return stringResult;
    
    }
}

Picker Provider implementation


//
//  DieRollDataSource.swift
//  SimpleDieRoller
//
//  Created by Jon Lundy on 6/4/14.
//  Copyright (c) 2014 Jon Lundy. All rights reserved.
//

import Foundation
import UIKit

// Component Info keeps information about each component. 
// It uses an array to track the data, as well suppling
// a text provider that converts an item in the array to
// a string.  Note that I have a placeholder for a view
// provider, but that is not implemented yet.
class ComponentInfo
{
    // The items contained within this component.
    var componentArray:Array<AnyObject>;
    // The default text provider just casts the object to value.
    // Convert an item to a string. The default is
    // the built in Swift conversion.
    var textProvider:((AnyObject)->String)={(value:AnyObject) in return "\(value)"};
    
    // Not implemented yet.
    var viewProvider:((AnyObject)->UIView)?=nil;
    // By default just use a weight of 1.
    var weight:Double=1;
    
    // A simple constructor.
    init(_ array:Array<AnyObject>)
    {
        componentArray=array;
    }
}
//the core class for the picker.
class PickerProvider :NSObject, UIPickerViewDataSource,UIPickerViewDelegate
{
    
    var componentArrays:Array<ComponentInfo>=Array<ComponentInfo>();
    var callback:(AnyObject[])->()  ;
   
    
    // Change the default callback, the default is to do
    // nothing.
    func  changeCallback(selectedCall:(AnyObject[])->()) ->PickerProvider
    {
        println("Entering setCallback");
        
        callback=selectedCall;
        return self;
    }
    // Set the text provider for the last component added.
    func setTextProvider(provider:(AnyObject)->String)->PickerProvider
    {
        componentArrays[componentArrays.count-1].textProvider=provider;

        return self;
    }
    // Set the weight for the last callback.
    func setWeight(weight:Double) ->PickerProvider
    {
        componentArrays[componentArrays.count-1].weight=weight;
        return self;
    }
    // Add a component
    func  addComponent(newArray:Array<AnyObject> )->PickerProvider
    {
        var comp = ComponentInfo(newArray);
        componentArrays.append(comp);
        return self;
    }
    
    // Add a range of numbers.
    func addComponentRange(minValue:Int, maxValue:Int)->PickerProvider
    {
        var newarray=getArray(minValue, maxNum:maxValue)
        addComponent(newarray);
        return self;
    }
    
    // Create an array of numbers.
    func getArray(minNum:Int,maxNum:Int) -> Int[]
    {
        var returnValue=Int[]();
        for i in minNum...maxNum
        {
            returnValue.append(i);
        }
        return returnValue;
    }
    
    // The initial method will take a callback for when a selection si made as well
    // as the array of objects to display.
    init()
    {
        callback = { (selectedRows: AnyObject[])->() in
        };

          
    }
    
    // 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 componentArrays.count;
    }
    
    // Internal function to retrieve a specific array.
    func getArray( compNum:Int) -> Array<AnyObject>
    {
        return componentArrays[compNum].componentArray;
    }
    
    
    


    // 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
    {
        var comp=componentArrays[component];
      
        let value : AnyObject=comp.componentArray[row];
        return comp.textProvider(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
    {
        var totalWeight:Double=0.0;
        var myComponent=self.componentArrays[component].weight;
        
        for  component in self.componentArrays
        {
            totalWeight += component.weight;
        }
        let width:Double=pickerView.frame.width-10;
        let value=width / totalWeight * myComponent;
        return value;
    }
    
    func pickerView(pickerView: UIPickerView!, didSelectRow row: Int, inComponent component: Int)
    {
        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);

    }


    

}

What's next:

The next version should support:
The ability to have a custom view in addition to pure text in the picker.
The ability to have the contents of some of the components change based upon selections in the previous components (for example the date picker won't allow you to pick Day 31 for February.

No comments:

Post a Comment