Swift Enums: Initialization with Associated Values from a Server Response

Originally posted 1/10/2016. Updated for Swift 3 on 12/18/16

Goal: Initialize a Swift enum with associated values from a JSON response. We'll write a "Drawing" type that could be used to display lines, circles, rectangles, and freeform drawings.

Swift Enumerations with Associated Values are a powerful way to represent a group of related values that may be associated with different types. The Apple documentation on enums uses the example of an inventory tracking system with different types of barcodes, UPCA and QR. UPCA and QR codes are both "Barcode" values so it makes sense to group them in a common type, but UPCA is associated with four Int values and a QR code is associated with a single String value.* See the example from Apple here.

Creating an enum with associated values is easy in Swift. Let's say we're developing an app that allows users to create and display drawings. There are four supported drawing types in the app: line, circle, rectangle, and freeform. We could use the following enum with associated values to represent this Drawing type:

enum Drawing {
    case line(startPoint: CGPoint, endPoint: CGPoint)
    case circle(center: CGPoint, radius: CGFloat)
    case rectangle(origin: CGPoint, size: CGSize)
    case freeform(points: [CGPoint])
}

Then to store a circle drawing:

let circle = Drawing.circle(center: CGPoint(x: 10, y: 10), radius: 5)

But what if we're developing a client app that receives drawing data from a server? It would be useful to be able to initialize a Drawing value from a server response object. For that we'll use a failable initializer that parses the response object and initializes an appropriate Drawing value if possible. First, let's recreate what some deserialized JSON might look like:

let line: [String: Any] = ["type": "line", "startX": 4, "startY": 8, "endX": 10, "endY": 6]
let circle: [String: Any] = ["type": "circle", "centerX": 10, "centerY": 10, "radius": 5]
let rectangle: [String: Any] = ["type": "rectangle", "originX": 6, "originY": 6, "width": 10, "height": 10]
let freeform: [String: Any] = ["type": "freeform", "xValues": [2, 2.01, 2.02, 2.03, 2.04, 2.05], "yValues": [2, 2.01, 2.02, 2.03, 2.04, 2.05]]
let responseObject = [line, circle, rectangle, freeform]

The responseObject is an array of dictionaries that represent drawings. All of the dictionaries have a "type", but the other keys are specific to each type of drawing. Now let's define an initializer to convert this responseObject into Drawing values we can use in our app.

init?(responseObject: [String: Any]) {
    guard let type = responseObject["type"] as? String else { return nil }
    switch type {
    case "line":
        guard let startX = responseObject["startX"] as? CGFloat else {return nil }
        guard let startY = responseObject["startY"] as? CGFloat else {return nil }
        guard let endX = responseObject["endX"] as? CGFloat else {return nil }
        guard let endY = responseObject["endY"] as? CGFloat else {return nil }
        self = .line(startPoint: CGPoint(x: startX, y: startY), endPoint: CGPoint(x: endX, y: endY))
    case "circle":
        guard let centerX = responseObject["centerX"] as? CGFloat else {return nil }
        guard let centerY = responseObject["centerY"] as? CGFloat else {return nil }
        guard let radius = responseObject["radius"] as? CGFloat else {return nil }
        self = .circle(center: CGPoint(x: centerX, y: centerY), radius: radius)
    case "rectangle":
        guard let originX = responseObject["originX"] as? CGFloat else {return nil }
        guard let originY = responseObject["originY"] as? CGFloat else {return nil }
        guard let width = responseObject["width"] as? CGFloat else {return nil }
        guard let height = responseObject["height"] as? CGFloat else {return nil }
        self = .rectangle(origin: CGPoint(x: originX, y: originY), size: CGSize(width: width, height: height))
    case "freeform":
        guard let xValues = responseObject["xValues"] as? [CGFloat] else { return nil }
        guard let yValues = responseObject["yValues"] as? [CGFloat] else { return nil }
        if xValues.count != yValues.count { return nil }
        var points = [CGPoint]()
        for index in 0...(xValues.count - 1) {
            let point = CGPoint(x: xValues[index], y: yValues[index])
            points.append(point)
        }
        self = .freeform(points: points)
    default:
        return nil
    }
}

The first thing to note here is that we're using a failable initializer. The response object being passed in could be any dictionary of String keys and AnyObject values. If at any point in the initialization process we realize we can't return a valid Drawing we will simply return nil.

Since all of the drawing dictionaries have a "type" key, we can switch on that value to determine which enum case we will use to initialize the Drawing. Then we just need to extract the associated values for each case from the dictionary and set "self" equal to the appropriate case. If the "type" key exists but is mapped to a value our app doesn't support (e.g. "triangle"), we return nil in the "default" case.

To test this code, we can iterate through the dictionaries in the responseObject array, pass them into the initializer, and print the result:

for drawingData in responseObject {
    if let drawing = Drawing(responseObject: drawingData) {
        print(drawing)
    } else {
        print("Invalid Drawing Data: \(drawingData)")
    }
}

As a final note, this is of course a naïve implementation since it doesn't account for different screen sizes / display areas. In a real drawing app you might lock in an aspect ratio and normalize the values so drawings look good in all display areas that are constrained to that aspect ratio.

* Why am I using "associated with" instead of "composed of" when talking about enum cases? It's because enumerations are really about distinguishing between the cases, independent of their associated values. For example, you may want different behavior for QR and UPCA barcodes that depends only on the type of barcode, not on the associated information required to work with or display the barcode.

switch barcode {
case .upca(_, _, _, _):
    print("This is a UPCA barcode")
case .qrCode(_):
    print("This is a QR barcode")
}