3
\$\begingroup\$

I'm working on a battery indicator feature in my app, and I was wondering if there is a better way than what I am doing to check what image should be displayed when. I am currently using if statements, but I was wondering if case statements would be a better option.

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    UIDevice *device = [UIDevice currentDevice];

    //Observer for the battery level
    //Updates the percentage label and the battery icon
    if (KEY_PATH_IF_THEN(@"batteryLevel")) {
        LabelBatteryStatus.text = [NSString stringWithFormat:@"%.0f%%", device.batteryLevel * 100];

        //shows battery image
        if (kBatteryLevelBetween(100, 100)) {
            batteryImage.image = [UIImage imageNamed:@"battery100fullycharged.png"];

        } else if (kBatteryLevelBetween(99, 81)) {
            batteryImage.image = [UIImage imageNamed:@"battery90.png"];

        } else if (kBatteryLevelBetween(80, 65)) {
            batteryImage.image = [UIImage imageNamed:@"battery75.png"];

        } else if (kBatteryLevelBetween(64, 41)) {
            batteryImage.image = [UIImage imageNamed:@"battery50.png"];

        } else if (kBatteryLevelBetween(40, 35)) {
            batteryImage.image = [UIImage imageNamed:@"battery30.png"];

        } else if (kBatteryLevelBetween(34, 15)) {
            batteryImage.image = [UIImage imageNamed:@"battery20.png"];

        } else if (kBatteryLevelBetween(14, 0)) {
            batteryImage.image = [UIImage imageNamed:@"battery10.png"];

        }
    }
}

kBatteryLevelBetween:

#define kBatteryLevelBetween(higherNum,lowerNum)    (device.batteryLevel *100 <= higherNum && device.batteryLevel *100 >= lowerNum)
\$\endgroup\$
3
\$\begingroup\$

I know you have already accepted an answer to this, but in general, I would recommend a completely different strategy. Instead of picking from many different images, drawn and named to match the battery level, pick a battery background (fuel, or fill level) image, that you can turn into a stretchable image (sometimes called a 9-patch image). UIImage fully supports this concept.

I actually built an app where I did exactly this. I built a class that completely encapsulates the battery display. In order to use it, you simply tell it the current battery level (e.g. 0 to 100) and it stretches the background image accordingly, to yield a perfectly continuous (not discrete) battery meter.

Also, if you fundamentally want a few different images, for example, to support changing the color when the battery gets too low, then this implementation supports that, too. My meter shows red fuel when the level gets below a certain threshold. It also allows the battery meter to animate smoothly, when you change its value.

Anyway, you can take a look at the project here on my blog. Sample code is provided, too.

\$\endgroup\$
7
\$\begingroup\$

I think this code is easier to read and should result in significantly less comparisons, as it just compares in every step the lower bound, where as your define macro checks twice each. And note that you are checking also equality. this should result into

  1. at least 2 checks at cpu level and
  2. checking equality for floats in nver a good idea, as due to rounding errors they tend to be not equal, even if in pure math they should be (a 1 might be save as 0.99 or 1.001)
  3. and if you would check integer numbers for being in some range it make sense, to have an equal check just on one bound like if (a <= higherNum && b > lowerNum)

my code suggestion

-(NSUInteger)batteryLevel
{
    UIDevice *device = [UIDevice currentDevice];
    float level = device.batteryLevel;
    if(level > .99) return 100;
    if(level > .80) return 90;
    if(level > .64) return 75;
    if(level > .40) return 50;
    if(level > .34) return 30;
    if(level > .14) return 20;
    return 10;
}


-(UIImage *)imageForBatteryLevel
{
    NSUInteger level = [self batteryLevel];
    NSString *imageName;
    if(level > 99){
        imageName = @"battery100fullycharged.png";
    } else {
        imageName = [NSString stringWithFormat:@"battery%d.png", level];
    }
    return [UIImage imageNamed:imageName];
}

- (void)observeValueForKeyPath:(NSString *)keyPath 
                      ofObject:(id)object 
                        change:(NSDictionary *)change 
                       context:(void *)context 
{
    if (KEY_PATH_IF_THEN(@"batteryLevel")) {
        LabelBatteryStatus.text = [NSString stringWithFormat:@"%.0f%%", device.batteryLevel * 100];
        batteryImage.image = [self imageForBatteryLevel];
    }
}

I was taught at university, that in assembler code checking equality is actually done by checking, that the value is neither smaller nor greater, this makes it 2 comparisons. I am not sure, that this is true for every CPU, but let's walk through an example with that.

Let's assume, the level is down to 2%. with your code, it will go liek this:

  1. is .02 * 100 bigger or equal than 100 and
  2. is .02 * 100 smaller or equal than 100 -> No
  3. is .02 * 100 smaller or equal than 99 and
  4. is .02 * 100 bigger or equal than 81 -> No
  5. is .02 * 100 smaller or equal than 80 and
  6. is .02 * 100 bigger or equal than 65 -> No
  7. is .02 * 100 smaller or equal than 64 and
  8. is .02 * 100 bigger or equal than 41 -> No
  9. is .02 * 100 smaller or equal than 40 and
  10. is .02 * 100 bigger or equal than 35 -> No
  11. is .02 * 100 smaller or equal than 34 and
  12. is .02 * 100 bigger or equal than 15 -> No
  13. is .02 * 100 smaller or equal than 14 -> Yes

As you see you have 13 comparisons for smaller or bigger and 13 for equal. that yields in 39 comparisons. Additionally you have 13 times the same floating number multiplication (.02 * 100). And a gap between the boundaries: If the percentage would result in 14.5, no rule would be triggered.

Let's use my rules

  1. is .02 > .99 -> No
  2. is .02 > .80 -> No
  3. is .02 > .64 -> No
  4. is .02 > .40 -> No
  5. is .02 > .34 -> No
  6. is .02 > .14 -> No

=> return 10

As you see, I have maximum 6 comparisons and no floating point multiplication.

\$\endgroup\$
1
\$\begingroup\$

When I see a bunch of repetitive if statements of this kind, I often think "this belongs in some kind of tabular format". Even if that table is something simple like a struct array definition.

Why not something like this? (Warning, my objc is a bit rusty; my C is much fresher.)

struct BatteryMapping
{
   int minValue;
   NSString *imagePath;
};

static const struct BatteryMapping Mappings[] =
{
   {100, @"battery100fullycharged.png"},
   {81,  @"battery90.png"},
   {65,  @"battery75.png"},
   {41,  @"battery50.png"},
   {35,  @"battery30.png"},
   {15,  @"battery20.png"},
   {0,   @"battery10.png"},
   {0,   nil}  // terminating record.  shouldn't hit this but just in case...
};

const struct BatteryMapping *p = Mappings;
NSString *imagePath = nil;

int percent = device.batteryLevel * 100;

while (!imagePath && p->imagePath)
{
   if (percent >= p->minValue)
      imagePath = p->imagePath;
   else
      ++p;
}

if (imagePath)
   batteryImage.image = [UIImage imageNamed:imagePath];
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.