Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using an enum's default value as a dictionary key without explicitly casting to String

If I declare an enum like this:

enum Keys {
    case key_one
    case key_two
}

I can print it and it will be automatically converted to a String:

print(Keys.key_one) // prints "key_one"

If I then make a dictionary that maps Strings to whatever (but let's choose Strings again for simplicity), I think that it should be able to add a key by using Keys.key_one as the key, right? Wrong.

var myDict = [String : String]()

/// error: cannot subscript a value of type '[String : String]' with an index
/// of type 'Keys'
myDict[Keys.key_one] = "firstKey"

I can do it if I explicitly convert Keys.key_one to a String like this though:

myDict[String(Keys.key_one)] = "firstKey"
print(myDict) // ["key_one": "firstKey"]

So I want to do this without having to wrap my enum with String() every time.

I've tried a few things by doing an extension off of Dictionary using the where keyword with the Key and trying to implement new subscript functions, but I can't get it to work. What's the trick?

like image 394
Tim Fuqua Avatar asked Mar 01 '16 01:03

Tim Fuqua


1 Answers

Update and improvement for Swift 4.2

extension Dictionary {
    subscript(key: APIKeys) -> Value? {
        get {
            guard let key = key.stringValue as? Key else { return nil }
            return self[key]
        }
        set(value) {
            guard let key = key.stringValue as? Key else { return }
            guard let value = value else { self.removeValue(forKey: key); return }
            self.updateValue(value, forKey: key)
        }
    }
}

protocol APIKeys {}
extension APIKeys {
    var stringValue: String {
        return String(describing: self)
    }
}

enum Keys: APIKeys {
    case key_one
    case key_two
}

var myStringDict = [AnyHashable : Any]()
var model1StringDict = [String : Any]()
var model2StringDict = [String : String]()

myStringDict.updateValue("firstValue", forKey: Keys.key_one.stringValue)                    // [key_one: firstValue]
myStringDict[Keys.key_two] = "secondValue"                                                  // [key_two: secondValue, key_one: firstValue]
myStringDict[Keys.key_one] = nil                                                            // [key_two: secondValue]
myStringDict.removeValue(forKey: Keys.key_two.stringValue)                                  // []

model1StringDict.updateValue("firstValue", forKey: Model1Keys.model_1_key_one.stringValue)  // [model_1_key_one: firstValue]
model1StringDict[Model1Keys.model_1_key_two] = "secondValue"                                // [model_1_key_two: secondValue, model_1_key_one: firstValue]
model1StringDict[Model1Keys.model_1_key_one] = nil                                          // [model_1_key_two: secondValue]

model2StringDict.updateValue("firstValue", forKey: Model2Keys.model_2_key_one.stringValue)  // [model_2_key_one: firstValue]
model2StringDict[Model2Keys.model_2_key_two] = "secondValue"                                // [model_2_key_two: secondValue, model_2_key_one: firstValue]
model2StringDict[Model2Keys.model_2_key_one] = nil                                          // [model_2_key_two: secondValue]

I specifically changed the types of the 3 dictionaries to show the common ways of typing a dictionary ([AnyHashable : Any], [String : Any], and [String : String]), and showed that this works with each of the types.

It's important to note that if you use updateValue instead of the assignment operator when the key of your dictionary is AnyHashable, then you need to specify the String value of the key with .stringValue. Otherwise, the exact type of the key being stored will not explicitly be a String, and it'll get messed up later if you try to, say, remove a value under your key via assigning nil to that key. For a dictionary where the key is specifically typed to be String, then the updateValue function will have a compile time error saying that the key needs to be a String, so you can't mess it up that way.

Swift 2.3

I figured out the extension solution that I wanted.

extension Dictionary {
    
    subscript(key: Keys) -> Value? {
        get {
            return self[String(key) as! Key]
        }
        set(value) {
            guard
                let value = value else {
                    self.removeValueForKey(String(key) as! Key)
                    return
            }

            self.updateValue(value, forKey: String(key) as! Key)
        }
    }

}

enum Keys {
    case key_one
    case key_two
}

var myStringDict = [String : String]()
/// Adding the first key value through the String() way on purpose
myStringDict.updateValue("firstValue", forKey: String(Keys.key_one))
// myStringDict: ["key_one": "firstValue"]
// myStringDict[Keys.key_one]!: firstValue

myStringDict[Keys.key_two] = "secondValue"
// myStringDict: ["key_one": "firstValue", "key_two": "secondValue"]

myStringDict[Keys.key_one] = nil
// myStringDict: ["key_two": "secondValue"]

Notice that the declared dictionary key type is String, but I'm able to just use Keys.key_one and the subscript in the dictionary extension takes care of the rest.

I can probably put some better guarding around the as! conversion to Key, but I'm not sure it's needed, as I know that my enum can always be converted to a valid Key by the String() cast.

Improvement to answer

Even better, since I'm using this for API Keys, I made a blank protocol called APIKeys and each model will implement their own Keys enum that conforms to the APIKeys protocol. And the dictionary's subscript is updated to take in APIKeys as the Key value.

extension Dictionary {
    
    subscript(key: APIKeys) -> Value? {
        get {
            return self[String(key) as! Key]
        }
        set(value) {
            guard
                let value = value else {
                    self.removeValueForKey(String(key) as! Key)
                    return
            }

            self.updateValue(value, forKey: String(key) as! Key)
        }
    }

}

protocol APIKeys {}

enum Keys: APIKeys {
    case key_one
    case key_two
}

enum Model1Keys: APIKeys {
    case model_1_key_one
    case model_1_key_two
}

enum Model2Keys: APIKeys {
    case model_2_key_one
    case model_2_key_two
}

var myStringDict = [String : String]()
var model1StringDict = [String : String]()
var model2StringDict = [String : String]()

myStringDict.updateValue("firstValue", forKey: String(Keys.key_one))    // myStringDict: ["key_one": "firstValue"]
myStringDict[Keys.key_two] = "secondValue"                              // myStringDict: ["key_one": "firstValue", "key_two": "secondValue"]
myStringDict[Keys.key_one] = nil                                        // myStringDict: ["key_two": "secondValue"]

model1StringDict.updateValue("firstValue", forKey: String(Model1Keys.model_1_key_one))  // model1StringDict: ["model_1_key_one": "firstValue"]
model1StringDict[Model1Keys.model_1_key_two] = "secondValue"                            // model1StringDict: ["model_1_key_one": "firstValue", "model_1_key_two": "secondValue"]
model1StringDict[Model1Keys.model_1_key_one] = nil                                      // model1StringDict: ["model_1_key_two": "secondValue"]

model2StringDict.updateValue("firstValue", forKey: String(Model2Keys.model_2_key_one))  // model2StringDict: ["model_2_key_one": "firstValue"]
model2StringDict[Model2Keys.model_2_key_two] = "secondValue"                            // model2StringDict: ["model_2_key_one": "firstValue", "model_2_key_two": "secondValue"]
model2StringDict[Model2Keys.model_2_key_one] = nil                                      // model2StringDict: ["model_2_key_two": "secondValue"]
like image 87
Tim Fuqua Avatar answered Oct 14 '22 03:10

Tim Fuqua