Mohsen
Mohsen

Reputation: 65775

enum of non-literal values in Swift

Is there any way to map a non-literal value like tuple of dictionary to enums? Following code will throw Raw value for enum must be literal.

enum FileType {
    case VIDEO = ["name": "Video", "contentTypeMatcher": "video/"]
    case IMAGE = ["name": "Image", "contentTypeMatcher": "image/"]
    case AUDIO = ["name": "Audio", "contentTypeMatcher": "aduio/"]
    case PDF   = ["name": "PDF", "contentTypeMatcher":"application/pdf"]
    case TEXT  = ["name": "Text", "contentTypeMatcher": "text/"]
    case FOLDER= ["name": "Folder", "contentTypeMatcher" :"application/x-directory"]
    case PLAIN = ["name": "Plain", "contentTypeMatcher": ""]
}

It's the same when I use tuples:

enum FileType {
    case VIDEO  = (name: "Video", contentTypeMatcher: "video/")
    case IMAGE  = (name: "Image", contentTypeMatcher: "image/")
    case AUDIO  = (name: "Audio", contentTypeMatcher: "aduio/")
    case PDF    = (name: "PDF", contentTypeMatcher:"application/pdf")
    case TEXT   = (name: "Text", contentTypeMatcher: "text/")
    case FOLDER = (name: "Folder", contentTypeMatcher :"application/x-directory")
    case PLAIN  = (name: "Plain", contentTypeMatcher: "")
}

Upvotes: 12

Views: 8665

Answers (3)

Scott Wood
Scott Wood

Reputation: 404

My coworkers and I have been debating this topic recently as Swifts enum type is unique from other languages. In a language like Java where an enum is just a class that inherits from Enumeration, you can have static non-literal values assigned to each case.

In swift, we can not find a supported way to do this. From Swift documentation:

If a value (known as a “raw” value) is provided for each enumeration case, the value can be a string, a character, or a value of any integer or floating-point type.

Alternatively, enumeration cases can specify associated values of any type to be stored along with each different case value, much as unions or variants do in other languages. You can define a common set of related cases as part of one enumeration, each of which has a different set of values of appropriate types associated with it.

The second paragraph may seem like it can do what @Antonio asked but it is not. In swift's example:

enum Barcode {
   case upc(Int, Int, Int, Int)
   case qrCode(String)
}

But each enum is an instance with different value types (tuple vs string) and the values within them are different based on each instance of the enum created.

I wanted something that allowed more than the limited raw values but each enum contained the same value type (ie tuple, object, etc...) and is static.

With my coworkers input we came up with two options that have different tradeoffs.

The first is a private static dictionary by the enum that holds the value type you desire:

enum FooBarDict {
   case foo
   case bar

   private static let dict = [foo: (x: 42, y: "The answer to life, the universe, and everything"),
                              bar: (x: 420, y: "Party time")]

   var x: Int? { return FooBarDict.dict[self]?.x }
   var y: String? { return FooBarDict.dict[self]?.y }
}

Our issue with this implementation is that there's no way at compile time that you can ensure that the developer has exhaustively included all of the enum cases. This means that any properties you right must be optional or return a default.

To resolve that issue we came up with the following:

enum FooBarFunc {
    case foo
    case bar

    typealias Values = (x: Int, y: String)
    private func getValues() -> Values {
        switch self {
        case .foo: return (x: 42, y: "The answer to life, the universe, and everything")
        case .bar: return (x: 420, y: "Party time")
        }
    }

    var x: Int { return getValues().x }
    var y: String { return getValues().y }
}

Now it is exhaustive due to the switch statement in the getValues! A developer can not add a new case and compile without explicitly adding the value type.

My (perhaps unfounded) fear with this approach is that it may be both slower due to the switch statement lookup - although this may be optimized to be as fast as the dictionary lookup. And I am unsure if it will create a new value each time a enum property is requested. I'm sure I could find answers to both of these concerns but I've already wasted too much time on it.

To be honest, I hope I'm just missing something about the language and this is easily done in another way.

Upvotes: 0

Stanislav Smida
Stanislav Smida

Reputation: 1585

@Antonio gives workaround but does not answer the actual question.

Define your enum.

enum FileType {

    case Image, Video
}

Give cases non-literal values, whatever type you want with conforming to RawRepresentable protocol. Do it by enum extension to have cleaner code.

extension FileType: RawRepresentable {

    typealias Tuple = (name: String, contentTypeMatcher: String)

    private static let allCases = [FileType.Image, .Video]

    // MARK: RawRepresentable

    typealias RawValue = Tuple

    init?(rawValue: Tuple) {

        guard let c = { () -> FileType? in

            for iCase in FileType.allCases {
                if rawValue == iCase.rawValue {
                    return iCase
                }
            }
            return nil

        }() else { return nil }
        self = c
    }

    var rawValue: Tuple {

        switch self {
        case .Image: return Tuple("Image", "image/")
        case .Video: return Tuple("Video", "video/")
        }
    }
}

To be able to match Tuple in switch, implement pattern matching operator.

private func ~= (lhs: FileType.Tuple, rhs: FileType.Tuple) -> Bool {

    return lhs.contentTypeMatcher == rhs.contentTypeMatcher && lhs.name == rhs.name
}

And thats it...

let a = FileType.Image
print(a.rawValue.name) // "Image"
let b = FileType(rawValue: a.rawValue)!
print(a == b) // "true"
print(b.rawValue.contentTypeMatcher) // "image/"

Let's say I answered the question without questioning. Now... Enums (in Swift at least) are designed to have unique cases. Caveat to this workaround is that you can (I hope by accident) hold same rawValue for more cases. Generally your example code smells to me. Unless you (for very reasonable reason) need to create new enum value from tuple, consider redesign. If you want go with this workaround, I suggest (depends on project) to implement some check if all case raw values are unique. If not, consider this:

enum FileType {

    case Video, Image

    var name: String {
        switch self {
        case .Image: return "Image"
        case .Video: return "Video"
    }

    var contentTypeMatcher: String {
        switch self {
        case .Image: return "image/"
        case .Video: return "video/"
    }
}

Upvotes: 6

Antonio
Antonio

Reputation: 72750

The language reference, when talking about Enumeration Declaration, clearly states that:

the raw-value type must conform to the Equatable protocol and one of the following literal-convertible protocols: IntegerLiteralConvertible for integer literals, FloatingPointLiteralConvertible for floating-point literals, StringLiteralConvertible for string literals that contain any number of characters, and ExtendedGraphemeClusterLiteralConvertible for string literals that contain only a single character.

So nothing else but literals can be used as raw values.

A possible workaround is to represent the dictionary as a string - for example, you can separate elements with commas, and key from value with colon:

enum FileType : String {
    case VIDEO = "name:Video,contentTypeMatcher:video/"
    case IMAGE = "name:Image,contentTypeMatcher:image/"
    ...
}

Then, using a computed property (or a method if you prefer), reconstruct the dictionary:

var dictValue: [String : String] {
    var dict = [String : String]()

    var elements = self.rawValue.componentsSeparatedByString(",")
    for element in elements {
        var parts = element.componentsSeparatedByString(":")
        if parts.count == 2 {
            dict[parts[0]] = parts[1]
        }
    }

    return dict
}

Upvotes: 4

Related Questions