Compile-time checked switch statements in Objective-C

Thanks to its design Swift can catch more issues at compile-time than Objective-C. Some similar checks can be performed in Objective-C too, although they're not well-known and sometimes tricky. In this post I'll describe how to get as much help as possible from the compiler when it comes to switch statements.

Let's say we're trying to introduce a very rudimentary theming1 functionality to our app. At the moment we have only two themes, denoted by an enum:

typedef NS_ENUM(NSUInteger, AHKTheme) {
  AHKThemeDay,
  AHKThemeNight,
};

We want to have a different text color depending on the current theme. Let's create a method called +textColorForTheme: that will do just that:

+ (UIColor *)textColorForTheme:(AHKTheme)theme
{
  UIColor *color;

  switch (theme) {
    case AHKThemeDay:
      color = [UIColor blackColor];
      break;

    case AHKThemeNight:
      color = [UIColor whiteColor];
      break;

    default:
      break;
  }

  return color;
}

In here, the default case doesn't do anything. I left it because that's how Xcode suggests writing it, i.e. that's how it's written in its Snippets Library. Other tools, such as SCXcodeSwitchExpander plugin, add a default case by the default too. The code looks OK and works fine for now. It's not future-proof, though.

A couple of weeks pass and we get the feedback from the users of our app saying that sepia theme would make a really useful new feature. We proceed by adding a third value to AHKTheme:

typedef NS_ENUM(NSUInteger, AHKTheme) {
  AHKThemeDay,
  AHKThemeNight,
  AHKThemeSepia,
};

If we pass AHKThemeSepia to textColorForTheme: we'll get a nil in return. Looking for wrong colors through the whole app can quickly become tiresome, so we'll just update our switch statement by adding NSAssert(NO, @"This code path shouldn't be reached"); after default:. If we run the app and reach the path where our method is called, the execution will stop due to a failed assertion.

We can do better than that, by moving the check to compile-time. We simply remove the default case altogether. Here's how our method looks after this quick change:

+ (UIColor *)textColorForTheme:(AHKTheme)theme
{
  UIColor *color;

  switch (theme) {
    case AHKThemeDay:
      color = [UIColor blackColor];
      break;

    case AHKThemeNight:
      color = [UIColor whiteColor];
      break;
  }

  return color;
}

When we perform the build, we get a nice compile-time warning:

Enumeration value 'AHKThemeSepia' not handled in switch.

At this moment it would be best to enable Treat Warnings as Errors in Build Settings in our Xcode project. It's not always possible – especially in legacy projects – so we can do a small refactoring to get a compile-time error anyway. We just remove color variable and return the value straight from each case:

+ (UIColor *)textColorForTheme:(AHKTheme)theme
{
  switch (theme) {
    case AHKThemeDay:
      return [UIColor blackColor];

    case AHKThemeNight:
      return [UIColor whiteColor];
  }
}

If we build the project now, we'll get a nice error:

Control may reach end of non-void function

We add a new case for our sepia theme and call it a day:

case AHKThemeSepia:
  return [UIColor lightGrayColor];

There's one more thing that still could be improved. Even though the compiler doesn't give any indication of it, it's possible that textColorForTheme: will return nil. Since AHKTheme uses NSUInteger to store its value, we can just pass any number that fits in NSUInteger to textColorForTheme:. To help with that, we can enable Out-of-Range Enum Assignments in our build settings. It'll help with simple cases. For example compiling:

  UIColor *textColor = [[self class] textColorForTheme:3];

will produce a warning:

Integer constant not in range of enumerated type 'AHKTheme' (aka 'enum AHKTheme')

It's easy to cheat the compiler, though, by simply assigning a number to a variable first:

NSUInteger fakeTheme = 3;
UIColor *textColor = [[self class] textColorForTheme:fakeTheme];

As software-engineers we know that Murphy's law is as true as ever, so we'll be best off by adding an assertion at the end of textColorForTheme:, leading to a final version:

+ (UIColor *)textColorForTheme:(AHKTheme)theme
{
  switch (theme) {
    case AHKThemeDay:
      return [UIColor blackColor];

    case AHKThemeNight:
      return [UIColor whiteColor];

    case AHKThemeSepia:
      return [UIColor lightGrayColor];
  }

  NSAssert(NO, @"This code path shouldn't be reached");
}

And that's how I write my switch statements.


  1. A proper solution would probably have all colors for a given theme kept in a single class. There'll still be a similar switch statement in at least one place, though.