Skip to main content

Sealed Classes

Overview

Sealed classes are conceptually similar feature to enums with associated values in Swift. However, these features have some semantic differences, so they cannot be translated directly. Not to mention that Objective-C doesn't support enums with associated values anyway.

Kotlin solves this issue by translating the sealed classes to regular Objective-C classes. As a result, sealed classes in Swift are missing the support for exhaustive switching, which is why you need to put the default case to all switch statements.

SKIE adds a way to exhaustively switch over a sealed class hierarchy by generating a Swift enum that wraps the Kotlin sealed class. This enum then can be used in the switch statements instead of the original sealed class. To make the syntax more convenient, SKIE also generates a function onEnum(of:), which converts the Kotlin class to the Swift enum.

tip

Sealed interfaces are also supported by SKIE.

Examples

Basic usage

Let's say we have the following Kotlin sealed class:

Kotlin
sealed class Status {
object Loading : Status()
data class Error(val message: String) : Status()
data class Success(val result: SomeData) : Status()
}

Without SKIE, you have to use the following syntax in the switch statement:

Swift without SKIE
func updateStatus(status: Status) {
switch status {
case _ as Status.Loading:
showLoading()
case let error as Status.Error:
showError(message: error.message)
case let success as Status.Success:
showResult(data: success.result)
default:
fatalError("Unknown status")
}
}

With SKIE, this can be simplified to:

Swift with SKIE
func updateStatus(status: Status) {
switch onEnum(of: status) {
case .loading:
showLoading()
case .error(let error):
showError(message: error.message)
case .success(let success):
showResult(data: success.result)
}
}

Notice that the Kotlin object is wrapped in the generated enum as an associated value. This object has the correct subtype, so you do not have to explicitly cast it to the subtype - which was needed in the previous example.

Optional sealed class

SKIE generates an additional overload for the onEnum function that accepts an optional value and returns an optional enum case. This makes it possible to use such optional values without manually unwrapping them first. So, given the sealed class from the previous example, you can also write:

Swift with SKIE
func updateStatus(status: Status?) {
switch onEnum(of: status) {
case .loading:
showLoading()
case .error(let error):
showError(message: error.message)
case .success(let success):
showResult(data: success.result)
case .none:
showNoStatus()
}
}

Hidden sealed classes

Not all subclasses of a sealed class have to be exposed to Swift. This can happen for many reasons. For example, the subclass might be an internal class (or a private class).

Let's take this example where one of the subclasses is an internal class:

Kotlin
sealed class Status {
object Loading : Status()
internal data class Error(val message: String) : Status()
data class Success(val result: SomeData) : Status()
}

In this case, the Error class is not exposed to Swift, so it cannot be used in the switch statement. SKIE solves this issue by generating an else case that is used to handle all hidden subclasses:

Swift with SKIE
func updateStatus(status: Status) {
switch onEnum(of: status) {
case .loading:
showLoading()
case .success(let success):
showResult(data: success.result)
case .else:
unknownStatus()
}
}

Limitations

This feature itself doesn't have any known limitations. However, there is one limitation related to the fundamental difference between sealed interfaces and enums with associated values in Swift: While sealed classes (as any other Kotlin class) conform to Hashable, sealed interfaces do not. Therefore, it's impossible to put sealed interfaces in a Set or use them as keys in a Dictionary.

To improve the support for sealed interfaces, SKIE adds a Hashable conformance to the generated enums when possible. The requirement for adding Hashable is that all exposed direct children of the sealed type must be classes.

In other cases (when a sealed interface is extended by another interface), it's possible to implement the Hashable protocol manually using Swift extensions. Note that this approach is only possible with the enum generated by SKIE because Swift extensions cannot add protocol conformance to other protocols. So, if you need the Hashable conformance, you must use the generated enum instead of the original sealed interface.

Migration and Compatibility

This feature should not cause any breaking changes.