Skip to main content

Enums

Overview

Kotlin compiler exports Kotlin enums as regular Obj-C classes with static properties representing the enum cases. This approach has some drawbacks, including the missing support for exhaustive switching, which is why need to put the default case to all switch statements. This adds unnecessary boilerplate and makes it very easy to forget to update all switch statements when you add a new enum case.

SKIE solves this issue by generating a wrapping Swift enum for a given Kotlin enum. Additionally, SKIE automatically replaces references to the Kotlin enums with the generated Swift enums with no involvement from the developer - in almost all cases. The original Kotlin enum is still available but prefixed with two underscores __ to avoid name collisions.

There is also the inconvenience that Kotlin and Swift have different conventions for naming enum cases. Swift uses camelCase, whereas Kotlin uses UPPER_SNAKE_CASE or PascalCase. For this reason, Kotlin renames the enum cases to follow the Swift naming convention. However, it uses a simplified naming algorithm that only accounts for UPPER_SNAKE_CASE, not PascalCase.

SKIE also solves this issue - by implementing a more sophisticated algorithm that supports PascalCase.

Examples

Exhaustive Switching

Given this enum:

Kotlin
enum class Turn {
Left, Right
}

Without SKIE, Swift needs a default case:

Swift without SKIE
func changeDirection(turn: Turn) {
switch turn {
case .left:
goLeft()
case .right:
goRight()
default:
startOver()
}
}

And if we add a new enum case...

Kotlin
enum class Turn {
Left, Right, Neither
}

... the Swift code won't tell us about the new case, and it will use the default case when it encounters the new case. This is probably not what we wanted.

SKIE adds back the exhaustive checking, so the default case is no longer required, which solves this problem.

Swift with SKIE
func changeDirection(turn: Turn) {
switch turn {
case .left:
goLeft()
case .right:
goRight()
case .neither:
noChange()
}
}

Enum case names

Without SKIE, the cases of the following Kotlin enum:

Kotlin
enum class E {
camelCase, PascalCase, UPPER_SNAKE_CASE;
}

would be renamed to:

Swift without SKIE
case camelcase
case pascalcase
case upperSnakeCase

Notice that the PascalCase and camelCase cases are missing the capitalized 'C'.

With SKIE, we get the expected result:

Swift with SKIE
case camelCase
case pascalCase
case upperSnakeCase

Limitations

This feature has two limitations:

  • The generated Swift enum cannot implement the same interfaces as the original Kotlin enum.
  • SKIE cannot automatically replace the enum types in generics.

Enums implementing Interfaces

Swift enums (and structs) cannot implement Obj-C protocols, so SKIE cannot add conformance to the original Kotlin enum's interfaces. This is usually not a problem unless you want to pass instances of the enum to a Kotlin function that expects the interface type. A typical example of this situation would be an interface extension function. However, interface methods are supported because they are handled in the same way as other enum methods.

To work around this limitation, you can convert the Swift enum back to the original Kotlin enum using the toKotlinEnum() method. Similarly, it's possible to convert the Kotlin enum to the Swift enum using the toSwiftEnum() method. You can also cast between these two enums (in both directions) using the as keyword.

Examples:

Swift with SKIE
let turn: Turn = Turn.left

// Conversion to Kotlin enum
let kotlinEnum: __Turn = turn.toKotlinEnum()
let kotlinEnum: __Turn = turn as __Turn // The original enum is prefixed with two underscores.

// Conversion to Swift enum (turn == swiftEnum)
let swiftEnum: Turn = kotlinEnum.toSwiftEnum()
let swiftEnum: Turn = kotlinEnum as Turn

Alternatively, you can disable this feature for problematic enums using the configuration.

Using Enums in generics

This limitation is caused by a problem similar to the previous one. This time, the issue is that Obj-C generics require the type to be a class - which the generated enum is not. Therefore, SKIE cannot replace enums in type arguments of classes and all methods of that class will use the original Kotlin enum in place of the type parameter.

So for the function enumResult in:

Kotlin
data class ResultWrapper<T : Any>(val t: T)

fun enumResult(): ResultWrapper<Turn> {
return ResultWrapper(Turn.Left)
}

will return ResultWrapper<__Turn> in Swift, rather than the expected ResultWrapper<Turn>.

The workarounds are the same as in the previous limitation: You can use the toSwiftEnum() method to get the more useful Swift enum or disable this feature.

Migration and Compatibility

This feature introduces some breaking changes for the Swift code, but those should be quick to fix in most cases. There are multiple sources of these breaking changes:

Enabling this feature will also introduce warnings about the default case not being reachable in all switch statements that previously used the Kotlin enums. This can be easily fixed by removing the no longer necessary default case.

Enum case names

As mentioned above, SKIE renames the enum cases to follow the Swift naming convention. This will result in an error message saying something like Type 'E' has no member 'xyz'. Since the original enum case names are usually multiple words without any uppercase letters, you might get away with doing a simple find & replace for each affected enum case.

Additionally, SKIE renames cases that collide with Swift keywords and Objective-C methods. Kotlin also does this, but its implementation is not correct in some cases, which creates a difference in some situations. For example:

  • Kotlin (but not SKIE) renames return to return_, which is unnecessary because return is only a soft keyword in Swift.
  • On the other hand, zone is not renamed by Kotlin, which is incorrect because this name collides with an Obj-C method.

Note that the names that collide with Obj-C methods are renamed by adding the the prefix, instead of the _ suffix. So, for example, zone becomes theZone.

tip

You can look at the generated Swift enum in Xcode to see the correct case names.

Built-in Functions

Enum classes in Kotlin have some built-in functions and properties: the static functions values() and valueOf(String), and the instance properties name and ordinal.

These functions are also bridged to Swift, but how exactly depends on the function:

values()

This function is replaced by its Swift equivalent allCases property from the CaseIterable protocol to which the generated Swift enum conforms.

valueOf(String)

This function currently does not have a direct equivalent in Swift, but we plan to add one in the future. For now, you can work around this issue by adding conformance to the RawRepresentable protocol, which adds the init?(rawValue: T) constructor. Alternatively, you can use the valueOf(String) function of the original Kotlin enum (which is prefixed by the __) and then convert the result to the Swift enum using the toSwiftEnum() extension.

name

The name property is preserved and returns the Kotlin case name.

ordinal

The ordinal property is also preserved and behaves the same as in Kotlin.