androidguy
androidguy

Reputation: 3177

Can I instantiate or produce an enum value from the enum name and the value's rawValue at runtime in Swift?

We are using Swift 5.0. I need to turn a list of strings into a set of enum cases regularly. I wrote a Kotlin function easily that takes an enum class at runtime and a list of strings, and converts it to a Java EnumSet (well, 2 functions that work together):

fun <EnumT : Enum<EnumT>> ConvertStrToEnum(enumClass: Class<EnumT>, str: String?): EnumT? {
    if (str == null)
        return null
    for (enumval in enumClass.enumConstants) {
        if (enumval.toString() == str)
            return enumval
    }
    throw IllegalArgumentException("Gave an invalid enum value for class ${enumClass.canonicalName}: [$str]")
}

fun <EnumT : Enum<EnumT> > ConvertStrArrayToEnumSet(enumClass: Class<EnumT>, array: List<String>?) : EnumSet<EnumT> {
    val set = EnumSet.noneOf(enumClass)
    array?.forEach { value -> ignoreExceptions { set.add(ConvertStrToEnum(enumClass, value)) } }
    return set
}

And, to be clear, an actual usage is:

var intent: EnumSet<Intent>
intent = ConvertStrArrayToEnumSet(Intent::class.java, filters.array(MatchFilter.Intent.jsonName))

Can I write a function in Swift 5 that achieves the same result? I wrote this for one conversion, here's the example. If I can't write this function I will have this boilerplate code repeated throughout the app.

  public var intents: Set<Intent>
  if let jsonIntents = filters?["intent"] as? Array<String> {
     for jsonIntent in jsonIntents {
        if let intent = Intent(rawValue: jsonIntent) {
           intents.insert(intent)
        }
     }
  }

Upvotes: 2

Views: 198

Answers (2)

user652038
user652038

Reputation:

Sweeper's answer is good, but I see you put some effort into error handling. Swift isn't good about helping you with that, so you have to make your own extensions.

(Dictionary and RawRepresentable are from Swift 1, which didn't have errors. They never got modernized, and just return optionals.)

/// Acts as a dictionary that `throw`s instead of returning optionals.
public protocol valueForKeyThrowingAccessor {
  associatedtype Key

  /// Should just be a throwing subscript, but those don't exist yet.
  func value<Value>(for: Key) throws -> Value
}
/// Acts as a dictionary.
public protocol valueForKeySubscript: valueForKeyThrowingAccessor {
  associatedtype Value

  subscript(key: Key) -> Value? { get }
}

public extension valueForKeySubscript {
  /// - Throws: `KeyValuePairs<Key, Value>.AccessError.noValue`
  func value(for key: Key) throws -> Value {
    guard let value = self[key]
    else { throw KeyValuePairs<Key, Value>.AccessError.noValue(key: key) }

    return value
  }

  /// - Throws: `KeyValuePairs<Key, Value>.AccessError.typeCastFailure`
  func value<Value>(for key: Key) throws -> Value {
    guard let value = try value(for: key) as? Value
    else { throw KeyValuePairs<Key, Value>.AccessError.typeCastFailure(key: key) }

    return value
  }
}

extension Dictionary: valueForKeySubscript { }
public extension KeyValuePairs {
  /// An error throw from trying to access a value for a key.
  enum AccessError: Error {
    case noValue(key: Key)
    case typeCastFailure(key: Key)
  }
}
public extension RawRepresentable {
  /// Like `init(rawValue:)`, if it was throwing instead of failable.
  /// - Throws: `RawRepresentableExtensions<Self>.Error.invalidRawValue`
  /// if there is no value of the type that corresponds with the specified raw value.
  init(_ rawValue: RawValue) throws {
    guard let instance = Self(rawValue: rawValue)
    else { throw RawRepresentableExtensions<Self>.Error.invalidRawValue(rawValue) }

    self = instance
  }
}

/// A namespace for nested types within `RawRepresentable`.
public enum RawRepresentableExtensions<RawRepresentable: Swift.RawRepresentable> {
  public enum Error: Swift.Error {
    case invalidRawValue(RawRepresentable.RawValue)
  }
}

public extension InitializableWithElementSequence where Element: RawRepresentable {
  /// - Throws: `RawRepresentableExtensions<Element>.Error.invalidRawValue`
  init<RawValues: Sequence>(rawValues: RawValues) throws
  where RawValues.Element == Element.RawValue {
    self.init(
      try rawValues.map(Element.init)
    )
  }
}
/// A type that can be initialized with a `Sequence` of its `Element`s.
public protocol InitializableWithElementSequence: Sequence {
  init<Sequence: Swift.Sequence>(_: Sequence)
  where Sequence.Element == Element
}

extension Array: InitializableWithElementSequence { }
extension Set: InitializableWithElementSequence { }
extension InitializableWithElementSequence where Element == Intent {
  init(filters: [String: Any]) throws {
    try self.init(
      rawValues: try filters.value(for: "intent") as [String]
    )
  }
}
try filters.map(Set.init)

Upvotes: 0

Sweeper
Sweeper

Reputation: 273380

Assuming your enums are RawRepresentable with RawValue == String...

Enums in Swift don't have a special "base class" like Enum. But in this situation, we really just need to make use of their common property - both RawRepresentable and Hashable. Sure, lots of non-enums have this property, too. So our method will work on not just enums, but any type that conforms to these two protocols. That's rather nice isn't it?

func convertStringArrayToEnumSet<T>(type: T.Type, _ strings: [String]) -> Set<T> 
    where T : RawRepresentable & Hashable, T.RawValue == String {
    Set(strings.compactMap(T.init(rawValue:)))
}

Note the use of compactMap, which discards any invalid raw values.

In fact, you can generalise this to not just string arrays, but any array:

func convertRawValueArrayToEnumSet<T>(type: T.Type, _ rawValues: [T.RawValue]) -> Set<T> 
    where T : RawRepresentable & Hashable {
    Set(rawValues.compactMap(T.init(rawValue:)))
}

Upvotes: 1

Related Questions