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:
enum class Turn {
Left, Right
}
Without SKIE, Swift needs a default case:
func changeDirection(turn: Turn) {
switch turn {
case .left:
goLeft()
case .right:
goRight()
default:
startOver()
}
}
And if we add a new enum case...
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.
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:
enum class E {
camelCase, PascalCase, UPPER_SNAKE_CASE;
}
would be renamed to:
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:
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:
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:
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:
- Changes to enum case names
- Changes to built-in functions
- The interface conformance limitation.
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
toreturn_
, which is unnecessary becausereturn
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
.
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.