Two Practical Uses of Enum in Swift

Swift enums with associated types and a generic enum pattern

Tom Zurkan
Better Programming

--

I was recently tasked with writing the Optimizely Swift SDK. The timing seemed right, what with the introduction of Swift 5 and support for embedded frameworks in Swift.

For a little background, the Optimizely Swift SDK is used to run A/B testing as well as feature management on your iOS or tvOS device.

After completing the Swift SDK (we are in GA now), I looked back on some of the patterns that emerged. Some came out early, like enum usage, while others came out of needs during development. This has turned into a four-part series in which I’ll discuss the design patterns that were most useful. In particular, we will discuss Swift enums with associated types and the result generic enum pattern, which was adopted in Swift 5. Then, we’ll discuss the generic class AtomicProperty that turned out to be highly useful. Next, we discuss dependency injection. Finally, we’ll cover how to expose Objective-C APIs in your Swift code without polluting your Swift design, implementation, and code.

Let’s jump right into it!

The first challenge when developing this SDK was consuming a config JSON file. In order to do this, I used an online JSON to Swift data objects converter to create my initial data objects. This handled things pretty well but required quite a bit of additional work when the JSON field could be multiple types. I ended up with something like this:

struct UserAttribute: Codable, Equatable { // MARK: — JSON parse var name: String? var type: String? var match: String? var value: Any?}

This is fine and doable. It just means you need to add custom encode and decode methods. It also means you have to figure out the type and cast every time you want to use it. A cleaner and more reusable solution is to use a Swift enum with associated values. This looks more like this:

struct UserAttribute: Codable, Equatable { // MARK: — JSON parse var name: String? var type: String? var match: String? var value: AttributeValue?}

Now you can use the CodingKey to define your json names and let the AttributeValue do the heavy lifting. Think of the Swift enum associated values as a c union, where it can be cast to any number of types. We use the case statement to associate a name and type for each value. We also implement Codable which is type alias for both Encodable and Decodable protocols for json encode/decoding. Below is a code example — a pretty straightforward declaration of an attribute value that supports multiple types (this could be a generic attribute of another data struct):

enum AttributeValue: Codable { case string(String) case int(Int64) // supported value range [-2⁵³, 2⁵³] case double(Double) case bool(Bool) case others // here we get the container and try and decode the value as       different types and setting // self to the type we decoded. init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() if let value = try? container.decode(String.self) {  self = .string(value)  return } if let value = try? container.decode(Double.self) {  self = .double(value)  return } if let value = try? container.decode(Int64.self) {  self = .int(value)  return } if let value = try? container.decode(Bool.self) {  self = .bool(value)  return } // accept all other types (null, {}, []) for forward compatibility support self = .others}// here we are encoding the values by switching on self.func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() switch self {  case .string(let value):   try container.encode(value)  case .double(let value):   try container.encode(value)  case .int(let value):   try container.encode(value)  case .bool(let value):  try container.encode(value)  case .others:  return  } }}

Here is a sample usage:

func testEncodeDecodeString() { let attributeValue = AttributeValue.string(“json”) let jsonData = try! JSONSerialization.data(withJSONObject: [“json”], options: []) let attributes = try! JSONDecoder().decode([AttributeValue].self, from: jsonData) switch attributes[0] {  case .string(let str):   XCTAssert(str == “json”)   XCTAssert(attributes[0] == attributeValue)  default:   XCTAssert(false)  }}

As you can see, using enums is definitely a powerful tool in Swift for json encoding and decoding proper values. By pulling the code out of your container object, you can reuse the enum anywhere you need to support multiple properties. We could have even added some additional validation methods or hooks for specific validation. I found this encapsulation method much cleaner than supporting Any? on my container object and constantly having to cast it. It also helped make my container encoding and decoding clean.

Another great pattern adopted in Swift 5 is the result enum. The result enum holds the return type or an error and uses generics for you to determine what the error and return values might be. It makes it easy to switch and process a result.

enum Result<T, Error> { case success(T) case failure(error)}

This can be used in defining a function signature as follows:

public enum OptimizelyError: Error { case generic}public typealias DatafileDownloadCompletionHandler = (Result<Data?, OptimizelyError>) -> Voidfunc downloadDatafile(sdkKey: String,completionHandler:@escaping DatafileDownloadCompletionHandler) { let task = session.downloadTask(with: request) { (url, response, error) in var result = Result<Data?,OptimizelyError>.failure(.generic(“Failed to parse”)) if error != nil { self.logger.e(error.debugDescription) result = .failure(.generic(error.debugDescription)) } else if let response = response as? HTTPURLResponse { if response.statusCode == 200 { let data = self.getResponseData(sdkKey: sdkKey, response: response,   url: url) result = .success(data) } else if response.statusCode == 304 {  self.logger.d(“The datafile was not modified and won’t be downloaded again”)  result = .success(nil) }  completionHandler(result) }task.resume}

This can be consumed as follows:

downloadDatafile(sdkKey: sdkKey) { (result) in switch result {  case .success(let data):
datafile = data
case .failure(let error):
self.logger.e(error.reason)
}
}

Before Swift 5, we wrote our own result, as did most Swift developers. As a matter of fact, we still use our OptimizelyResult. Result enum is a welcome addition and a tip of the hat to a cool pattern that was already widely adopted. The resulting pattern and the enum associated values for encoding and decoding are just a couple of examples of the power of enumerations in Swift. Using them as described here is a great way to easily integrate your json objects and process results.

I hope this was useful. Next time we will discuss atomic properties in Swift.

--

--

Engineer working on full stack. His interests lie in modern languages and how best to develop elegant crash proof code.