Gertjan.com
Gertjan.com

Reputation: 440

Generic optional enum func

In my project I have a couple of enums which I use everywhere, these enums are created based on external json, so input is always optional. If an enum can't be created from the input, I'll define a default value.

Example of how I do things now:

enum TextAlign:String {
    case left, center, right
}

let rawData = [String:Any]()

func getTextAlign() -> TextAlign {
    if let rawTextAlignString = rawData["textAlign"] as? String, let align = TextAlign(rawValue: rawTextAlignString) {
        return align
    }

    return TextAlign.left
}
let textAlign = self.getTextAlign()

This works obviously, but I would like to make my constructor a bit more swifty, generic and applicable to more of these enums. My goal is to instantiate these enums like this:

let textAlign = TextAlign(rawOptionalValue: rawData["textAlign"] as? String) ?? TextAlign.left

So I basically want a failable initializer, which I can just write for the TextAlign enum, but there has to be a way to declare this in a more 'generic' way so that I can use the initializer on all of my enum:String instances. I'm struggeling a bit with the syntax and options of generics in swift.

Any idea?

Update #1

I see a lot of answers which are all not wrong, but probably I wasn't clear enough in what I'm looking for.

I have more enums like this:

enum TextAlign:String {
    case left, center, right
}

enum CornerRadius:String {
    case none, medium, large
}

enum Spacing:String {
    case tight, medium, loose
}

I would like to define just 1 function that can initialize all these enums. (not because I'm lazy, but because I want to understand how to use Generics for this)

Probably what I need is some static func in an extension that applies to all these 'String/RawRepresentable' enums. I don't want to write all these failable initializers for each enum. (I believe it should be possible but I can't figure out the syntax)

Update #2

After playing a bit with Joakim's answer I came up with the following solution:

extension RawRepresentable {
    static func create<T:Any>(_ value: Any?, defaultValue: Self) -> Self where Self.RawValue == T {
        guard let rawValue = value as? T, let instance = Self.init(rawValue: rawValue) else {
            return defaultValue
        }

        return instance
    }
}

This allows me to instantiate enums of type String and Int (and more) with this function. Like this:

enum TextAlign:String {
    case left, center, right
}

enum CornerRadius:Int {
    case none, medium, large
}

let json:[String:Any] = [
    "textAlign":"left",
    "cornerRadius":0
]

let cornerRadius = CornerRadius.create(json["cornerRadius"], defaultValue: .medium)
let align = TextAlign.create(json["textAlign"], defaultValue: .center)

I like that I can just pas Any? as an argument and that it takes care of the casting on its own by doing let rawValue = value as? T.

Update #3 (solution)

Okay, the complexcity of it all still bothered me a bit, so I tried the init route, which imo, looks way cleaner. The whole thing now looks like this:

extension RawRepresentable {
    init(from value: Any?, or defaultValue: Self) {
        self = Self.init(from: value) ?? defaultValue
    }

    init?(from value: Any?) {
        guard let rawValue = value as? Self.RawValue, let instance = Self.init(rawValue: rawValue) else {
            return nil
        }

        self = instance
    }
}

I created a failable and a non-failable init with a default value for convenience purposes.

Now I can just instantiate any enum like this:

let cornerRadius = CornerRadius(json["cornerRadius"], or: .medium)

// or an optional one
let align = TextAlign(json["textAlign"])

Now I'm done with the updates...

Upvotes: 2

Views: 802

Answers (5)

Mihai Fratu
Mihai Fratu

Reputation: 7663

You can declare your enums like this

enum TextAlign: String {
    case left, center, right

    init(rawOptionalValue: String?) {
        self = TextAlign(rawValue: rawOptionalValue ?? TextAlign.left.rawValue) ?? .left
    }
}

And then instantiate it like this:

let textAlign = TextAlign(rawOptionalValue: rawData["textAlign"] as? String)

Update

Here's one example with a default value as an optional parameter as well:

enum TextAlign: String {
    case left, center, right

    init(rawOptionalValue: String?, defaultValue: TextAlign = TextAlign.left) {
        self = TextAlign(rawValue: rawOptionalValue ?? defaultValue.rawValue) ?? defaultValue
    }
}

let textAlign1 = TextAlign(rawOptionalValue: "left") // .left
let textAlign2 = TextAlign(rawOptionalValue: "right") // .right
let textAlign3 = TextAlign(rawOptionalValue: "center") // .center
let textAlign4 = TextAlign(rawOptionalValue: "notAnAlignment") // .left
let textAlign5 = TextAlign(rawOptionalValue: nil, defaultValue: .center) // .center

Update 2

Ok, now I understand. Well then based on your latest update you can also do this I guess:

extension RawRepresentable {
    init(rawOptionalValue: Any?, defaultValue: Self) {
        guard let value = rawOptionalValue as? Self.RawValue else {
            self = defaultValue
            return
        }
        self = Self.init(rawValue: value) ?? defaultValue
    }
}

The only thing here compared to my previous try is that you cannot provide a default defaultValue. So you would use it like this:

let textAlign1 = TextAlign(rawOptionalValue: "left", defaultValue: .left) // .left
let textAlign2 = TextAlign(rawOptionalValue: "right", defaultValue: .left) // .right
let textAlign3 = TextAlign(rawOptionalValue: "center", defaultValue: .left) // .center
let textAlign4 = TextAlign(rawOptionalValue: "notAnAlignment", defaultValue: .left) // .left
let textAlign5 = TextAlign(rawOptionalValue: nil, defaultValue: .center) // .center

Upvotes: 2

Alexander
Alexander

Reputation: 63167

I don't think you need a new function for this. Just use Optional.map(_:):

let alignment = rawData["textAlign"].map(TextAlign.init(rawValue:)) ?? .left

Upvotes: 1

Joakim Danielson
Joakim Danielson

Reputation: 51892

Here is a function you can use to create enum items from a string

func createEnumItem<T: RawRepresentable>(_ value: String) -> T? where T.RawValue == String {
    return  T.init(rawValue: value)
}

and then use it like

let textAlign: TextAlign = createEnumItem("right")!
let radius: CornerRadius = createEnumItem("medium")!

Note that you always include the enum type in the variable declaration.

Of course since the return value is optional you need to handle that in a better way than my example here.

Update

In case you always know what your default is here is a modified version

func createEnumItem<T: RawRepresentable>(_ value: String, withDefault defaultItem: T) -> T where T.RawValue == String {
    guard let item = T.init(rawValue: value) else {
        return defaultItem
    }
    return item
}

Upvotes: 3

Gi0R
Gi0R

Reputation: 1457

You can define a static func like so and use it everywhere you need in your code

enum TextAlign: String {
    case left, center, right

    static func getTextAlign(rawData: [String:Any]) -> TextAlign {
        if let rawTextAlignString = rawData["textAlign"] as? String, let align = TextAlign(rawValue: rawTextAlignString) {
            return align
        }

        return TextAlign.left
    }
}

// Test
var myRawData = [String:Any]()

let textAlign1 = TextAlign.getTextAlign(rawData: myRawData) // left

myRawData["textAlign"] = "center"

let textAlign2 = TextAlign.getTextAlign(rawData: myRawData) // center

Upvotes: 0

Jogendar Choudhary
Jogendar Choudhary

Reputation: 3494

You can try this:

 let textAlign = TextAlign(rawValue: rawData["textAlign"] as? String ?? TextAlign.left.rawValue)

Upvotes: 0

Related Questions