Reputation: 3177
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
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
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