Skip to main content

Features

SKIE currently offers improvements for several important Kotlin features, and many smaller enhancements and bug fixes for the Kotlin compiler. Here is a quick overview of what SKIE can improve for you:

Each feature also has a dedicated page that contains all the details you need to know to use the feature successfully in your project.

Enumerations

Kotlin has two different concepts for representing a finite set of values: enums and sealed classes/interfaces. However, neither of them is directly mapped to Swift enums. This causes some problems when using Kotlin enums and sealed classes from Swift. The most significant one is the missing support for exhaustive switching, which is why you need to put the default case to all switch statements.

SKIE solves this issue by generating wrapping Swift enums for both Kotlin enums and sealed classes. However, the exact details of how this wrapping works differ between these two cases.

Enums

Kotlin enums are more straightforward to support because they are semantically almost a perfect subset of Swift enums. Therefore, SKIE can automatically replace the Kotlin enums with the generated Swift enums with no involvement from the developer - in almost all cases.

For example, with this Kotlin enum:

Kotlin
enum class Turn {
Left, Right
}

you no longer need to write the default case in Swift:

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

Read more about Enums

Sealed Classes

Sealed classes are conceptually similar to enums with associated values in Swift. However, they have some significant semantic differences, so they cannot be translated directly.

Kotlin exposes these sealed classes as regular classes; SKIE doesn't change that. Instead, it generates a Swift enum that wraps the Kotlin sealed class. This enum 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.

For example, this sealed class:

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

can be used in Swift like this:

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

Read more about Sealed Classes

Functions

SKIE uses various techniques to improve the Swift interop for Kotlin functions, which lose many of the more advanced features.

Default Arguments

While both Kotlin and Swift support default arguments for functions, the same is not true for Objective-C. As a result, all Kotlin functions are exported without the default arguments. SKIE adds back this feature by generating overloads for all possible combinations of the default arguments.

For example:

Kotlin
fun sayHello(message: String = "Hello") {
println(message)
}
Swift with SKIE
// calls sayHello(message: "Hello")

sayHello()
caution

When this feature is overused, it can create a non-negligible overhead due to the number of generated functions. For this reason, the default arguments are disabled by default and intended to be enabled only when needed using the configuration.

Read more about Default Arguments

Global Functions and Properties

Kotlin exposes global functions under a namespace named after the file that contains the global functions.

So, for example, the following Kotlin code:

Kotlin (File.kt)
fun globalFunction(i: Int): Int = i

can be called from Swift like this:

Swift without SKIE
FileKt.globalFunction(i: 1)

SKIE improves the syntax by generating wrappers for these global functions and properties. So with SKIE, the same function can be called simply:

Swift with SKIE
globalFunction(i: 1)

Read more about Global Functions and Properties

Interface Extensions

Interface extensions experience a similar problem as global functions.

For example:

Kotlin (File.kt)
interface I

class C : I

fun I.interfaceExtension(i: Int): Int = i

has to be called like this:

Swift without SKIE
FileKt.interfaceExtension(C(), i: 1)

The solution to this problem is also similar. So with SKIE, the interface extensions can be called using the same syntax as class extensions:

Swift with SKIE
C().interfaceExtension(i: 1)

Read more about Interface Extensions

Overloaded Functions

Consider the following two Kotlin functions:

Kotlin
fun foo(i: Int) {
}

fun foo(i: String) {
}

These functions can share the same name in Kotlin because Kotlin supports function overloading based on parameter types. And even though Swift also supports this feature, Objective-C doesn't. Therefore, the Kotlin compiler renames one of these functions to avoid a name collision.

So for example, these two functions would have to be called like this from Swift:

Swift without SKIE
foo(i: 1)

foo(i_: "A")

Because this renaming is not necessary for Swift, SKIE can leave the original name intact, resulting in:

Swift with SKIE
foo(i: 1)

foo(i: "A")

Read more about overloading functions

