Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Kotlin File in Native iOS Project with Kotlin/Native

Tags:

ios

kotlin

native

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?

like image 627
CryptUser Avatar asked Dec 01 '17 15:12

CryptUser


People also ask

Is Kotlin Native for iOS?

The Kotlin/Native compiler can produce a framework for macOS and iOS out of the Kotlin code. The created framework contains all declarations and binaries needed to use it with Objective-C and Swift. The best way to understand the techniques is to try it for ourselves.

Can Kotlin compile to Native?

Yes. Kotlin is supported as a first-class language on Android.

Is Kotlin good for iOS?

As a standalone language, it provides excellent features over Java, but it also works well in conjunction with the older language. Kotlin's multiplatform capabilities allow developers to share code, logic, and data across several platforms: IOS, Android, Web, and more.

How do I run Kotlin app on iOS?

Run on a different iPhone simulated deviceClick the + button above the list of configurations and select iOS Application. Name your configuration. Select a simulated device in the Execution target list, and then click OK. Click Run to run your application on the new simulated device.


1 Answers

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:

Kotlin <-> Swift

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.

like image 147
CryptUser Avatar answered Sep 18 '22 00:09

CryptUser