views:

500

answers:

4

Hi

I am implementing a UISlider a user can manipulate to set a distance. I have never used the CocoaTouch UISlider, but have used other frameworks sliders, usually there is a variable for setting the "step" and other "helper" properties.

The documentation for the UISlider deals only with a max and min value, and the output is always a 6 decimal float with a linear relation to the position of the "slider nob". I guess I will have to implement the desired functionality step by step.

To the user, the min/max values range from 10 m to 999 Km, I am trying to implement this in an exponential way, that will feel natural to the user. I.e. the user experiences a feeling of control over the values, big or small. Also that the "output" has reasonable values. Values like 10m 200m 2.5km 150 km etc. instead of 1.2342356 m or 108.93837756 km.

I would like for the step size to increase by 10m for the first 200m, then maybe by 50m up to 500m, then when passing the 1000 m value, it starts to deal with Kilometers, so then it is step size = 1 km up until 50 km, then maybe 25 km steps etc.

Any way I go about this I end up doing a lot of rounding and a lot of calculations wrapped in a forrest of if statements and NSString/Number conversions, each time the user moves the slider just a little.

I was hoping someone could lend me a bit of inspiration/math help or make me aware of a more lean approach to solving this problem.

My last idea is to populate and array with a 100 string values, then have the slider int value correspond to a string, this is not very flexible, but doable.

Thank you in advance for any help given:)

A: 

The easiest answer is to use a Segmented Control with the different 'step sizes'. Depending on what option the user selects, is what step size your slider will have. I'd even recommend that this is perhaps a more user friendly way to approach it :).

Feel free to use Dapp to play with the look of your app and see how a segmented control might fit with the design.

However... you wanted a leaner approach ;).

I started writing the 10 or so steps needed, but I stopped when I realised you had probably already come to a similar solution. Your string array idea is fine, I assume you will simply convert the slider value to an integer and grab the relevant index from the array?

Sometimes as programmers we go too far with our approaches to a problem. Yes, a string array isn't the most 'flexible' solution but it is fast! And, I'd argue that even a genius mathematical solution isn't as flexible as you may think. Also, if you don't plan on changing the values anytime soon and don't need different slider ranges on different sliders, then it makes sense to just create a static array.

Good luck! :)

Raal
Hi Raal and thanks."Sometimes as programmers we go too far with our approaches to a problem" perfectly sums up the last couple of hours:)I took me 25 minutes to put together the solution that I described in my earlier comment with the array. It works perfectly, I only deal with 75 values so the lookup time/overhead is almost non existing.I made my slider go from 0-74 and I round/cast the numbers to ints and just look up the corresponding value. I chose a value of index 21 for when it changed from meters to kilometers.I'll just post the code in an answer if anyone could benefit from it.
RickiG
A: 

A quick scaling solution involves three special methods:

- (CGFloat)scaleValue:(CGFloat)value {
    return pow(value, 10);
}

- (CGFloat)unscaleValue:(CGFloat)value {
    return pow(value, 1.0 / 10.0);
}

- (CGFloat)roundValue:(CGFloat)value {
    if(value <=   200) return floor(value /   10) * 10;
    if(value <=   500) return floor(value /   50) * 50;
    if(value <=  1000) return floor(value /  100) * 100;
    if(value <= 50000) return floor(value / 1000) * 1000;
    return floor(value / 25000) * 25000;
}

Reacting to the slider changes would be something like:

- (void)sliderChange:(id)sender {
    CGFloat value = mySlider.value;
    value = [self scaleValue:value];
    value = [self roundValue:value];
    valueLabel.text = [NSString stringWithFormat:@"%f", value];
}

You can init with the following code:

mySlider.maximumValue = [self unscaleValue:999000];
mySlider.minimumValue = [self unscaleValue:10];

I got all of the above to work without any problems but it could use some bulletproofing. The scaleValue: and unscaleValue: methods should check for unsupported values and I am sure sliderChange: method could be more efficient.

In terms of speed, I am sure that this is faster than pulling objects out of an array. The actual slider behavior may feel a little wonky and there isn't much control over exactly what values are available with the slider, so it may not do exactly what you want. Using really high powers seemed to make it more useful.

MrHen
Thank you MrHen!That is a quite lean approach for truly exponential values.When I get to optimizing I'll try out how it compares agains the array lookup.Thanks again.
RickiG
A: 
+ (NSArray*) getSliderNumbers {

    NSArray *sliderNumbers = [NSArray arrayWithObjects:@"10",
                          @"20",
                          @"30",
                          @"40",
                          @"50",
                          @"60",
                          @"70",
                          @"80",
                          @"90",
                          @"100",
                          @"150",
                          @"200",
                          @"250",
                          @"300",
                          @"350",
                          @"400",
                          @"450",
                          @"500",
                          @"600",
                          @"700",
                          @"800",
                          @"900",
                          @"1",
                          @"1.5",
                          @"2.0",
                          @"2.5",
                          @"3.0",
                          @"3.5",
                          @"4",
                          @"4.5",
                          @"5",
                          @"5.5",
                          @"6",
                          @"6.5",
                          @"7",
                          @"7.5",
                          @"8",
                          @"8.5",
                          @"9",
                          @"9.5",
                          @"10",
                          @"15",
                          @"20",
                          @"25",
                          @"30",
                          @"35",
                          @"40",
                          @"45",
                          @"50",
                          @"55",
                          @"60",
                          @"65",
                          @"70",
                          @"75",
                          @"80",
                          @"85",
                          @"90",
                          @"95",
                          @"100",
                          @"200",
                          @"300",
                          @"400",
                          @"500",
                          @"600",
                          @"700",
                          @"800",
                          @"900",
                          nil];
    return sliderNumbers;

}

above is loaded into an array upon instantiation:

Set up the slider:

    customSlider.minimumValue = 0.0f;
    customSlider.maximumValue = (CGFloat)[sliderNumbers count] - 1;
    customSlider.continuous = YES;
    customSlider.value = customSlider.maximumValue;

The method called on UIControlEventValueChanged

- (void) sliderMove:(UISlider*) theSlider {

    NSInteger numberLookup = lroundf([theSlider value]);

    NSString *distanceString = [sliderNumbers objectAtIndex:numberLookup];
    CGFloat distanceInMeters;

    if (numberLookup > 21) {

        [self.indicator.indicatorLabel setText:[NSString stringWithFormat:@"%@ km", distanceString]];       
        distanceInMeters = [distanceString floatValue] * 1000;
    } else {

        [self.indicator.indicatorLabel setText:[NSString stringWithFormat:@"%@ m", distanceString]];
        distanceInMeters = [distanceString floatValue];
    }

    if (oldDistanceInMeters != distanceInMeters) {

        [self.delegate distanceSliderChanged:distanceInMeters];
        oldDistanceInMeters = distanceInMeters;
    }
}

This even takes care of formatting the string for the user interface e.g. "200 m" or "1.5 km" and updates the delegate with the distance number in meters for using when sorting my results with a predicate.

RickiG
A: 

In order to do this right, you want your slider to represent exponents. So for instance if you want a scale of 10-100000, you'd want your slider to have a range of 1(10 = 10^1) through 5 (100,000 = 10^5). You can set the stepping on the slider to a fraction in order to give you the precision you want. For my example, I'll be using a min of 20,000 and a max of 20,000,000 for output (because that's what I needed when I sat down to figure this out). Since I am basically going to increase by 1000 times from min to max I want 10^3, so my slider is going to be 0-3 (10^0 evaluates to 1). I'll use .001 stepping so there are a total of 3000 possible positions. Here's the code you'll need to accomplish this:

  $("#amount-slider").slider({
   value:20000,
   min: 0,
   max: 3,
   step:.001,
   slide: function(event, ui) {
    $("#amount").val(Math.pow(10, ui.value)*20000);
   }
  });

Suppose you want the inverse of this function, so you can set the slider position given some external input. Then you want to use logarithm:

  var amtVal = parseFloat($("#amount").val());

  $('#amount-slider').slider('value', ((Math.log(amtVal/20000)/Math.log(10))));

You'll need to adjust the 20000 in both functions to match your base amount. If your max must be derived by exponents of something other than 10, change 10 as well as the max (exponent). Life will be easier if you go up by powers of ten.