I would like to include a Kotlin file that only performs data processing and network operations in an existing iOS project, while keeping native iOS UI code.
While I thought that this may be achievable with Kotlin/Native, the iOS samples (1,2) that I found that use Kotlin/Native seem to take over the iOS UI code as well.
Is including a Kotlin file for data transfer in iOS possible with Kotlin/Native without touching the UI code, and if so, what are the steps to do so?
Yes, it is possible in a cross-platform project to transfer data between Kotlin and native iOS UI Code by using Kotlin/Native. This allows to have a common code base for the data model based on Kotlin, while e.g. continuing to use native UI code for iOS.
The original proof:
The project https://github.com/justMaku/Kotlin-Native-with-Swift pointed me in the right direction, since it shows the essential steps to do so:
In a Swift UIViewController, it calls a wrapper function that shall receive a string from a Kotlin function. The call is mediated through a C++ layer, which itself starts the Kotlin runtime, passes the request to a Kotlin function, receives the string from it, and passes it back to the Swift UIViewController, which then displays it.
On the technical level, the project contains a script that compiles the Kotlin, C++, and Kotlin/Native part into a static library, which then can be called from the native iOS project.
To get the code to run, I had (after cloning from git) to perform a "git submodule sync" before running "./setup.sh".
To transfer data with a data model based on Kotlin, I would like to have a generic function, that can pass data to Kotlin, modify that data, and return the result back to the native iOS code. As a proof of principle, that such a function can be build, I extended the project to not only receive a string from Kotlin, but send one to Kotlin, append it, and send the result back.
Extension of the project:
Since there were some roadblocks in this seemingly simple extension, I lay out the steps for anybody interested. If you follow along, you should get the following displayed:
The text may be stupid, but it tells you, what happens.
The changes in ViewController.swift in the function viewDidAppear are:
let swiftMessage: String = "Hello Kotlin, this is Swift!"
let cStr = swiftMessage.cString(using: String.Encoding.utf8)
if let retVal = kotlin_wrapper(cStr) {
let string = String(cString: retVal)
...
}
You see the text that Swift sends to Kotlin in the wrapper function (in the end, the resulting 'string' variable will be displayed). One could directly pass the Swift String to the wrapper, but I wanted to highlight that the wrapper will consider the input and output as c-strings. Indeed, the file Kotlin Native-Bridging-Header.h inside the native iOS project now becomes:
extern const char* kotlin_wrapper(const char* swiftMessage);
On it goes to the file Launcher.cpp. Since the original file used a KString as result value of kotlin_main, I tried for some time to convert const char* to KString and pass that to kotlin_main. In the end I found, that it is much simpler to directly transfer the const char* variables to Kotlin, and do the transformation there with the functions that are given to us by Kotlin/Native.
My Launcher.cpp then became more compact than the original. Here is the complete file:
#include "Memory.h"
#include "Natives.h"
#include "Runtime.h"
#include "KString.h"
#include <stdlib.h>
#include <string>
extern "C" const char* kotlin_main(const char* swiftMessageChar);
extern "C" const char* kotlin_wrapper(const char* swiftMessageChar) {
RuntimeState* state = InitRuntime();
if (state == nullptr) {
return "Failed to initialize the kotlin runtime";
}
const char* exitMessage = kotlin_main(swiftMessageChar);
DeinitRuntime(state);
return exitMessage;
}
You see how the wrapper first starts the Kotlin runtime and then calls the function kotlin_main, which resides in the file kotlin.kt:
import konan.internal.ExportForCppRuntime
import kotlinx.cinterop.CPointer
import kotlinx.cinterop.ByteVar
import kotlinx.cinterop.cstr
import kotlinx.cinterop.nativeHeap
import kotlinx.cinterop.toKString
@ExportForCppRuntime
fun kotlin_main(cPtr: CPointer<ByteVar>): CPointer<ByteVar> {
val swiftMessage = cPtr.toKString()
val kotlinMessage = "Hello Swift, I got your message: '$swiftMessage'."
val returnPtr = kotlinMessage.cstr.getPointer(nativeHeap)
return returnPtr
}
The pointer is converted to a Kotlin String, and then used in the creation of the kotlinMessage (the example of a data transformation). The result message is then transformed back to a pointer, and passed through the wrapper back to the Swift UIViewController.
Where to go from here?
In principle, one could use this framework without touching the C++ layer again. Just define pack and unpack functions, that pack arbitrary data types into a string and unpack the string to the respective data type on the other side. Such pack and unpack functions have to be written only once per language, and can be reused for different projects, if done sufficiently generic. In practice, I probably would first rewrite the above code to pass binary data, and then write the pack and unpack functions to transform arbitrary data types to and from binary data.
You can use kotlin as a framework if you want, so the kotlin code stays in framework file so you can use some common code on both android and iOS without writing your complete iOS app in kotlin.
Use gradle to build your kotlin code in objc/swift compatible framework
In your build.gradle file
buildscript {
ext.kotlin_native_version = '0.5'
repositories {
mavenCentral()
maven {
url "https://dl.bintray.com/jetbrains/kotlin-native-dependencies"
}
}
dependencies {
classpath "org.jetbrains.kotlin:kotlin-native-gradle-plugin:$kotlin_native_version"
}
}
group 'nz.salect'
version '0.1'
apply plugin: "konan"
konan.targets = ["iphone", "iphone_sim"]
konanArtifacts {
framework('nativeLibs')
}
It will generate two .framework files, one for simulator other for the actual device, put the framework in your project and link that to your project as any other third party framework.
Cmd: ./gradlew build
Note: Every time you change your kotlin files build and replace your
framework file as well(you can create a shell script and add that to
build phases to do that automatically).
Cheers !!!