It’s been a while since I made a post. Back around memorial day weekend I was about to start on my first ever iOS project, never having worked on Objective-C on have been wanting to do a post of some of the interesting bits of working with it for the first time. Unfortunately, this is not that post.
Instead, I’m going to discuss a little spark of ingenuity that came to me while I was refactoring the source code I was working on. The code had several times where it needed to create a picker to build a number using several rollers. One of the problems with just extracting out the common code was that there were several different pickers with n columns, decimals, or different limits to accomodate.
I find it somewhat strange that the Cocoa libraries don’t already provide a number picker implementation when they do provide one for dates. Oh well, that’s where this comes in. Before I begin going into actual code I do have to indicate that I haven’t quite extracted this out into a class where everyone can take advantage but when I get to that I will update this post with a link to the inevitable github repo. I also haven’t gotten to refining the class yet so there will certainly be some updates needing to be made, so bear with me (and feel free to comment with improvements and I’ll make sure to get them in now, or you can wait till the repo exists and make some pull requests).
To start, here’s our header file to indicate our public interface.
#import "AbstractPickerViewController.h"
@interface AbstractNumberPickerViewController:AbstractPickerViewController
@property (strong, nonatomic, readonly) NSNumber *maxValue;
@property (strong, nonatomic, readonly) NSArray *columnValues;
@property (strong, nonatomic, readonly) NSArray *labels;
- (id) init:(NSNumber *)maxValue;
- (id) initWithLabels:(NSNumber *)maxValue labels:(NSArray *)labels;
- (NSArray *) split:(NSNumber *)val;
- (NSArray *) splitAndPad:(NSNumber *)val;
- (NSNumber *) getValue;
- (NSString *) getSelectedLabel;
- (NSInteger) getLabelColumnNum;
- (void) setPickerToCurrentValue:(NSNumber *)val;
- (void) setPickerToCurrentValue:(NSNumber *)val label:(NSString *)label;
@end
To begin with we have our class declaration and we see that we are also extending an AbstractPickerViewController. This class is actually just a utility base class that provides some capabilities for viewing the picker in a popover for iPads or an action sheet for iPhones. I wouldn’t worry about this right now though as it is purely for viewing capabilities.
Next we have the fields of the class, all read only as the are for gathering information after creation, not modifying them. After those, we have our initializers. Obviously the easiest way to initialize a picker for numbers is with a number so we have 2 init methods, one that takes just the number of the max (currently this just sets it to the highest digit and as many 9s as necessary to allow for numbers leading up to there, so for 5000 it would be 5999) and another that takes the max number as well as a list of labels. The labels are useful for different situations like if you wanted to do file sizes (kb, mb, gb, etc.) or a # of animals or whatever.
Finally we have our public messages. Now, some of these are merely exposed so that I could create test cases, I suppose I could leave them as undefined in the header and use @selectors to execute them in my tests so perhaps I’ll do that when I refactor. Ideally the 4 messages that should be exposed are the last 4, getValue, getSelectedLabel, and the 2 setPickerToCurrentValue messages. We will take a look at all of these things now.
#pragma mark - initializers
- (id) init:(NSNumber *)maxValue {
self = [self init];
decimalColumn = -1;
NSArray *digits = [self split:maxValue];
_columnValues = [[NSMutableArray alloc] init];
for (int i = 0; i <; [digits count]; i++) {
int max = 9;
int colVal = [(NSNumber *)[digits objectAtIndex:i] intValue];
if (i == 0) {
max = colVal;
}
NSMutableArray *column = [[NSMutableArray alloc] init];
for (int j = 0; j <;= max; j++) {
[column addObject:[NSNumber numberWithInt:j]];
}
[_columnValues addObject:column];
}
return self;
}
- (NSArray *) split:(NSNumber *)val {
_maxValue = val;
if (val && [val intValue] >;= 0) {
NSString *stringVal = [self getNumberString:val];
return [self splitStringToArray:stringVal];
}
return nil;
}
- (NSArray *) splitStringToArray:(NSString *)stringVal {
NSMutableArray *splitVal = [[NSMutableArray alloc] init];
for (int i = 0; i <; [stringVal length]; i++) {
unichar num = [stringVal characterAtIndex:i];
NSString *numStr = [NSString stringWithFormat:@"%c", num];
NSLog(@"Parsed digit %@", numStr);
if ([numStr isEqualToString:@"."]) {
if (decimalColumn == -1) {
decimalColumn = i;
}
num = [stringVal characterAtIndex:++i];
numStr = [NSString stringWithFormat:@"%c", num];
}
[splitVal addObject:[NSNumber numberWithInt:[numStr intValue]]];
}
return splitVal;
}
- (NSString *) getNumberString:(NSNumber *)val {
NSString *stringVal = [val stringValue];
if ([stringVal hasPrefix:@"0."]) {
stringVal = [stringVal stringByReplacingOccurrencesOfString:@"0." withString:@"."];
}
return stringVal;
}
- (id) initWithLabels:(NSNumber *)maxValue labels:(NSArray *)labels {
self = [self init:maxValue];
_labels = labels;
[_columnValues addObject:labels];
return self;
}
Here we have our initializers, nothing much interesting about the one with labels but the really interesting pieces here lie within the init with the max number so let’s take a moment to step through what’s going on here. So first we have:
decimalColumn = -1;
NSArray *digits = [self split:maxValue];
First we have the decimalColumn reset to -1 to make sure there is a value as well as it being invalid. Next we split the max number provided into an appropriate number of columns (i.e 123 becomes [1,2,3])
We set the decimalColumn to -1 to make sure that it has been reset and then we split the provided number into an array of values instead (i.e. 123 becomes ['1','2','3]). We do this by looping converting the number to a string and stripping out a leading 0 if the number provided was a float <; 0 and then taking that string and looping through each character and putting it’s numeric value into the return array. If we encounter a decimal point we skip over it and note where we found it in the decimalColumn variable. After we have the split number value we then want to set up the values that each column will provide the user to pick from. This is done by first indicating what the maximum value the first column should be and for each expected column to enter a value from 0 – max (9 in most cases). Additionally, note that we have the capabilities in there to provide for adding commas to necessary positions as well as the decimal point in the correct column. Next we have the functional portions for getting our selections:
#pragma mark - instance methods
- (NSNumber *) getValue {
NSString *pickerNum = [self getPickerNumberAsString];
return [NSNumber numberWithFloat:[pickerNum floatValue]];
}
- (NSString *) getPickerNumberAsString {
NSMutableString *val = [NSMutableString stringWithString:@""];
int columns = [self getNumberColumnCount];
for (int i = 0; i <; columns; i++) {
if (i == decimalColumn) {
[val appendFormat:@".%d", [self.pickerView selectedRowInComponent:i]];
} else {
[val appendFormat:@"%d", [self.pickerView selectedRowInComponent:i]];
}
}
return val;
}
- (NSString *) getSelectedLabel {
NSString *labelVal = @"";
if (_labels != nil) {
labelVal = [_labels objectAtIndex:[self.pickerView
selectedRowInComponent:
([_columnValues count] - 1)]];
}
return labelVal;
}
- (NSInteger) getNumberColumnCount {
int columns = [_columnValues count];
if (_labels != nil) {
columns--;
}
return columns;
}
Here we have our simple methods for actually getting our selected values. Our getValue method just loops through our columns and appends each digit to a string and returns an NSNumber object that was created from the string value’s floatValue method. Nothing much special going on here. Let me know if anything is confusing here.
Finally we have the following set current value methods.
- (void) setPickerToCurrentValue:(NSNumber *)val {
NSArray *splitVal = [self splitAndPad:val];
int diff = [self getNumberColumnCount] - [splitVal count];
NSLog(@"Setting %d column out of a possible %d, difference of %d",
[splitVal count], [self getNumberColumnCount], diff);
for (int i = 0; i <; [self getNumberColumnCount]; i++) {
if (i <; diff) {
[self.pickerView selectRow:0 inComponent:i animated:NO];
NSLog(@"row selection 0");
} else {
int row = [(NSNumber *)[splitVal objectAtIndex:(i-diff)] intValue];
NSLog(@"row selection %d", row);
[self.pickerView selectRow:row inComponent:i animated:NO];
}
}
}
- (NSArray *) splitAndPad:(NSNumber *)val {
NSMutableArray *splitVal = [[self split:val] mutableCopy];
int countNumberCols = [self getNumberColumnCount];
if ([splitVal count] <; countNumberCols) {
int numFloatPrecision = 0;
int stringLength = countNumberCols;
if (decimalColumn >;= 0) {
stringLength++;
numFloatPrecision = countNumberCols - decimalColumn;
}
NSString *formatted = [NSString stringWithFormat:@"%0*.*f",
stringLength, numFloatPrecision,
[val floatValue]];
NSLog(@"Front: %d Back: %d", stringLength, numFloatPrecision);
NSLog(@"Did we format correctly? %@", formatted);
return [self splitStringToArray:formatted];
}
return splitVal;
}
- (void) setPickerToCurrentValue:(NSNumber *)val label:(NSString *)label {
[self setPickerToCurrentValue:val];
for (int i = 0; i <; [_labels count]; i++) {
NSString *labelVal = [_labels objectAtIndex:i];
if ([labelVal isEqualToString:label]) {
[self.pickerView selectRow:i
inComponent:([_columnValues count] - 1)
animated:NO];
}
}
}
Now, here we have the most interesting bit that needed to be considered in developing this, basically when I have, say 5 columns but the selected value is only 3 digits, how do I compensate for those missing digits? Even more so, how can I handle floats if I have 2 float digits and I pass in 2 digits, or 1.1 when there is NN.NN total format? This was an interesting problem to solve (and it’s not quite there yet since I don’t do floating point rounds so 1.234 can properly set NN.NN) but I think the solution that was added is pretty nifty.
The setPickerToCurrentValue:(NSNumber*) interface seems to have remnants of my previous attempt so I will have to clean those up but the main item to look at is the splitAndPad method. We start this by determining the length of the input number string as well as the length of the floating points. We can then use those values to utilize the string formatting to pad the end of the string with as many 0s are necessary as well as padding the front of the string so that we will end up with a numeric string with the exact length of maximum length. We can then split this string up into the column values we will set and we use that array to set our column values.
I’ve left out other methods, such as those of the picker delegate, but I feel like if you want to take this implementation and use it before I get my repo set up you can easily implement those for yourself. I hope that this will be helpful for some folks and I will do my best to get that repository up with a valid implementation. I look forward to others helping make this even better for the future.
Like this:
Like Loading...