Kotlin/Native as an Apple framework – tutorial
Kotlin/Native provides bidirectional interoperability with Swift/Objective-C. You can both use Objective-C frameworks and libraries in Kotlin code, and Kotlin modules in Swift/Objective-C code.
Kotlin/Native comes with a set of pre-imported system frameworks; it's also possible to import an existing framework and use it from Kotlin. In this tutorial, you'll learn how to create your own framework and use Kotlin/Native code from Swift/Objective-C applications on macOS and iOS.
In this tutorial, you will:
You can use the command line to generate a Kotlin framework, either directly or with a script file (such as .sh
or .bat
file). However, this approach doesn't scale well for big projects that have hundreds of files and libraries. Using a build system simplifies the process by downloading and caching the Kotlin/Native compiler binaries and libraries with transitive dependencies and run the compiler and tests. Kotlin/Native can use the Gradle build system through the Kotlin Multiplatform plugin.
Create a Kotlin library
The Kotlin/Native compiler can produce a framework for macOS and iOS from the Kotlin code. The created framework contains all declarations and binaries needed to use it with Swift/Objective-C.
Let's first create a Kotlin library:
In the
src/nativeMain/kotlin
directory, create thelib.kt
file with the library contents:package example object Object { val field = "A" } interface Interface { fun iMember() {} } class Clazz : Interface { fun member(p: Int): ULong? = 42UL } fun forIntegers(b: Byte, s: UShort, i: Int, l: ULong?) { } fun forFloats(f: Float, d: Double?) { } fun strings(str: String?) : String { return "That is '$str' from C" } fun acceptFun(f: (String) -> String?) = f("Kotlin/Native rocks!") fun supplyFun() : (String) -> String? = { "$it is cool!" }Update your
build.gradle(.kts)
Gradle build file with the following:plugins { kotlin("multiplatform") version "2.1.0" } repositories { mavenCentral() } kotlin { iosArm64("native") { binaries { framework { baseName = "Demo" } } } } tasks.wrapper { gradleVersion = "8.10" distributionType = Wrapper.DistributionType.ALL }plugins { id 'org.jetbrains.kotlin.multiplatform' version '2.1.0' } repositories { mavenCentral() } kotlin { iosArm64("native") { binaries { framework { baseName = "Demo" } } } } wrapper { gradleVersion = "8.10" distributionType = "ALL" }The
binaries {}
block configures the project to generate a dynamic or shared library.Kotlin/Native supports the
iosArm64
,iosX64
, andiosSimulatorArm64
targets for iOS, as well asmacosX64
andmacosArm64
targets for macOS. So, you can replace theiosArm64()
with the respective Gradle function for your target platform:Target platform/device
Gradle function
macOS x86_64
macosX64()
macOS ARM64
macosArm64()
iOS ARM64
iosArm64()
iOS Simulator (x86_64)
iosX64()
iOS Simulator (ARM64)
iosSimulatorArm64()
For information on other supported Apple targets, see Kotlin/Native target support.
Run the
linkDebugFrameworkNative
Gradle task in the IDE or use the following console command in your terminal to build the framework:./gradlew linkDebugFrameworkNative
The build generates the framework into the build/bin/native/debugFramework
directory.
Generated framework headers
Each framework variant contains a header file. The headers don't depend on the target platform. Header files contain definitions for your Kotlin code and a few Kotlin-wide declarations. Let's see what's inside.
Kotlin/Native runtime declarations
In the build/bin/native/debugFramework/Demo.framework/Headers
directory, open the Demo.h
header file. Take a look at Kotlin runtime declarations:
Kotlin classes have a KotlinBase
base class in Swift/Objective-C that extends the NSObject
class there. There are also wrappers for collections and exceptions. Most of the collection types are mapped to similar collection types in Swift/Objective-C:
Kotlin | Swift | Objective-C |
---|---|---|
List | Array | NSArray |
MutableList | NSMutableArray | NSMutableArray |
Set | Set | NSSet |
MutableSet | NSMutableSet | NSMutableSet |
Map | Dictionary | NSDictionary |
MutableMap | NSMutableDictionary | NSMutableDictionary |
Kotlin numbers and NSNumber
The next part of the Demo.h
file contains type mappings between Kotlin/Native number types and NSNumber
. The base class is called DemoNumber
in Objective-C and KotlinNumber
in Swift. It extends NSNumber
.
For each Kotlin number type, there is a corresponding predefined child class:
Kotlin | Swift | Objective-C | Simple type |
---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Every number type has a class method to create a new instance from the corresponding simple type. Also, there is an instance method to extract a simple value back. Schematically, all such declarations look like that:
Here, __TYPE__
is one of the simple type names, and __CTYPE__
is the corresponding Objective-C type, for example, initWithChar(char)
.
These types are used to map boxed Kotlin number types to Swift/Objective-C. In Swift, you can call the constructor to create an instance, for example, KotlinLong(value: 42)
.
Classes and objects from Kotlin
Let's see how class
and object
are mapped to Swift/Objective-C. The generated Demo.h
file contains the exact definitions for Class
, Interface
, and Object
:
Objective-C attributes in this code help use the framework from both Swift and Objective-C languages. DemoInterface
, DemoClazz
, and DemoObject
are created for Interface
, Clazz
, and Object
, respectively.
The Interface
is turned into @protocol
, while both a class
and an object
are represented as @interface
. The Demo
prefix comes from the framework name. The nullable return type ULong?
is turned into DemoULong
in Objective-C.
Global declarations from Kotlin
All global functions from Kotlin are turned into DemoLibKt
in Objective-C and into LibKt
in Swift, where Demo
is the framework name set by the -output
parameter of kotlinc-native
:
Kotlin String
and Objective-C NSString*
are mapped transparently. Similarly, Unit
type from Kotlin is mapped to void
. The primitive types are mapped directly. Non-nullable primitive types are mapped transparently. Nullable primitive types are mapped to Kotlin<TYPE>*
types, as shown in the table. Both higher-order functions acceptFunF
and supplyFun
are included and accept Objective-C blocks.
You can find more information about type mapping in Interoperability with Swift/Objective-C.
Garbage collection and reference counting
Swift and Objective-C use automatic reference counting (ARC). Kotlin/Native has its own garbage collector, which is also integrated with Objective-C/Swift ARC.
Unused Kotlin objects are automatically removed. You don't need to take additional steps to control the lifetime of Kotlin/Native instances from Swift or Objective-C.
Use code from Objective-C
Let's call the framework from Objective-C. In the framework directory, create the main.m
file with the following code:
Here, you call Kotlin classes directly from Objective-C code. A Kotlin object uses the <object name>.shared
class property, which allows you to get the object's only instance and call object methods on it.
The widespread pattern is used to create an instance of the Clazz
class. You call the [[ DemoClazz alloc] init]
on Objective-C. You can also use [DemoClazz new]
for constructors without parameters.
Global declarations from the Kotlin sources are scoped under the DemoLibKt
class in Objective-C. All Kotlin functions are turned into class methods of that class.
The strings
function is turned into DemoLibKt.stringsStr
function in Objective-C, so you can pass NSString
directly to it. The return value is visible as NSString
too.
Use code from Swift
The framework you generated has helper attributes to make it easier to use with Swift. Let's convert the previous Objective-C example into Swift.
In the framework directory, create the main.swift
file with the following code:
There are some small differences between the original Kotlin code and its Swift version. In Kotlin, any object declaration has only one instance. The Object.shared
syntax is used to access this single instance.
Kotlin function and property names are translated as is. Kotlin's String
is turned into Swift's String
. Swift hides NSNumber*
boxing too. You can also pass a Swift closure to Kotlin and call a Kotlin lambda function from Swift.
You can find more information about type mapping in Interoperability with Swift/Objective-C.
Connect the framework to your iOS project
Now you can connect the generated framework to your iOS project as a dependency. There are multiple ways to set it up and automate the process, choose the method that suits you best: