Stupid Swift Tricks #5

Pickable Enums

Here’s a very common thing in iOS apps: You have a table view, with a list of items, each one a simple line of text, and a checkmark next to the one that’s selected.

Sure, there are often better ways of doing this — particularly for UI that’s at the core of your app — but still. It’s familiar to users, and for things like settings, it’s very useful. Indeed, the iOS system settings are full of ‘em. To take one example to use in this post, in the Messages section, when you tap “Keep Messages”, you can pick “30 Days”, “1 Year” or “Forever”.

Sometimes the choices are dynamic (e.g. picking the default calendar, where the choices depend on which calendars the user has created), but a lot of the time, including the Keep Messages example, there’s a fixed list, and in code, these settings are probably represented by an enum:

Swift

Hypothetical Enum
enum KeepMessagesOptions: Int { case For30Days, For1Year, Forever }

By basing the enum on an Int, each item has an index associated with it (0 for For30Days, 1 for For1Year, 2 for Forever), so you can translate from NSIndexPath.row to enum and back again.

But, for storage in the settings, you need something else as well: a permanent identity, separate from the index. Why? Well, let’s say in the next update you decide to add more granularity:

Swift

Hypothetical iOS 10 Enum
enum KeepMessagesOptions: Int { case For30Days, For6Months, For1Year, Forever }

If an iOS 9 user chose to keep their messages forever, and you used the index (2) to store their setting, now that value matches For1Year and they might lose a bunch of messages they wanted to keep. That’s no good.

Of course, you can’t just use the names shown on screen either, because those are a) translated to different languages and b) could have typos that later get fixed or something like that. In general, you never want to be using user-facing strings for settings storage. So you need some specific machine-readable IDs to use instead.

This is all pretty obvious. Why am I belabouring it? Because it’s kind of annoying to have to keep handling this stuff.

In Objective-C, as in C or C++, enums are just a slightly prettier way of defining a constant integer. So, to provide a UI to the user to pick an option, you need to define two lists of strings to represent them (one for machine-readable IDs, one for localised human-readable display names), make a table view controller to present the choices, pass in the appropriate human-readable strings each time you use it, define some kind of API for it to hand the result back, and you have to handle all the translations between index, machine-readable ID and human-readable title whenever you read/write the settings.

It’s not difficult. But it’s tedious.

A Better Way

In Swift however, an enum can do a whole lot more. Let’s define a quick protocol:

Swift

PickableEnum Protocol
protocol PickableEnum { var displayName: String { get } var permanentID: String { get } static var allValues: [Self] { get } static func fromPermanentID(id: String) -> Self? }

And it becomes pretty trivial to make a generic view controller that can pick from any PickableEnum. Add in Futures and you can use it as simply as this:

Swift

Example Method in a View Controller
@IBAction func showChoicesForKeepMessage() { // assume we already got the current value from somewhere EnumPickerController.pickFromEnum(currentValue, from: self).onSuccess { newValue in // do something with newValue, probably involving writing // newValue.permanentID to NSUserDefaults somewhere. } }

That looks pretty nice, but what about the enums themselves — have we just pushed all the extra work into them, when they implement the PickableEnum protocol? Nah — protocol extensions to the rescue:

Swift

PickableEnum Extension
extension PickableEnum where Self: RawRepresentable, Self.RawValue == Int { var displayName: String { return Localised("\(self.dynamicType).\(self)") } var permanentID: String { return String(self) } static var allValues: [Self] { var result: [Self] = [] var value = 0 while let item = Self(rawValue: value) { result.append(item) value += 1 } return result } static func fromPermanentID(id: String) -> Self? { return allValues.indexOf { $0.permanentID == id }.flatMap { self.init(rawValue: $0) } } }

Write that once, and now we have sensible default implementations for all of the protocol requirements. So all we have to do is add the conformance to our enum and we’re ready to go:

Swift

Conformant Enum
enum KeepMessagesOptions: Int, PickableEnum { case For30Days, For6Months, For1Year, Forever }

Of course, you can use your own implementations; for example, if you needed backward-compatibility with existing settings IDs already in NSUserDefaults:

Swift

Enum with Custom IDs
enum KeepMessagesOptions: Int, PickableEnum { case For30Days, For6Months, For1Year, Forever var permanentID: String { return ["30d", "6m", "1y", "forever"][rawValue] } }

But you don’t have to.

And for human-readable strings, just add them to your app’s localised strings file, using the enum type & cases as keys, which you’d need to do anyway:

Localized.strings
"KeepMessagesOptions.For30Days" = "For 30 Days"; // etc

(I’ve assumed, above, that Localised() is a function that uses the standard cocoa localisation routines to look up the appropriate text in the main bundle. Why not NSLocalizedString()? That’s intended for static strings only, and will probably give you trouble if you run genstrings or something like that on a constructed string like we have here.)

Alternatives and Implementation Notes

Another possibility is that you could use enums backed by String instead of Int. This has the advantage that you can use the default init(rawValue:) and rawValue members to convert back and forth between ID and case, and the protocol boils down to just allValues and displayName, if you extend it from the standard library RawRepresentable protocol:

Swift

Alternate PickableEnum Protocol
protocol PickableEnum: RawRepresentable { var displayName: String { get } static var allValues: [Self] { get } }

Simpler in theory, but in practice, you still have to index into the list of enum cases to translate to/from table view rows. And you need to define the allValues array by hand for each one. If you find you’re always doing that anyway, using a string-backed enum might be a better bet. Or, in future, Swift might gain a way to access all the possible cases of an enum automagically (at least for enums without associated data) even for those backed by non-integer types. The compiler has that information, after all.footnote 1

But in my case, I also stick with integer-backed enums for another reason: because it’s easier to handle them when I need to use the values inside Ferrite‘s audio engine (written in Objective-C++, as mentioned previously).

Another minor implementation note: I’ve given the simplest definition above, but in practice I found that it can help if allValues, instead of returning an array of the actual instances, returns an array of tuples, e.g. [(name: String, id: String)]. That’s the data the view controller actually needs, and it reduces the sometimes-“contagious” effect of generics since it only ever needs to deal with known types. That may or may not be valuable to you.

Oh, and here’s another thing… there’s actually nothing saying the type must be an enum. Remember back at the beginning I said that sometimes the list is based on user-supplied data, not a fixed set? You can make your own struct that conforms to PickableEnum and your view controller (or any other widgets you’ve adapted to use it… UIPickerView perhaps?) will accept it just fine, and with a bit of luck, there’s another repetitive chunk of code you never have to write again.


1. And likes to remind you of that fact when you write switch statements ;)