From Kotlin to Native: Or how Kotlin concepts are mapped to the Apple framework
TL;DR This post is about how some specific Kotlin features are compiled to Objective-C and how they can be used in a Swift project when using Kotlin Native to build an Apple framework.
Recently, I have started exploring and experimenting with Kotlin Native and, till now, my main focus was on activities related with the preparation of a project. As part of this, I managed to write a few posts about activities like the project setup, unit tests, code quality tools and continuous integration.
Now, it’s time to dig deeper and investigate on how things are working under the hood. So, in this post I will focus on exploring how specific Kotlin concepts like functions, enumerations, sealed classes, generics and others are mapped to Objective-C when building the Apple framework and how they can be used in a Swift project that uses this framework.
Project
Before we start, it is worth pointing out that all the code used in this post can be found on a repo hosted on GitHub. It’s a Kotlin Native project with the bare minimum setup alongside an iOS project that embeds the generated Apple framework, and it has the following structure:
.
├── build
│ ├── ..
│ └── ..
├── build.gradle.kts
├── gradle
│ └── ...
├── gradlew
├── gradlew.bat
├── iOSPlayground
│ ├── iOSPlayground
│ │ ├── AppDelegate.swift
│ │ ├── Assets.xcassets
│ │ │ ├── ...
│ │ │ └── ...
│ │ ├── Base.lproj
│ │ │ ├── LaunchScreen.storyboard
│ │ │ └── Main.storyboard
│ │ ├── Info.plist
│ │ └── ViewController.swift
│ └── iOSPlayground.xcodeproj
│ ├── ...
│ └── ...
└── src
├── commonMain
│ └── kotlin
│ └── common.kt
└── nativeMain
└── kotlin
└── ios.kt
where src
directory contains the K/N module and the iOSPlayground
directory contains the iOS app, inside which there is a file named ViewController.swift
that contains all the Swift code.
To generate the Apple framework, you have to run ./gradlew linkNative
on the root folder of the project and the generated framework will be produced in the following directory:
build/bin/native/debugFramework/Playground.framework
Playground
Functions: high-level, extensions, variadic
Let’s start with functions and define three of them in the common.kt
file. One will be an extension to the Int
type, one will be a high-level function and the last one will be a variadic function.
fun Int.squared(): Int {
return this * 2
}
fun highLevelFunction(): String {
return "I'am a high-level-function"
}
fun average(vararg values: Int): Double {
return values.average()
}
Then, run ./gradlew linkNative
and check the header file (Playground.h
) from the framework, which is supposed to contain something like the following snippet:
__attribute__((objc_subclassing_restricted))
__attribute__((swift_name("CommonKt")))
@interface PlaygroundCommonKt : KotlinBase
+ (int32_t)squared:(int32_t)receiver __attribute__((swift_name("squared(_:)")));
+ (NSString *)highLevelFunction __attribute__((swift_name("highLevelFunction()")));
+ (double)averageValues:(PlaygroundKotlinIntArray *)values __attribute__((swift_name("average(values:)")));
@end;
Based on the content of this file, we can figure out how to use these functions in a Swift project. For example, we have to use the CommonKt.
before the functions and the variadic function well… is no longer a variadic function but rather expect a single parameter of type KotlinIntArray
. An example of using these functions on a Swift project is the following snippet:
// MARK: - Functions
let squared = CommonKt.squared(2)
print("Square of 2 equals \(squared)")
/// Output: Square of 2 equals 4
print(CommonKt.highLevelFunction())
/// Output: I'am a high-level-function
let array = KotlinIntArray(size: 3)
array.set(index: 0, value: 12)
array.set(index: 1, value: 10)
array.set(index: 2, value: 11)
let average = CommonKt.average(values: array)
print("The average of 12, 10 and 11 is: \(average)")
/// Output: "The average of 12, 10 and 11 is: 11.0
Enumerations
Let’s continue to Kotlin’s enum classes. For this example, I will make use of an enum with programming languages which will also have a single parameter, the year of the first appearance of each language, like the following snippet:
enum class Languages(val sinceYear: String) {
OBJC("1984"),
SWIFT("2014"),
KOTLIN("2011")
}
If you compile to the native framework, you will get the following content on the header file of the framework:
__attribute__((objc_subclassing_restricted))
__attribute__((swift_name("Languages")))
@interface PlaygroundLanguages : PlaygroundKotlinEnum<PlaygroundLanguages *>
+ (instancetype)alloc __attribute__((unavailable));
+ (instancetype)allocWithZone:(struct _NSZone *)zone __attribute__((unavailable));
@property (class, readonly) PlaygroundLanguages *objc __attribute__((swift_name("objc")));
@property (class, readonly) PlaygroundLanguages *swift __attribute__((swift_name("swift")));
@property (class, readonly) PlaygroundLanguages *kotlin __attribute__((swift_name("kotlin")));
- (instancetype)initWithName:(NSString *)name ordinal:(int32_t)ordinal __attribute__((swift_name("init(name:ordinal:)"))) __attribute__((objc_designated_initializer)) __attribute__((unavailable));
- (int32_t)compareToOther:(PlaygroundLanguages *)other __attribute__((swift_name("compareTo(other:)")));
@property (readonly) NSString *sinceYear __attribute__((swift_name("sinceYear")));
@end;
From the ObjC header, it’s obvious that the enum
is not actually an enum
but a class and each case is a class parameter.
Given this, an example of how we can use it in a Swift project is the following snippet
// MARK: - Enums
let swift = Languages.swift
let kotlin = Languages.kotlin
let lang = Languages.objc
print("Swift's first appearance: \(swift.sinceYear) \nKotlin's first appearance: \(kotlin.sinceYear)")
/// Output: Swift's first appearance: 2014
/// Kotlin's first appearance: 2011
if case Languages.swift = lang {
print("Swift")
} else {
print("Not Swift")
}
/// Output: Not Swift
On the bright side, it turns out that it conforms to the Comparable
protocol thanks to the KotlinEnum
class, so we can make use of Swift’s switch
or if case
statements.
Sealed Classes & Generics
The next to take a look at are Sealed classes and Generics. Before we move on, is worth mentioning that Generics is a feature added with Kotlin 1.3.40 and it is disabled by default. In order to enable this feature, you have to first make sure that you are using a version of Kotlin greater or equal to 1.3.40 and that you add the freeCompilerArgs.add("-Xobjc-generics")
on your build.gradle.kts
like in the following snippet.
iosX64("native") {
binaries {
framework {
baseName = "Playground"
freeCompilerArgs.add("-Xobjc-generics")
}
}
}
For this example, I will make use of the common scenario of a Tree that contains Nodes.
// Sealed class & generic
sealed class Tree<T> {
data class Node<T>(var value: T,
var left: Tree<T> = None(),
var right: Tree<T> = None()): Tree<T>()
class None<T>: Tree<T>()
}
The header of the generated framework, after running ./gradlew linkNative
, will be like:
__attribute__((swift_name("Tree")))
@interface PlaygroundTree<T> : KotlinBase
@end;
__attribute__((objc_subclassing_restricted))
__attribute__((swift_name("TreeNode")))
@interface PlaygroundTreeNode<T> : PlaygroundTree<T>
- (instancetype)initWithValue:(T _Nullable)value left:(PlaygroundTree<T> *)left right:(PlaygroundTree<T> *)right __attribute__((swift_name("init(value:left:right:)"))) __attribute__((objc_designated_initializer));
- (BOOL)isEqual:(id _Nullable)other __attribute__((swift_name("isEqual(_:)")));
- (NSUInteger)hash __attribute__((swift_name("hash()")));
- (NSString *)description __attribute__((swift_name("description()")));
- (T _Nullable)component1 __attribute__((swift_name("component1()")));
- (PlaygroundTree<T> *)component2 __attribute__((swift_name("component2()")));
- (PlaygroundTree<T> *)component3 __attribute__((swift_name("component3()")));
- (PlaygroundTreeNode<T> *)doCopyValue:(T _Nullable)value left:(PlaygroundTree<T> *)left right:(PlaygroundTree<T> *)right __attribute__((swift_name("doCopy(value:left:right:)")));
@property T _Nullable value __attribute__((swift_name("value")));
@property PlaygroundTree<T> *left __attribute__((swift_name("left")));
@property PlaygroundTree<T> *right __attribute__((swift_name("right")));
@end;
__attribute__((objc_subclassing_restricted))
__attribute__((swift_name("TreeNone")))
@interface PlaygroundTreeNone<T> : PlaygroundTree<T>
- (instancetype)init __attribute__((swift_name("init()"))) __attribute__((objc_designated_initializer));
+ (instancetype)new __attribute__((availability(swift, unavailable, message="use object initializers instead")));
@end;
And we can use it as in the following Swift snippet:
func description(for node: TreeNode<NSString>) -> String {
var description = "Node '\(node.value as! String)'"
if node.right is TreeNone<NSString> && node.left is TreeNone<NSString> {
description.append(" doesn't have any child")
} else if let leftChild = node.left as? TreeNode<NSString>,
let rightChild = node.right as? TreeNode<NSString> {
description.append(" has children '\(leftChild.value as! String)' and '\(rightChild.value as! String)'")
}
return description
}
let leftChild = TreeNode<NSString>(value: "Left Child", left: TreeNone(), right: TreeNone())
let rightChild = TreeNode<NSString>(value: "Right Child", left: TreeNone(), right: TreeNone())
let parent = TreeNode<NSString>(value: "Parent", left: leftChild, right: rightChild)
print(description(for: leftChild))
print(description(for: parent))
/// Output: Node 'Left Child' doesn't have any child
/// Node 'Parent' has children 'Left Child' and 'Right Child'
Note that the generic type should be a class, so that’s why NSString is used instead of Swift’s String.
Interfaces & Inheritance
The next on the list is interfaces & inheritance. Again, I will use a common example, the shapes. In the following snippet I define an interface and two implementations of it.
// Interfaces / Inheritance
interface Shape {
fun area(): Float
}
class Square(val side: Float) : Shape {
override fun area() = side.pow(2)
}
open class Rect(val width: Float, val height: Float) : Shape {
override fun area() = width * height
}
As you may have noticed, one of these implementation classes has the open
keyword, which means that it can be subclassed. If you don’t add the open
keyword, you will not be able to create a subclass, which is obvious by the ObjC header objc_subclassing_restricted
attribute.
__attribute__((swift_name("Shape")))
@protocol PlaygroundShape
@required
- (float)area __attribute__((swift_name("area()")));
@end;
__attribute__((objc_subclassing_restricted))
__attribute__((swift_name("Square")))
@interface PlaygroundSquare : KotlinBase <PlaygroundShape>
- (instancetype)initWithSide:(float)side __attribute__((swift_name("init(side:)"))) __attribute__((objc_designated_initializer));
- (float)area __attribute__((swift_name("area()")));
@property (readonly) float side __attribute__((swift_name("side")));
@end;
__attribute__((swift_name("Rect")))
@interface PlaygroundRect : KotlinBase <PlaygroundShape>
- (instancetype)initWithWidth:(float)width height:(float)height __attribute__((swift_name("init(width:height:)"))) __attribute__((objc_designated_initializer));
- (float)area __attribute__((swift_name("area()")));
@property (readonly) float width __attribute__((swift_name("width")));
@property (readonly) float height __attribute__((swift_name("height")));
@end;
The Swift implementation is straight-forward and an example of this could be the following snippet.
// MARK: - Interfaces & Inheritance
class Circle: Shape {
func area() -> Float {
return Float.pi * radius * radius
}
let radius: Float
init(radius: Float) {
self.radius = radius
}
}
class DummyRect: Rect {
override func area() -> Float {
return width * height * 1.3
}
}
let square = Square(side: 4)
let rect = Rect(width: 2, height: 2)
let dummyRect = DummyRect(width: 2, height: 2)
let shapes: Array<Shape> = [square, rect, dummyRect]
shapes.forEach { shape in
print("Shape area is \(shape.area())")
/// Outputs: Shape area is 16.0
/// Shape area is 4.0
/// Shape area is 5.2
}
The only thing to notice is that we have to define the type of the Array since, by default, an array of type KotlinBase
is created.
Expect/Actual keywords a.k.a Platform-Specific code
Often times, we have to provide some platform specific code. For example, we may want to get the device’s platform and version. To cover for these scenarios we use the expect
/actual
keywords. In the common.kt
, we can declare a class, a function or a variable with the expect
keyword and keep the body empty, like we would do for an interface. An example of such function is the following snippet.
// Expect
expect fun platformName(): String
Now, in the ios.kt
file, we make use of the actual
keyword to provide the implementation of the class, function or variable defined with the expect
keyword
For this example, we will add the following snippet on the ios.kt
file:
import platform.UIKit.UIDevice
actual fun platformName(): String {
return UIDevice.currentDevice.systemName() +
" " +
UIDevice.currentDevice.systemVersion
}
After compiling, the header of the generated framework will look like this:
__attribute__((objc_subclassing_restricted))
__attribute__((swift_name("IosKt")))
@interface PlaygroundIosKt : KotlinBase
+ (NSString *)platformName __attribute__((swift_name("platformName()")));
@end;
And we can use it in Swift in the following way:
// MARK: - Platform Specific Code
print("platform is \(IosKt.platformName())")
/// Outputs: platform is iOS 12.4
And that’s it for this post.
Wrap-up
To sum up, in this post, we have seen a few cases of how Kotlin features are compiled to Obj-C and how we can use them in a Swift project. As we saw in a few examples, some features are not compiled exactly as we would expect and their usage is not so straight-forward. Therefore, a project like this playground can be really useful in many cases.
The smaller scope and size of this kind of projects make them suitable for experimenting; be it a new implementation or a new feature introduced on a newer Kotlin version just like I did with the -Xobjc-generics
argument that I mentioned before. And also they are a good candidate for reference purposes.
Thanks for reading, I hope that you found this post useful and should you have any suggestions on how to enrich this playground or any other questions or comments, just let me know on Twitter or by email!