Coroutines Interop

The Coroutines library consists of two main features: suspend functions and Flows. Both of these features are frequently needed in the Swift code, but they do not have good support from the compiler. As a result, using them from Swift is cumbersome and error-prone.

SKIE adds a better support for both of these features, which eliminates most of the problems.

Suspend Functions

The Kotlin compiler compiles suspend functions as Obj-C callback functions, which can be called from Swift using the async/await syntax. However, this is just Swift's syntax sugar. Swift still handles these functions as callback functions that do not support cancellation.

SKIE solves this limitation by generating proper Swift async functions and provides a custom runtime that bridges the concurrency contexts of both platforms. As a result, SKIE supports automatic two-way cancellation.

Another limitation is that these suspend functions can only be called from the main thread. This limitation can be solved without SKIE by passing a special configuration flag to the compiler. However, SKIE adds this support out of the box.

As an example, let's say we have this suspend function:

Kotlin
class ChatRoom {
suspend fun send(message: String) {
// Send a message
}
}

with SKIE this function behaves exactly the same as any other Swift async function:

Swift with SKIE
let chatRoom = ChatRoom()

let task = Task.detached { // Runs on a background thread
try? await chatRoom.send(message: "some message")
}

// Cancellation of the task also cancels the Kotlin coroutine
task.cancel()

Read more about Suspend Functions

Flows

The problem with Flows in the context of Swift interop is that it is a regular Kotlin interface. While Swift also has its own implementation of Reactive Streams API in the form of AsyncSequence, these two are incompatible. Therefore, there is not only no direct interop between them, but there is also no easy way to manually cast one to the other.

SKIE automatically converts Flows to custom Swift classes that implement AsyncSequence, eliminating the need to convert between the two types. As with suspend functions, the concurrency contexts of both Kotlin and Swift cooperate, so there is the two-way cancellation and no thread restriction. As a bonus, the transformation preserves the type argument T of the Flow<T>, which was previously lost due to Flows being interfaces.

Suppose Kotlin exposes a StateFlow with a List<String> as a type argument:

Kotlin
class ChatRoom {
private val _messages = MutableStateFlow(emptyList<String>())
val messages: StateFlow<List<String>> = _messages
// etc
}

With SKIE, Swift sees the Flow as an AsyncSequence, with an array of strings ([String]) as its type argument.

Swift with SKIE
class ChatRoomViewModel: ObservableObject {
let chatRoom = ChatRoom()

@Published
private(set) var messages: [String] = []

@MainActor
func activate() async {
for await messages in chatRoom.messages {
// No type cast (eg `it as! [String]`) is needed because the generic type is preserved.
self.messages = messages
}
}
}

The converted Flow also seamlessly integrates with Swift's concurrency lifecycle. In our example, a task modifier starts the loop in activate(), which is automatically canceled when the user leaves the screen.

Swift with SKIE
VStack {
// SwiftUI views ..
}
.task {
await viewModel.activate()
}
tip

Other implementations of Reactive Streams API (like Combine or RxSwift) can also be indirectly supported using custom wrappers.

Read more about Flows

Swift Code Bundling

SKIE can bundle manually written Swift code directly into the generated Kotlin framework similar to how it bundles the generated Swift code.

This feature's primary intended use case is to allow KMP developers to write custom Swift wrappers for their Kotlin API. These wrappers allow you to better work around the remaining limitations of the Kotlin/Swift interop that SKIE currently does not solve automatically.

The main advantage of bundling the wrappers directly into the Kotlin framework is that you can keep the entire API in a single Framework. Having only a single Framework simplifies the distribution process and makes using and maintaining the Kotlin API easier.

The distribution process is simplified because you need to distribute only a single Kotlin framework (instead of distributing two or modifying the code in the Swift repository). Also, the bundled Swift code is easier to keep in sync with the Kotlin code because it is compiled together with the Kotlin code.

Read more about Swift Code Bundling