Skip to content
← All posts

KMP 102 · Part 3

KMP-102 - Using Kotlin Code in Swift

By 8 min read

In the last post, we learned how to create an XCFramework from Kotlin code and explored some characteristics of the generated build types.

With that done, we can move forward and learn how Kotlin code compiled to Objective-C works, and how to consume it on iOS.

Exporting a ‘Hello world’ from Kotlin to iOS

To get started, let’s understand a few important points about how Kotlin code is converted to Objective-C and, as a result, how to use it on iOS.

Let’s create a simple HelloWorld in Kotlin:

//HelloWorld.kt commonMain
expect fun helloWorld(): String

//HelloWorld.apple.kt appleMain
actual fun helloWorld(): String = "Olá mundo Apple Main"

Now we need to compile an XCFramework and integrate it into Xcode. There are plenty of tutorials online about how to do this; for this demonstration, I followed the guide “How to Integrate Kotlin Multiplatform (KMP) into Your iOS Project”.

The basic steps are:

  1. Compile the XCFramework with ./gradlew assembleKotlinSharedXCFramework. NOTE: replace “KotlinShared” with the name of your XCFramework. We explained this in the previous articles.
  2. Configure the Xcode project to consume the generated XCFramework.
  3. Use the Kotlin code on iOS.

Once all the setup is done, we can move forward and create a very simple SwiftUI screen to consume the Kotlin code:

import SwiftUI
import KotlinShared

struct ContentView: View {
    @State private var showText = false

    var body: some View {
        Button("Show Text") { showText.toggle() }
        if showText { Text(HelloWorld_appleKt.helloWorld()) }
    }
}

As a result, we get:

What’s going on here?

Let’s understand what’s happening behind the scenes:

  1. The Kotlin code is compiled to Objective-C and packaged into an XCFramework.
  2. The XCFramework is integrated into the Xcode project.
  3. With the XCFramework integrated, we can import the Kotlin code on iOS using import KotlinShared.
  4. Inside KotlinShared (the name of the XCFramework), we have access to the Kotlin code compiled to Objective-C.
  5. The HelloWorld_appleKt class is generated automatically by Kotlin/Native, giving us access to the helloWorld() method.
  6. And so we can use the Kotlin code on iOS!
import KotlinShared

let helloWorld = HelloWorld_appleKt.helloWorld()

But if you look closely, the syntax for accessing the Kotlin code on iOS is pretty strange. HelloWorld_appleKt.helloWorld() is far from idiomatic Swift.

Let’s take a closer look at this.

Understanding the code generated by Kotlin/Native

The biggest limitation in Kotlin/Native today is interoperability with Objective-C. Kotlin/Native can’t generate code that is 100% Swift-compatible.

That’s because Kotlin/Native is a compiler that generates Objective-C code, not Swift. The generated code is Objective-C-compatible, not Swift.

In other words, we have several Kotlin features translated directly to Swift (such as high order functions, enums, etc.), but we don’t have a direct Kotlin —> Objective-C translation.

To investigate how Kotlin code is translated to Objective-C, we can look at the code generated by Kotlin/Native. To do that, just cmd + click on our HelloWorld_appleKt class:

Hello world in Obj-C

To improve the experience of using Kotlin code on iOS, we can write our Kotlin code in a different way, so it’s more idiomatic to Swift.

Improving interoperability with Swift

We’ve seen that we can’t just write Kotlin code and expect it to be idiomatic to Swift, because of the fact that Kotlin/Native only generates Objective-C code.

For that, we have to write our Kotlin code in a way that’s more Swift-friendly. Let’s refactor the HelloWorld code to be more idiomatic to Swift:

// HelloWorld.apple.kt appleMain
package br.com.rsicarelli.example

@HiddenFromObjC
actual fun helloWorld(): String = "Olá mundo Apple Main"

object HelloWorld

fun HelloWorld.get(): String = helloWorld()

Now we go through the same steps to use it in Xcode:

  1. Compile the XCFramework with ./gradlew assembleKotlinSharedXCFramework.
  2. In Xcode, Products > Build for ... > Running, or simply cmd + shift + r

Right after the build, we notice that our previous HelloWorld_appleKt class is no longer available. Broken hello world in Xcode

Before understanding why, let’s integrate our KMP code using the new approach:

import KotlinShared

struct ContentView: View {
    @State private var showText = false

    var body: some View {
        Button("Show Text") { showText.toggle() }
        if showText { Text(HelloWorld.shared.get()) }
    }
}

Success! This code is more idiomatic to Swift, and we can now use the Kotlin code on iOS in a more natural way.

If we open the Objective-C code generated by Kotlin/Native, we notice some differences: Hello world idiomatic to Swift

Interestingly, our HelloWorld class is now generated as a Singleton, and the get method is generated as an extension!

What about the @HiddenFromObjC annotation?

