1

So I have several enums looks like this:

enum TrackType: String, CustomStringConvertible {
    case video
    case audio
    case subtitles
    case unsupported
        
    var description: String {
        switch self {
            case .video: return "视频"
            case .audio: return "音轨"
            case .subtitles: return "字幕"
            case .unsupported: return "不支持的 track"
        }
    }
        
    // custom initializater to provide default value
    // so I don't have to write:
    // "TrackType.init(rawValue: value) ?? .unsupported" 
    // everywhere
    init(rawValue: String) {
        switch rawValue {
            case "video": self = .video
            case "audio": self = .audio
            case "subtitles": self = .subtitles
            default: self = .unsupported
        }
    }
}

// usage
let value = "foobar"
let trackType = TrackType.init(rawValue: value) // .unsupported

The downside of this approach is I have to manually list all cases for every enum I write, so I go like this:

extension TrackType: ExpressibleByStringLiteral {
    init(stringLiteral value: String) {
        guard let validValue = Self(rawValue: value) else {
            self = .unsupported
            return
        }
        self = validValue
    }
}

// usage
let value = "foobar"
let trackType = value as TrackType // .unsupported

This way I could avoid the tedious listing work, but all of my enums have to conform to ExpressibleByStringLiteral, so it's still repetitive

I try to make a protocol like this:

protocol StringEnum: ExpressibleByStringLiteral {
    static var `default`: Self { get }

    init?(rawValue: String)
}

extension StringEnum {
    init(stringLiteral value: String) {
        guard let validValue = Self(rawValue: value) else {
            self = Self.`default`
            return
        }
        self = validValue
    }
}

// error:
// Initializer 'init(stringLiteral:)' has different argument labels from those required by protocol 'StringEnum' ('init(rawValue:)')
enum TrackType: StringEnum {
    static var `default` = TrackType.unsupported
    
    case video
    case audio
    case subtitles
    case unsupported
}

Where should I go from here?

I've seen answers in Default value for Enum in Swift, but none of them is convenient enough...

Paulo Mattos
  • 18,845
  • 10
  • 77
  • 85
NSFish
  • 354
  • 3
  • 10

2 Answers2

2

You can fix it by using the init(rawValue:) initializer for RawRepresentable enums. I also constrain the RawValue to String-only enums.

This does require you still mark the enum as having a String raw value in the enum itself.

Code:

protocol StringEnum: RawRepresentable, ExpressibleByStringLiteral where RawValue == String {
    static var `default`: Self { get }
}

extension StringEnum {
    init(stringLiteral value: String) {
        guard let validValue = Self(rawValue: value) else {
            self = Self.`default`
            return
        }

        self = validValue
    }
}

enum TrackType: String, StringEnum {
    static let `default` = TrackType.unsupported

    case video
    case audio
    case subtitles
    case unsupported
}

Usage:

let value: TrackType = "foobar"
print(value)

With 'foobar' the result is 'unsupported'. With 'video', the result is 'video'. It's working correctly.

George
  • 25,988
  • 10
  • 79
  • 133
0

You may already know this, but just in case you do not, as a best practice, try to avoid string literals as much as possible. The reason one typically uses enums in the first place is so that you do not rely on string literals all over your code (which are susceptible to typos that cannot be caught by the compiler).

If you do need to check if a string is a rawValue within a particular enum, you would not need init. You could just write succinctly:

TrackType(rawValue: value)

Also, just in case - I'm not sure if this applies to your use case without seeing the rest of the code, but if what you are trying to do is to provide a default description for certain enum values, then what you could do is the following:

var description: String
    {
    switch self
        {
        case .video: return "视频"
        case .audio: return "音轨"
        case .subtitles: return "字幕"
        default: return "不支持的 track" // .unsupported or any case other than the 3 above
        }
    }

Then the usage would be:

let value = "foobar"
let trackType = TrackType(rawValue: value) ?? .unsupported // .unsupported
let description = trackType.description  // "不支持的 track"

I know your question was wanting the code to be as succinct as possible, and I would contend that this is as succinct as you should be, without sacrificing readability and understanding of the code.

Gene Loparco
  • 2,157
  • 23
  • 23