The @HiddenFromObjC annotation is a Kotlin/Native annotation that indicates the method should not be exposed to Objective-C. This is useful for methods that shouldn’t be accessed directly from Objective-C, such as extension functions.

The reasoning behind using this annotation in this context is as follows: we have two ways to access the helloWorld() method:

  • Through the high-level function (high order function in Kotlin)
  • Through the extension of the HelloWorld object

In this case, exposing both ways to Objective-C doesn’t make sense, since the high-level function just delegates to the extension of the HelloWorld object. This can be confusing for whoever is consuming the Kotlin code on iOS.

So we use the @HiddenFromObjC annotation to hide the high-level function from Objective-C, and expose only the extension of the HelloWorld object!

Important notes:

  • The @HiddenFromObjC annotation is a Kotlin/Native annotation, which means we can’t use it in any other KMP source set.
  • The @HiddenFromObjC annotation can be used on functions, classes, attributes, etc.

Full documentation on interoperability between Kotlin and Objective-C can be found here: Interoperability with Swift/Objective-C.

Other ways to improve interoperability

This approach already works very well, but it can be pretty tedious to have to create an extension for every function we want to expose to iOS.

In the end, what we want is to have Kotlin code that’s idiomatic to Swift, but at the same time write Kotlin with all its potential.

For that, we have three options:

  1. Use the SKIE (Swift Kotlin Interface Enhancer) plugin
  2. Upgrade to Kotlin 2.1 and use the new Kotlin —> Swift interoperability system.
  3. Manually export extensions for every access we want to use on iOS, using Swift.

The first option is the most robust and the most recommended, since SKIE has a whole set of features that make interoperability between Kotlin and Swift easier.

The second option, exporting Swift code using Kotlin 2.1, is still experimental and not recommended for production.

The third way is very manual and can be quite tedious, but it’s a valid option for anyone who doesn’t want to use SKIE. As KMP devs, we want to write as little code as possible, so it’s a costly approach to scale.

For this article, we’ll use SKIE to improve interoperability between Kotlin and Swift!

Using SKIE to improve interoperability

Integrating SKIE into a KMP module is pretty straightforward, and the project provides detailed documentation about the integration: SKIE > Installation

But in short:

  1. Apply the co.touchlab.skie plugin in the build.gradle.kts of the KMP project
  2. The plugin should be applied only to the module that generates the XCFramework.

That’s basically it: apply the plugin and sync.

Now, let’s go back to our previous approach and just export the helloWorld() function (without the @HiddenFromObjC annotation):

// HelloWorld.apple.kt appleMain

actual fun helloWorld(): String = "Olá mundo Apple Main"

We follow the steps to use it in Xcode:

  1. Compile the XCFramework with ./gradlew assembleKotlinSharedXCFramework.
  2. On my machine I needed a clean build in Xcode, so Products > Clean Build Folder
  3. In Xcode, Products > Build for ... > Running, or simply cmd + shift + r

Now we can use the Kotlin code on iOS in a way that’s more idiomatic to Swift:

import SwiftUI
import KotlinShared

struct ContentView: View {
    @State private var showText = false

    var body: some View {
        Button("Show Text") { showText.toggle() }
        if showText { Text(helloWorld()) }
    }
}

Looking at the helloWorld() function, we notice that SKIE generates a global function that is accessible directly in Swift. This global function accesses Kotlin’s helloWorld() function (in its “ugly” form) and exposes it to Swift.

Much better, right? Now we can use the Kotlin code on iOS in a way that’s idiomatic to Swift!

Considerations about SKIE

SKIE is extremely powerful and makes interoperability between Kotlin and Swift much easier.

However, it’s important to remember that SKIE is an experimental plugin, and is subject to change and deprecations.

On top of that, since it adds an extra conversion layer, the XCFramework build is degraded, and build time can increase considerably.

That’s because SKIE walks through all of your Kotlin code and creates its Swift counterpart, which can be a pretty costly process. SKIE will do this not only with your Kotlin code, but also with all the dependencies you export as “api” to KotlinShared.

Reducing SKIE build time using annotations

A really nice SKIE feature is the ability to choose which SKIE features you want to use.

For that, SKIE provides a set of annotations that let you customize the export of Kotlin code to Swift. This allows us to cherry-pick exactly which code we want to export to Swift, and reduce SKIE’s build time.

Final thoughts

With this article, we managed to understand how to use Kotlin code in Swift, its characteristics and limitations, and how to improve interoperability between Kotlin and Swift with an alternative way of writing Kotlin code or by using SKIE.

KMP is excellent at exporting Objective-C code, but we’re currently limited when it comes to exporting Swift code. With SKIE, we can improve this limitation and export Kotlin code in a way that’s more idiomatic to Swift. And, in the upcoming Kotlin versions, interoperability between Kotlin and Swift will be even more robust and native.

I hope you enjoyed the article! 🚀

See you next time 🤙